Last active
December 22, 2021 18:03
-
-
Save JasonCanCode/4d43e3d4fee96c712fed1bbe32d5e94f to your computer and use it in GitHub Desktop.
Give a property the power of a BehaviorRelay while preserving the object itself. The object can remain a constant property of a View Model and still be updated through Rx bindings.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import RxCocoa | |
import RxSwift | |
/// To persist updates through either the `wrappedValue` or `relay` while keeping the implementing property a value type. | |
private var cache = NSCache<AnyObject, AnyObject>() | |
@propertyWrapper | |
struct Bindable<Element> { | |
var projectedValue: Bindable { self } | |
var wrappedValue: Element { | |
get { | |
cache.object(forKey: id) as? Element | |
?? historicalValue | |
} | |
set { | |
cache.removeObject(forKey: id) | |
historicalValue = newValue | |
relay.accept(newValue) | |
} | |
} | |
/// Used to cache the wrappedValue when it is updated by the relay | |
private let id: AnyObject | |
/// Used to store the wrappedValue when it is directly updated | |
private var historicalValue: Element | |
/// The secret sauce to make the property bindable | |
private let relay: BehaviorRelay<Element> | |
init(wrappedValue: Element) { | |
self.id = UUID() as AnyObject | |
self.historicalValue = wrappedValue | |
self.relay = .init(value: wrappedValue) | |
} | |
} | |
extension Bindable: ObservableType, ObserverType { | |
func subscribe<Observer>(_ observer: Observer) -> Disposable where | |
Observer: ObserverType, Element == Observer.Element { | |
relay.asObservable().subscribe(observer) | |
} | |
func on(_ event: Event<Element>) { | |
if case .next(let newValue) = event { | |
cache.setObject(newValue as AnyObject, forKey: id) | |
relay.accept(newValue) | |
} | |
} | |
} | |
extension Bindable { | |
func twoWayBind(to property: ControlProperty<Element>) -> Disposable { | |
return Disposables.create( | |
relay.bind(to: property), | |
property.bind(to: relay) | |
) | |
} | |
func twoWayBind(to property: ControlProperty<Element>, disposeBag: DisposeBag) { | |
disposeBag.insert { | |
relay.bind(to: property) | |
property.bind(to: relay) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// A super simple domain object | |
struct User { | |
@Bindable var name: String | |
} | |
/// Allows access to its domain model | |
struct DirectViewModel { | |
let user: User | |
} | |
/// Restricts direct access to its domain model, exposing the Bindable property through a computed property | |
struct IndirectViewModel { | |
var name: Bindable<String> { user.$name } | |
private let user: User | |
} | |
import RxSwift | |
import UIKit | |
class EditUserViewController: UIViewController { | |
// ... | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
// DirectViewModel | |
viewModel.user.$name | |
.map({ !$0.isEmpty }) | |
.bind(to: errorLabel.rx.isHidden) | |
.disposed(by: disposeBag) | |
// IndirectViewModel | |
viewModel.name | |
.map({ !$0.isEmpty }) | |
.bind(to: errorLabel.rx.isHidden) | |
.disposed(by: disposeBag) | |
// Two-way binding between entered text and computed property on the IndirectViewModel | |
disposeBag.insert | |
viewModel.name.bind(to: textField.rx.text.orEmpty) | |
textField.rx.text.orEmpty.bind(to: viewModel.name) | |
// OR | |
viewModel.name.twoWayBind(to: textField.rx.text.orEmpty) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment