SwiftEvents
SwiftEvents is a lightweight library for creating and observing events.
It includes:
Observable<T>
for data binding that can be used in MVVM. Observable is implemented using Event class.Event<T>
for closure based delegation and one-to-many notifications.
Features:
-
Type safety: the concrete type value is delivered to the subscriber without the need for downcasting
-
Thread safety: you can
subscribe
/bind
,trigger
,unsubscribe
/unbind
from any thread without issues such as data races -
Memory safety: automatic preventing retain cycles, without strictly having to specify
[weak self]
in closure when subscribing/binding. Whether you specified[weak self]
or not, it’s sometimes forgotten to specify - safety against memory leaks will be ensured automatically. As well as automatic removal of subscribers/observers when they are deallocated -
Comprehensive unit test coverage.
Installation
CocoaPods
To install SwiftEvents using CocoaPods, add this line to your Podfile
:
pod 'SwiftEvents', '~> 1.1.2'
Carthage
To install SwiftEvents using Carthage, add this line to your Cartfile
:
github "denissimon/SwiftEvents"
Swift Package Manager
To install SwiftEvents using the Swift Package Manager, add it to your Package.swift
file:
dependencies: [
.Package(url: "https://github.com/denissimon/SwiftEvents.git", from: "1.1.2")
]
Manually
Just drag SwiftEvents.swift
to the project tree.
Usage
Data binding
- Replace the
Type
of property to observe with theObservable<Type>
- Bind to the Observable
Example:
class ViewModel {
var infoLabel: Observable<String> = Observable("init value")
func set(newValue: String) {
infoLabel.value = newValue
}
}
class View: UIViewController {
var viewModel = ViewModel()
@IBOutlet weak var infoLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
infoLabel.text = viewModel.infoLabel.value
viewModel.infoLabel.bind(self) { (self, value) in self.updateInfoLabel(value) }
}
private func updateInfoLabel(_ value: String) {
infoLabel.text = value
}
}
Note: here a capture list is intentionally not specified in closure before (self, value)
because under the hood SwiftEvents will construct a new closure with [weak target]
included there. This way a strong reference cycle will be avoided.
In the above example, every time ViewModel changes the value of the observable property infoLabel
, View is notified and updates the infoLabel.text
.
You can use the infix operator <<< to set a new value for an observable property:
infoLabel <<< newValue
As with Event, an Observable can have multiple observers.
Delegation
Delegation can be implemented not only using protocols, but also based on closure. Such a one-to-one connection can be done in two steps:
- Create an Event for the publisher
- Subscribe to the Event
Example:
class MyModel {
let didDownload = Event<UIImage?>()
func downloadImage(for url: URL) {
service.download(url: url) { [weak self] image in
self?.didDownload.trigger(image)
}
}
}
class MyViewController: UIViewController {
let model = MyModel()
var image: UIImage?
override func viewDidLoad() {
super.viewDidLoad()
model.didDownload.subscribe(self) { (self, image) in self.updateImage(image) }
}
private func updateImage(_ image: UIImage?) {
self.image = image
}
}
You can use Event with any complex type, including custom types and multiple values like (UIImage, Int)
. You can also create several events (didDownload, onNetworkError etc), and trigger only what is needed.
Notifications
If notifications must be one-to-many, or two objects that need to be connected are too far apart, SwiftEvents can be used like NotificationCenter.
Example:
public class EventService {
public static let get = EventService()
private init() {}
public let onDataUpdate = Event<String?>()
}
class Controller1 {
init() {
EventService.get.onDataUpdate.subscribe(self) { (self, data) in
print("Controller1: '\(data)'")
}
}
}
class Controller2 {
init() {
EventService.get.onDataUpdate.subscribe(self) { (self, data) in
print("Controller2: '\(data)'")
}
}
}
class DataModel {
func requestData() {
// requesting code goes here
data = "some data"
EventService.get.onDataUpdate.trigger(data)
}
}
let sub1 = Controller1()
let sub2 = Controller2()
let pub = DataModel()
pub.requestData()
// => Controller1: 'some data'
// => Controller2: 'some data'
More examples
More usage examples can be found in this demo app.
Advanced features
Manual removal of a subscriber / observer
someEvent.subscribe(self) { (self, value) in self.setValue(value) }
someEvent.unsubscribe(self)
someObservable.bind(self) { (self, value) in self.setValue(value) }
someObservable.unbind(self)
Removal of all subscribers / observers
someEvent.unsubscribeAll()
someObservable.unbindAll()
The number of subscribers to the Event
let subscribersCount = someEvent.subscribersCount
The number of times the Event was triggered
let triggersCount = someEvent.triggersCount
Reset of triggersCount
someEvent.resetTriggersCount()
queue: DispatchQueue
By default, the provided handler is executed on the thread that triggers the Event / Observable. To change this default behaviour, you can set this parameter when subscribing/binding:
// This executes the handler on the main queue
someEvent.subscribe(self, queue: .main) { (self, image) in self.updateImage(image) }
someObservable.bind(self, queue: .main) { (self, image) in self.updateImage(image) }
One-time notification
To ensure that the handler will be executed only once:
someObservable.bind(self) { (self, data) in
self.useData(data)
self.someObservable.unbind(self)
}
N-time notifications
To ensure that the handler will be executed no more than n
times:
someEvent.subscribe(self) { (self, data) in
self.useData(data)
if self.someEvent.triggersCount == n {
self.someEvent.unsubscribe(self)
}
}
Alias methods
There are alias methods for Event addSubscriber
, removeSubscriber
and removeAllSubscribers
, which do the same thing as subscribe
, unsubscribe
and unsubscribeAll
respectively.
License
Licensed under the MIT license