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/unbindfrom 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
Typeof 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 <<< newValueAs 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.subscribersCountThe number of times the Event was triggered
let triggersCount = someEvent.triggersCountReset 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