SignalKit is a lightweight event and binding framework.
It provides you with a single unified API that lets you observe for KVO, Target-Action, NSNotificationCenter and custom event streams. You can then build a chain of operations on the incoming values using methods like next
, map
, filter
, debounce
or you can bind the value to a property of UI control.
Let’s observe and bind a signal of type String to the text property of UILabel:
let nameLabel = UILabel()
let name = ObservableProperty<String>("John")
name.observe().bindTo(textIn: nameLabel)
All the observations are made by calling a single method observe()
on the type that you wish to observe for changes. This method returns a type conforming to the SignalEventType
protocol and by using Protocol Oriented Programming SignalKit extends the SignalEventType
protocol to provide you with the available events for that type.
Now let’s observe an UIButton
for .TouchUpInside
control events, its easy as that:
let button = UIButton()
button.observe().tapEvent.next { _ in print("Tap!") }
Now in order to preserve the chain of operations we need to store it in a property or we can use an instance of the SignalBag
class:
let signalsBag = SignalBag()
...
name.observe()
.bindTo(textIn: nameLabel)
.addTo(signalsBag)
button.observe().tapEvent
.next { _ in print("Tap!") }
.addTo(signalsBag)
The addTo(...)
method will store the chain of signal operations to our signalsBag
and will return a disposable that we can use to remove the chain from the bag. The signalsBag
will handle for us the disposal of all observations and chain of operations on deinit.
How about observing a NSObject
using KVO:
// Person is a class that inherits from NSObject
let person = Person(name: "Jack")
person.observe()
.keyPath("name", value: person.name)
.next { print("Hello \($0)") }
.addTo(signalsBag)
As you know the KVO mechanism will return the value of the changed property as AnyObject
, but we want the type of the property that we are interested in, so we are using the value
parameter as the initial value and to specify the type of the property that we are interested in. SignalKit will perform an optional type cast for us and will dispatch the new value only if the type cast is successful, nice!
SignalKit comes with several special observation options for certain UIKit controls. Here is how we can observe a UIControl
for UIControlEvents
:
let slider = UISlider()
slider.observe()
.events(.ValueChanged)
.next{ print("New value: \($0.value)") }
.addTo(signalsBag)
Off course you can observe for multiple UIControlEvents
using the new in Swift 2.0 option set syntax: [.ValueChanged, .TouchUpInside]
As mentioned above SignalKit comes with special observation options for several UIKit controls, so we can observe the value changes in UISlider
like this:
slider.observe().valueChanges
.next { print("New value: \($0)") }
.addTo(signalsBag)
Notice that here we are getting back a signal of type Float with the current value of the slider.
Instead of printing the slider’s new value let's bind it to the text property of a UILabel
:
slider.observe().valueChanges
.map { "Value : \($0)" }
.bindTo(textIn: label)
.addTo(signalsBag)
At this point you may already guess how we are going to observe for notifications posted on the NSNotificationCenter
:
let center = NSNotificationCenter.defaultCenter()
center.observe()
.notification(UIKeyboardWillShowNotification)
.next { print($0.name) }
.addTo(signalsBag)
We can also observe for notifications posted by a certain object.
Wouldn’t it be great if there was a easy way to observe for keyboard notifications and to get the keyboard data that is posted by the system with the notification?
Well SignalKit comes with a handy Keyboard
structure that you can call the static method observe()
to observe for the keyboard events. When keyboard notification is posted by the system you will get back a signal of type KeyboardState
which you can query for the keyboard begin/end frames and animation curve and duration.
Keyboard.observe().willShow
.next { print($0.endFrame) }
.addTo(signalsBag)
Observable property is a thread safe Observable
implementation that have a notion of a current value. You can get the current value
and if you set a value it will be dispatched to all observers. Alternatively you can call dispatch(newValue)
to notify the observers:
// ViewModel
let name = ObservableProperty<String>("Jane")
// View/ViewController
name.observe()
.next { print("Name: \($0)") }
.addTo(signalsBag)
// prints "Name: Jane"
name.value = "John" // prints "Name: John"
SignalKit comes with the following SignalType
protocol extension operations:
Adds a new observer to a signal to perform a side effect:
name.observe().next { print($0) }
Transforms the signal to a signal of another type:
name.observe().map { $0.characters.count }.next { print($0) }
Filters the signal value using a predicate:
name.observe().filter { $0.characters.count > 3 }.next { print($0) }
Skip a certain number of signal values:
name.observe().skip(3).next { print($0) }
Deliver the signal on a given SignalScheduler.Queue
(dispatch queue):
// .MainQueue
// .UserInteractiveQueue
// .UserInitiatedQueue
// .UtilityQueue
// .BackgroundQueue
// .CustomQueue(dispatch_queue_t)
name.observe().deliverOn(.MainQueue).next { print($0) }
Sends only the latest values that are not followed by another values within the specified duration (seconds). You can also specify the SignalScheduler.Queue
on which to debounce which is by default the .MainQueue
:
name.observe().debounce(0.5).next { print($0) }
Delays the dispatch of the signal. Here you can also specify on which SignalScheduler.Queue
to delay the dispatch which is by default again the .MainQueue
:
name.observe().delay(0.2).next { print($0) }
Dispatches the new value only if it is not equal to the previous one (only for signals which type conforms to Equatable
protocol):
name.observe().distinct().next { print($0) }
Bind the value of the signal to an Observable
of the same type:
let anotherName = ObservableProperty<String>("")
name.observe().bindTo(anotherName)
Note: There are special bindTo(...) extensions for the UIKit UI components like UIView, UIControl and more.
Stores a chain of signal operations in a container that conforms to the SignalContainerType
protocol:
let signalsBag = SignalBag()
name.observe().next { print($0) }.addTo(signalsBag)
Combine the latest values of the current signal A and another signal B in a signal of type (A, B)
:
Let’s assume that we have emailField
, passwordField
and loginButton
controls and we want the loginButton
to be enabled only when both emailField
and passwordField
have valid content.
We can create two signals that observe for text changes in the fields and then map their text to a Boolean value using the functions isValidName
and isValidPassword
. Then we can combine the two signals to a signal of tuple type (Bool, Bool)
, map to Boolean true if both are equal to true and finally bind the resulting Boolean to the enabled property of UIButton
:
let emailField = UITextField()
let passwordField = UITextField()
let loginButton = UIButton()
let signalA = emailField.observe().text.map(isValidName)
let signalB = passwordField.observe().text.map(isValidPassword)
signalA.combineLatestWith(signalB)
.map { $0.0 == true && $0.1 == true }
.bindTo(enabled: loginButton)
.addTo(signalsBag)
A free function variant of the combineLatestWith
which combines two or three signals:
combineLatest(signalA, signalB)
.map { $0.0 == true && $0.1 == true }
.bindTo(enabled: loginButton)
.addTo(signalsBag)
Special operation on a signal of type (Bool, Bool)
or (Bool, Bool, Bool)
. Sends true if all values in a signal of tuple type are matching the predicate function. We can replace the above combineLatestWith
map operation with:
combineLatest(signalA, signalB)
.all { $0 == true }
.bindTo(enabled: loginButton)
.addTo(signalsBag)
Similar to all
, but send true if at least one value in a signal of tuple type (Bool, Bool)
or (Bool, Bool, Bool)
matches the predicate function:
combineLatest(signalA, signalB, signalC)
.some { $0 == true }
.bindTo(enabled: loginButton)
.addTo(signalsBag)
SignalKit comes with UIKit extensions that let you observe for different control events and to bind a signal to a property of UI component.
Take a look at SignalKit/Extensions/UIKit/
folder to explore the currently implemented observations and bindings for UIKit.
I will really love to include extensions for AppKit and WatchKit. Any help with that is welcome.
SignalKit requires Swift 2.0 and XCode 7 beta 5
Add the following line to your Cartfile
github "yankodimitrov/SignalKit"
Add the following line to your Podfile
pod “SignalKit”
- Support for more UIKit observations/bindings
- AppKit support
- WatchKit support
##License SignalKit is released under the MIT license. See the LICENSE.txt file for more info.