Skip to content

Instantly share code, notes, and snippets.

@JasonCanCode
Last active December 22, 2021 18:03
Show Gist options
  • Save JasonCanCode/4d43e3d4fee96c712fed1bbe32d5e94f to your computer and use it in GitHub Desktop.
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.
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)
}
}
}
/// 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