Skip to content

SignalKit is a type safe event and binding Swift framework with great focus on clean and readable API.

License

Notifications You must be signed in to change notification settings

shergin/SignalKit

 
 

Repository files navigation

SignalKit

SignalKit

Carthage compatible

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 DisposableBag class:

let signalsBag = DisposableBag()
...
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.

KVO

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!

Target-Action

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)

NSNotificationCenter

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.

Keyboard Events

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)

ObservablePropety

ObservablePropety is an Observable protocol implementation that have a notion of a current value. If you change the value it will be dispatched to all 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"

ObservableArray

When we alter the contents of the ObservableArray a new ObservableArrayEvent will be dispatched to all of its observers. We can also perform a batch update which will dispatch a single ObservableArrayEvent.Batch event with associated values for all inserted, updated and removed indexes.

UITableView and UICollectionView bindings

We can bind the changes in ObservableArray to UITableView or UICollectionView:

let list = ObservableArray([1, 2, 3])
let dataSource = MyCollectionViewDataSource(items: list)
let collectionView = UICollectionView(...)

list.observe()
	.bindTo(collectionView: collectionView, dataSource: dataSource)
    .addTo(signalsBag)

list.append(4)
list.removeAtIndex(0)

Note: If we need only one section of rows/items we can use a single dimensional array as shown above.

Batch Updates

Batch updates in ObservableArray will reflect to batch updates in the binded UITableView or UICollectionView:

let sectionOne = ObservableArray([1, 2])
let sectionTwo = ObservableArray([3, 4])
let list = ObservableArray<ObservableArray<Int>>([sectionOne, sectionTwo])

list.observe()
	.bindTo(tableView: tableView, dataSource: dataSource)
    .addTo(signalsBag)

list.performBatchUpdate { collection in
    
    collection[0].append(22)
    collection[0].removeAtIndex(0)
    collection.removeAtIndex(1)
}

Signal Operations

SignalKit comes with the following SignalType protocol extension operations:

next

Adds a new observer to a signal to perform a side effect:

name.observe().next { print($0) }

map

Transforms the signal to a signal of another type:

name.observe().map { $0.characters.count }.next { print($0) }

filter

Filters the signal value using a predicate:

name.observe().filter { $0.characters.count > 3 }.next { print($0) }

skip

Skip a certain number of signal values:

name.observe().skip(3).next { print($0) }

deliverOn

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) }

debounce

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) }

delay

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) }

distinct

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) }

bindTo

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.

addTo

Stores a chain of signal operations in a DisposableBag:

let signalsBag = DisposableBag()

name.observe().next { print($0) }.addTo(signalsBag)

combineLatestWith

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)

combineLatest

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)

all

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)

some

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)

UIKit

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.

AppKit & WatchKit

I will really love to include extensions for AppKit and WatchKit. Any help with that is welcome.

Installation

SignalKit requires Swift 2.0 and XCode 7

Carthage

Add the following line to your Cartfile

github "yankodimitrov/SignalKit" "master"

CocoaPods

Add the following line to your Podfile

pod “SignalKit”

Roadmap

  • 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.

About

SignalKit is a type safe event and binding Swift framework with great focus on clean and readable API.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Swift 97.5%
  • Objective-C 1.8%
  • Ruby 0.7%