Value Observing and Events for Swift
Swift lacks the powerful Key Value Observing (KVO) from Objective-C. But thanks to closures, generics and property observers, in some cases it allows for far more elegant observing. You have to be explicit about what can be observed, though.
Overview
Observable-Swift is a Swift library for value observing (via explicit usage of Observable<T>
) and subscribable events (also explicit, using Event<T>
). While it is not exactly "KVO for Swift" (it is explicit, there are no "Keys", ...) it is a catchy name so you can call it that if you want. The library is still under development, just as Swift is. Any contributions, both in terms of suggestions/ideas or actual code are welcome.
Observable-Swift is brought to you by Leszek Ślażyński (slazyk), you can follow me on twitter and github. Also check out SINQ my other Swift library that makes working with collections a breeze.
Observables
Using Observable<T>
and related classes you can implement wide range of patterns using value observing. Some of the features:
- observable variables and properties
- chaining of observables (a.k.a. key path observing)
- short readable syntax using
+=
,-=
,<-
/^=
,^
- alternative syntax for those who dislike custom operators
- handlers for before or after the change
- handlers for
{ oldValue:, newValue: }
(oldValue, newValue)
or(newValue)
- adding multiple handlers per observable
- removing / invalidating handlers
- handlers tied to observer lifetime
- observable mutations of value types (structs, tuples, ...)
conversions from observables to underlying type(not available since Swift Beta 6)- observables combining other observables
- observables as value types or reference types
- ...
Events
Sometimes, you don’t want to observe for value change, but other significant events. Under the hood Observable<T>
uses beforeChange
and afterChange
of EventReference<ValueChange<T>>
. You can, however, use Event<T>
or EventReference<T>
directly and implement other events too.
Installation
You can use either CocoaPods or Carthage to install Observable-Swift.
Otherwise, the easiest option to use Observable-Swift in your project is to clone this repo and add Observable-Swift.xcodeproj to your project/workspace and then add Observable.framework to frameworks for your target.
After that you just import Observable
.
Examples
Observable<T>
is a simple struct
allowing you to have observable variables.
// create a Observable<Int> variable
var x = Observable(0)
// add a handler
x.afterChange += { println("Changed x from \($0) to \($1)") }
// without operators: x.afterChange.add { ... }
// change the value, prints "Changed x from 0 to 42"
x <- 42
// alternativelyL x ^= 42, without operators: x.value = 42
You can, of course, have observable properties in a class
or a struct
:
struct Person {
let first: String
var last: Observable<String>
init(first: String, last: String) {
self.first = first
self.last = Observable(last)
}
}
var ramsay = Person(first: "Ramsay", last: "Snow")
ramsay.last.afterChange += { println("Ramsay \($0) is now Ramsay \($1)") }
ramsay.last <- "Bolton"
Up to Swift Beta 5 you could implicitly convert Observable<T>
to T
, and use it in places where T
is expected. Unfortunately Beta 6 forbids defining implicit conversions:
let x = Observable(20)
// You can use the value property ...
let y1 = x.value + 22
// ... or a postfix operator ...
let y2 = x^ + 22
/// ... which has the advantage of easy chaining
let y3 = obj.property^.whatever^.sthElse^
/// ... you can also use ^= instead of <- for consistency with the postfix ^
For value types (such as structs
or tuples
) you can also observe their mutations:
Since Observable
is a struct
, ramsay in example above gets mutated too. This means, you could observe ramsay as well.
struct Person {
let first: String
var last: String
var full: String { get { return "\(first) \(last)" } }
}
var ramsay = Observable(Person(first: "Ramsay", last: "Snow"))
// x += { ... } is the same as x.afterChange += { ... }
ramsay += { println("\($0.full) is now \($1.full)") }
ramsay.value.last = "Bolton"
You can remove observers by keeping the subscription object:
var x = Observable(0)
let subscr = x.afterChange += { (_,_) in println("changed") }
// ...
x.afterChange -= subscr
// without operators: x.afterChange.remove(subscr)
Invalidating it:
var x = Observable(0)
let subscr = x.afterChange += { (_,_) in println("changed") }
// ...
subscr.invalidate() // will be removed next time event fires
Or tie the subscription to object lifetime:
var x = Observable(0)
for _ in 0..1 {
let o = NSObject() // in real-world this would probably be self
x.afterChange.add(owner: o) { (oV, nV) in println("\(oV) -> \(nV)") }
x <- 42 // handler called
} // o deallocated, handler invalidated
x <- -1 // handler not called
You can also chain observables (observe "key paths"):
class Person {
let firstName: String
var lastName: Observable<String>
var friend: Observable<Person?> = Observable(nil)
// init(...) { ... }
}
let me = Person()
var myFriendsName : String? = nil
// we want to observe my current friend last name
// and get notified with name when the friend or the name changes
chain(me.friend).to{$0?.lastName}.afterChange += { (_, newName) in
myFriendsName = newName
}
// alternatively, we can do the same with '/' operator
(me.friend / {$0?.lastName}).afterChange += { (_, newName) in
myFriendsName = newName
}
Event<T>
is a simple struct
allowing you to define subscribable events. Observable<T>
uses EventReference<ValueChange<T>>
for afterChange
and beforeChange
.
class SomeClass {
// defining an event someone might be interested in
var somethingChanged = Event<String>()
// ...
func doSomething() {
// ...
// fire the event and notify all observers
somethingChanged.notify("Hello!")
// ...
}
}
var obj = SomeClass()
// subscribe to an event
obj.somethingChanged += { println($0) }
obj.doSomething()
More examples can be found in tests in ObservableTests.swift
Advanced
If you require observables as reference types, you can use either ObservableProxy
which is a reference type in between your code and the real Observable
value type. You can also use ObservableReference
which is a ObservableProxy
to an Observable
that it holds on a property.
Same is true for Event
, there is EventReference
as well. Actually, Observable
uses EventReference
instead of Event
, otherwise some use cases would be difficult to implement. This means, that if you want to unshare events and subscriptions you need to call observable.unshare(removeSubscriptions:)
.