What is LiveData?
LiveData is an observable data holder class. Meaning, it respects the lifecycle of other app components, such as business logic, UI, or services.
This awareness ensures that LiveData only updates app component observers that are in an active lifecycle state.
LiveData notifies active registered objects about updates.
You can register an observer paired with an object that implements the LifecycleOwner interface.
This relationship allows the observer to be removed when the state of the corresponding Lifecycle object changes to DESTROYED.
This is especially useful for UI components (UIView, UIViewController) because they can safely observe LiveData objects without worrying about memory leaks — activities and UI components are instantly unsubscribed when they are Deinit.
How to use the LiveData step by step?
Step 1:
A class with generic data holder
class LiveData<T> { private var value: T? { didSet { notifyObservers() } } }
Step 2:
Each time the data is set, we need to notify all registered observers.
class LiveData<T> { private var value: T? { didSet { notifyObservers() } } private var observers: [(AnyObject, ((T) -> Void)?)] = [] func publish(value: T) { self.value = value } private func notifyObservers() { // MARK: - TODO } private func notify(observer: AnyObject) { // MARK: - TODO } }
Step 3:
Now our LiveData Class needs to have observers and methods to add/remove.
class LiveData<T> { private var value: T? { didSet { notifyObservers() } } private var observers: [(AnyObject, ((T) ->; Void)?)] = [] func publish(value: T) { self.value = value } func add(observer: AnyObject, completion: @escaping (T) -> Void) { // MARK: - TODO } func remove(observer: AnyObject) { // MARK: - TODO } private func notifyObservers() { // MARK: - TODO } private func notify(observer: AnyObject) { // MARK: - TODO } }
Step 4:
In this step, we can take a break and talk about memory leaks and life cycle.
As you know it’s really important to handle memory and avoid retain cycle.
In our case, if we deep dive into the add(observer: completion:) we can’t directly store the observer into our array of observers because this one is going to be retained as a STRONG reference to the array (which is a struct).
func add(observer: AnyObject, completion: @escaping (T) -> Void)
Now the question is: How to solve this problem?
- We can create a WeakBox or WeakWrapper for each observer.
- Each Wrapper will contain a weak reference of observer and the completion binding.
- What does it look like?
private class WeakWrapper { var completion: ((T) -> Void)? weak var observer: AnyObject? }
Step 5:
The full code with concrete Implementation.
class LiveData<T> { private class WeakWrapper { var completion: ((T) -> Void)? weak var observer: AnyObject? } private var value: T? { didSet { notifyObservers() } } private var observers: [WeakWrapper] = [] func add(observer: AnyObject, completion: ((T) -> Void)?) { let weakWrapper = WeakWrapper() weakWrapper.completion = completion weakWrapper.observer = observer observers.append(weakWrapper) notify(observer: weakWrapper) } func remove(observer: AnyObject) { observers.removeFirst(where: { $0.observer == observer }) } func publish(value: T) { self.value = value } private func notifyObservers() { observers.forEach { notify(observer: $0) } } private func notify(observer: WeakWrapper) { guard let value = value else { return } observer.completion?(value) } } extension Array { mutating func removeFirst(where: ((Element) -> Bool)) { for i in 0..<count { guard `where`(self[i]) else { continue } self.remove(at: i) break } } }
Step 6:
The final deep dive, what’s wrong?
We solved a problem with the wrapper, but It’s not enough.
Why? because you need to manually remove the wrapper otherwise the object will be destroyed without the wrapper.
Because nobody is perfect and it is really easy to forget to remove this kind of object, like NSNotificationCenterObserver ¯\_(⊙︿⊙)_/¯
To never forget to destroy all objects we can use the same logic as RxSwift! It consists of creating a DisposeBag instance and using the Deinit of this one to handle the object destruction.
It’s just because the DisposeBag instance shares the same life cycle then the observer. On RxSwift if you forget to define this instance, the compiler will generate a warning.
DisposeBag code:
class DisposeBag { // <----- New var willBeDeInit: [(() ->; Void)?] = [] deinit { willBeDeInit.forEach { $0?() } willBeDeInit = [] } } class LiveData<T> { func add(observer: AnyObject, disposeBag: DisposeBag, completion: ((T) -> Void)?) { let weakWrapper = WeakWrapper() weakWrapper.disposeBag = disposeBag // <----- New weakWrapper.completion = completion weakWrapper.observer = observer observers.append(weakWrapper) disposeBag.willBeDeInit.append { [weak self] in // <----- New self?.remove(observer: observer) // <----- New } // <----- New notify(observer: weakWrapper) } }
Step 7: How to use our beautiful LiveData class?
Playground use cases:
The sample code below represents a view which will be updated each time the value of Bool changes.
// In your playground class View: UIView { let dispodeBag = DisposeBag() var liveData: LiveData&lt;Bool&gt;? func configure1() { liveData?.add(observer: self, disposeBag: dispodeBag) { [weak self] wvalue in guard let self = self else { return } print(">>>>>>>>; view:", self.tag, "Observer 1") } } func configure2() { liveData?.add(observer: self, disposeBag: dispodeBag) { [weak self] value in guard let self = self else { return } print(">>>>>>>> view:", self.tag, "Observer 2") } } func configure3() { liveData?.add(observer: self, disposeBag: dispodeBag) { [weak self] value in guard let self = self else { return } print(">>>>>>>> view:", self.tag, "Observer 3") } } } let liveData = LiveData<Bool>() let view1 = View() view1.tag = 1 view1.liveData = liveData view1.configure1() view1.configure2() let view2 = View() view2.tag = 2 view2.liveData = liveData view2.configure3() liveData.publish(value: true) print(">>>>>>>> Async Request. Now:", Date()) DispatchQueue.main.asyncAfter(deadline: .now() + 4) { liveData.publish(value: false) print(">>>>>>>> Done. Now:", Date()) } print("<<<<<<<<<< Remove all observers from View 2") liveData.remove(observer: view2)
Printed result:
[-------Start-------] >>>>>>>> view: 1 Observer 1 >>>>>>>> view: 1 Observer 2 >>>>>>>> view: 2 Observer 3 >>>>>>>> Async Simulation. Now: 2020-01-21 13:56:19 +0000 >>>>>>>> Remove all observers from View 2 >>>>>>>> view: 1 Observer 1 >>>>>>>> view: 1 Observer 2 <<<<<<<< Done. Now: 2020-01-21 13:56:24 +0000 [-------End-------]
Conclusion
And that’s it :). We have learned how to start building a reactive object. The top advantages of using this kind of object are simplicity and reusability.
Each view can be updated with the same income data. And you do not have to import a heavy SDK to get ‘binding mechanisms‘
But I encourage you to take a look at Apple Combine, it’s really easy and nice to use and you will get free updates by Apple