CocoaPods trunk is moving to be read-only. Read more on the blog, there are 18 months to go.
TestsTested | ✓ |
LangLanguage | SwiftSwift |
License | MIT |
ReleasedLast Release | Mar 2017 |
SwiftSwift Version | 3.0 |
SPMSupports SPM | ✓ |
Maintained by Patrick Sturm.
Patrick Sturm, [email protected]
SwiftySignals is available under the MIT license. See the LICENSE file for more info.
SwiftySignals started as a simple framework with only a few classes. Up to SwiftySignals 2 concurrency was completely ignored and left to the user. With SwiftySignals 3 the API becomes more flexible and thread safe. Concurrency is now supported widely. SwiftySignals 3 implements now the observer pattern and builds upon Apple’s dispatching framework.
The basic concept of SwiftySignals are observables and observers. Observables are able to send messages and observers are able to receive and to process messages. In general you never need to define observables or observers by yourself. You only need to deal with helper classes. There are mainly two types of helper classes in SwiftySignals:
Message sources have one or more observables. Modifiers are both - observers and observables. Modifiers can be connected to an observable. They are able to filter, map, discard and generate messages. As modifiers are observables themselves, modifiers can be connected to a modifier chain. A message starts at a message source and is send along the modifier chains.
Class Signal<T>
is the simplest implementation of a message source. A signal can be used to send a message of type T
. To do so, the function Signal<T>.fire(with: T)
can be used. If a message is fired, then it is send to all modifiers connected to Signal<T>.fired
which is called the end point.
Connecting modifiers to signals is quite easy. Let´s say, we want to print the message when it is fired. For that purpose we could use the modifier then
to connect to the signal’s endpoint fired
:
let signal = Signal<Int>(value: 15)
let observables = ObservableCollection()
signal
.fired
.then { print($0) }
.append(to: observables)
signal.fire(with: 20)
Important to note is, that modifier chain needs to be stored somewhere. If this is not done, then the modifier chain is deleted immediately due to automatic reference counting. However, modifiers can be appended to an observable collection to keep them alive. In this case the modifier chain is destroyed along with the observable collection.
In SwiftySignals there a few message sources defined.
Signals are of class Signal<T>
. A signal has a function fire(with message: T)
that sends a message to all observables which connected to the signal’s endpoint fired
.
Properties are of class Property<T>
. A property has an attribute value
of type T
. Whenever the value is modified, the new value is send to the property’s endpoint didSet
. Properties are thread safe. That means reading from and writing to the property’s value is allowed from all threads. But be aware the reading is a blocking command, that might cause issues in some situations.
let property = Property<Int>(value: 0)
let observables = ObservableCollection()
property
.didSet
.then { print("Value changed to \($0)") }
.append(to: observables)
for i in 0..<10 {
property.value += 1
}
Timer Signals are of class TimerSignal
. Such timers can be seen as time triggered signals with message type Void
. The timer is configured by its init(repeats: Bool)
function and activated by fire
. The timer is triggering messages with the given time interval (continuously if repeats is true).
let timer = TimerSignal(repeats: true)
let observables = ObservableCollection()
timer
.fired
.then {.print("Timer has been triggered") }
.append(to: observables)
timer.fire(after: Measurement(value: 10, UnitDuration.seconds)
Different modifiers can be connected to an endpoint. Modifiers transforms observables into different observables. As modifiers are observables as well it is possible to chain different modifiers together. If a modifier is connected, it receives automatically the last message that was sent.
The function .then
connects a modifier that executes an action when a message of type T
is sent. There are two possibilities to define such an action:
.then(do: (T)->Void)
As instance with: .then(call: (Object)->((T)->Void)), on: Object)
After the message is processed by the action, it is send to all observables connected to the modifier.
class ViewController { private let property = Property(value: 56) private let obersables = ObservableCollection()
override func viewDidLoad() {
property
.didSet
.then(call: ViewController.update, on: self)
property.value = 67
}
func update(value: Int) {
print("Value = \(value)")
}
}
The function .filter(predicate: (T)->Bool)
connects a modifier that checks if the message fulfills a predicate. If the predicate returns true
, the message is sent to all observables connected to the modifier. If the predicate return false
instead, the message is discarded.
let property = Property(value: 45)
let observables = Observables()
property
.didSet
.filter { $0 >= 100 }
.then { print("Value \($0) is larger or equal than 100") }
.append(to: observables)
The function .map(transform: (T)->S)
connects a modifier that transforms an incoming message of type T
to S
. The transformed message is sent to all observables connected to the modifier.
let property = Property(value: 100)
property
.didSet
.map { 2 * $0 }
.then { print($0) }
.append(to: observables)
The function .throttle(pause: Measurement<UnitDuration>)
connects a modifier that discards messages if they are sent to fast. There needs to be a at least a defined pause between two message so that none of them are discarded.
The function .discard(first n: Int)
connects a modifier that discards the first n messages. All other message after that are sent to the connected modifiers.
The function .distinct()
connects a modifier that discards a message if the previous passed message is equal. This modifiers is only available for comparable message types.
The function .debounce(timeout: Measurement<UnitDuration>)
connects a modifier that forwards the message after a given time (timeout). If a new message arrives before the timeout is reached, then the count-down is reset. Be aware if you a deal with a fast and continuous message flow, it could happen that all messages are discarded by debounce.
Each message source and each modifier use its own dispatch queue for synchronization. Closures defined by the user, like closures used in then
, filter
and map
are executed on the main queue by default. However, this behavior can be changed by function .dispatch
which comes in two flavors:
.dispatch(to queue: DispatchQueue)
.dispatch(qos: DispatchQoS)
The first version delegates all closures to the given. The second version delegates all closures to a global dispatch queue with the given quality of service class.
If the closures should be executed in synchronization queue of the modifier, then you can use function .noDispatch()
.
let property = Property(value: 50)
let observables = Observables()
property
.didSet
.dispatch(qos: DispatchQoS.userInitiated)
.then { print("I am running on a global queue.") }
.dispatch(qos: DispatchQoS.main)
.then { print("I am running on the main queue.") }
.append(to: observables)