Preface
I developed it entirely in 2017 using Swift. Using Swift for development is a very pleasant experience, and I no longer want to touch OC anymore. I recently wanted to make a responsive programming library, so I'll share it.
Learning responsive programming becomes a pain in the absence of good resources. When I started learning, I was looking for various tutorials. As a result, it was found that only a very small part of what was useful, and these small parts were just superficial things, which did not play much role in understanding the entire architecture.
Reactive Programing
Speaking of responsive programming, ReactiveCocoa and RxSwift can be said to be the best third-party open source libraries in iOS development. Today we won’t talk about ReactiveCocoa and RxSwif, let’s write a responsive programming library ourselves. If you are familiar with observer patterns, responsive programming is easy to understand.
Responsive programming is a programming paradigm for data flow and change propagation.
For example, user input, click events, variable values, etc. can all be regarded as a stream. You can observe this stream and do some operations based on this stream. The behavior of "listening" streams is called subscription. Responsiveness is based on this idea.
Without further ado, roll up your sleeves and start to do it.
Let’s take a network request to obtain user information as an example:
func fetchUser(with id: Int, completion: @escaping ((User) -> Void)) { (deadline: ()+2) { let user = User(name: "jewelz") completion(user) } }
The above is our usual approach, passing in a callback function into the request method and getting the result in the callback. In the responsive form, we listen to the request, and when the request is completed, the observer is updated.
func fetchUser(with id: Int) -> Signal {}
Send a network request like this:
fetchUser(with: "12345").subscribe({ })
Before completing Signal, you need to define the data structure returned after subscription. Here I only care about data in two states: success and failure, so you can write it like this:
enum Result { case success(Value) case error(Error) }
Now we can start implementing our Signal:
final class Signal { fileprivate typealias Subscriber = (Result) -> Void fileprivate var subscribers: [Subscriber] = [] func send(_ result: Result) { for subscriber in subscribers { subscriber(result) } } func subscribe(_ subscriber: @escaping (Result) -> Void) { (subscriber) } }
Write a small example to test:
let signal = Signal() { result in print(result) } (.success(100)) (.success(200)) // Print success(100) success(200)
Our Signal is working properly, but there is still a lot of room for improvement. We can use a factory method to create a Signal and turn send into private:
static func empty() -> ((Result) -> Void, Signal) { let signal = Signal() return (, signal) } fileprivate func send(_ result: Result) { ... }
Now we need to use Signal like this:
let (sink, signal) = () { result in print(result) } sink(.success(100)) sink(.success(200))
Next we can bind a Signal to UITextField, and just add a computed property to UITextField in Extension:
extension UITextField { var signal: Signal { let (sink, signal) = () let observer = KeyValueObserver(object: self, keyPath: #keyPath(text)) { str in sink(.success(str)) } (observer) return signal } }
The observer in the above code is a local variable, which will be destroyed after the signal is called, so the object needs to be saved in Signal. You can add an array to Signal to save objects that need to extend their life cycle. KeyValueObserver is a simple encapsulation of KVO, which is implemented as follows:
final class KeyValueObserver: NSObject { private let object: NSObject private let keyPath: String private let callback: (T) -> Void init(object: NSObject, keyPath: String, callback: @escaping (T) -> Void) { = object = keyPath = callback () (self, forKeyPath: keyPath, options: [.new], context: nil) } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard let keyPath = keyPath, keyPath == , let value = change?[.newKey] as? T else { return } callback(value) } deinit { (self, forKeyPath: keyPath) } }
It's ready to use now({})
Let's observe the changes in the UITextField content.
Write a VC on Playground to test it:
class VC { let textField = UITextField() var signal: Signal? func viewDidLoad() { signal = signal?.subscribe({ result in print(result) }) = "1234567" } deinit { print("Removing vc") } } var vc: VC? = VC() vc?.viewDidLoad() vc = nil // Print success("1234567") Removing vc
Reference Cycles
I added the deinit method in the Signal above:
deinit { print("Removing Signal") }
Finally, I found that the destruction method of Signal was not executed, which means that there are circular references in the above code. In fact, if you carefully analyze the implementation of signal in the expansion of UITextField above, you can find out what the problem is.
let observer = KeyValueObserver(object: self, keyPath: #keyPath(text)) { str in sink(.success(str)) }
In the KeyValueObserver callback, thesink()
Method, and sink method is actually(_:)
Method, here the signal variable is captured in the closure, and a circular reference is formed. This can be solved by using weak. The modified code looks like this:
static func empty() -> ((Result) -> Void, Signal) { let signal = Signal() return ({[weak signal] value in signal?.send(value)}, signal) }
Run it again and the destructor method of Signal can be executed.
The above implements a simple responsive programming library. However, there are still many problems here, such as we should remove the observer at the appropriate time. Now our observer is added to the subscribers array, so we don't know which observer to remove, so we replace the number with a dictionary and use UUID as key:
fileprivate typealias Token = UUID fileprivate var subscribers: [Token: Subscriber] = [:]
We can imitate the Disposable in RxSwift to remove observers, and the implementation code is as follows:
final class Disposable { private let dispose: () -> Void static func create(_ dispose: @escaping () -> Void) -> Disposable { return Disposable(dispose) } init(_ dispose: @escaping () -> Void) { = dispose } deinit { dispose() } }
The original subscribe(_:) just returns a Disposable:
func subscribe(_ subscriber: @escaping (Result) -> Void) -> Disposable { let token = UUID() subscribers[token] = subscriber return { [token] = nil } }
In this way, we can remove the observer by destroying the Disposable at the right time.
As a responsive programming library, we will have map, flatMap, filter, reduce and other methods, so our libraries must not be missing, we can simply implement several.
map
map is relatively simple, which is the process of using a function that returns the value as a wrapper value to apply a wrapper value to a wrapper value. The wrapper value here can be understood as a structure that can contain other values, such as an array in Swift, and optional types are wrapper values. They all have overloaded map, flatMap and other functions. Taking arrays as an example, we often use this:
let images = ["1", "2", "3"].map{ UIImage(named: $0) }
Now implement our map function:
func map(_ transform: @escaping (Value) -> T) -> Signal { let (sink, signal) = () let dispose = subscribe { (result) in sink((transform)) } (dispose) return signal }
I also implemented the map function for Result:
extension Result { func map(_ transform: @escaping (Value) -> T) -> Result { switch self { case .success(let value): return .success(transform(value)) case .error(let error): return .error(error) } } } // Test let (sink, intSignal) = () intSignal .map{ String($0)} .subscribe { result in print(result) } sink(.success(100)) // Print success("100")
flatMap
flatMap and map are very similar, but there are some differences. Taking optional models as an example, Swif t defines map and flatMap like this:
public func map(_ transform: (Wrapped) throws -> U) rethrows -> U? public func flatMap(_ transform: (Wrapped) throws -> U?) rethrows -> U?
The difference between flatMap and map is mainly reflected in the different return values of the transform function. The function return value type accepted by map is of type U, while the function return value type accepted by flatMap is of type U?. For example, for an optional value, you can call it like this:
let aString: String? = "¥99.9" let price = { Float($0)} // Price is nil
Here we keep the flatMap consistent with the array in Swift and the flatMap in optional.
So our flatMap should be defined like this: flatMap(_ transform: @escaping (Value) -> Signal) -> Signal.
After understanding the difference between flatMap and map, it is very simple to implement:
func flatMap(_ transform: @escaping (Value) -> Signal) -> Signal { let (sink, signal) = () var _dispose: Disposable? let dispose = subscribe { (result) in switch result { case .success(let value): let new = transform(value) _dispose = ({ _result in sink(_result) }) case .error(let error): sink(.error(error)) } } if _dispose != nil { (_dispose!) } (dispose) return signal }
Now we can simulate a network request to test flatMap:
func users() -> Signal { let (sink, signal) = () (deadline: ()+2) { let users = Array(1...10).map{ User(id: String(describing: $0)) } sink(.success(users)) } return signal } func userDetail(with id: String) -> Signal { let (sink, signal) = () (deadline: ()+2) { sink(.success(User(id: id, name: "jewelz"))) } return signal } let dispose = users() .flatMap { return (with: $!.id) } .subscribe { result in print(result) } (dispose) // Print: success((name: Optional("jewelz"), id: "1"))
By using flatMap, we can easily convert one Signal to another Signal, which is convenient when we handle multiple requests nested.
Written at the end
The above implements a simple responsive programming library through more than 100 lines of code. However, for a library, the above content is far from enough. Signal is not atomic yet, and should be thread-installed as a practical library. Also, our handling of Disposable is not elegant enough, and we can imitate the DisposeBag in RxSwift. The above questions can be left to readers for their own thinking.
Okay, the above is the entire content of this article. I hope that the content of this article has a certain reference value for everyone's study or work. If you have any questions, you can leave a message to communicate. Thank you for your support.