Search code examples
iosswiftrx-swiftreactivex

RxSwift Variable composition without subscribe/bind/drive


I need some internal state in a viewModel but also trying to follow the "no subscription / bind / drive / ..." ideal approach and only compose between Observables.

How can I specify what a Variable observes?

Example:

private var userProfilesToFetch = Variable<[String]>([])
private var users: Variable<[User]> {

    return //something that observes fetchUserProfiles() and when it emits, appends to its .value
}

private func fetchUserProfiles() -> Observable<User?> {

    let reference = databaseRef.child("users")

    return userProfilesToFetch.asObservable()
        .filter({ $0 != [] })
        .map({ $0.last! })
        .flatMap({ (userId) -> Observable<User?> in

            return self.service.observeAllChildren(of: reference.child(userId), by: .value)
                .map({ (snapshot) -> User? in

                        guard let values = snapshot.value as? [String: AnyObject] else { return nil }

                        var user = User(dictionary: values)

                        user.id = snapshot.key

                        return user
                })
        })
} 

Solution

  • The "ideal approach" is to avoid the use of Subjects/Variables. Instead, favor sequence emitters (functions that return an observable,) sequence receivers (functions that accept an observable as a parameter,) and sequence transformers (functions that do both.)

    Sequence emitters and receivers necessarily perform side effects and inside a sequence receiver, there is necessarily a subscribe/bind in order to unpack the value and use it.

    There should be a direct and obvious link between the emitters and receivers. Subjects/Variables tend to break that link.

    In this ideal approach, your "view model" is not a class/struct containing a bunch of variables. Your view model is a function that takes observables as parameters and returns observables for the view controller to bind to. For example:

    class MyViewController: UIViewController {
        @IBOutlet weak var name: UITextView!
        @IBOutlet weak var label: UILabel!
    
        override
        func viewDidLoad() {
            super.viewDidLoad()
            let viewModel = myViewModel(name: name.rx.text.orEmpty)
            viewModel.label.bind(to: label.rx.text).disposed(by: bag)
        }
        let bag = DisposeBag()
    }
    
    struct MyViewModel { 
        let label: Observable<String>
    }
    
    // this function could be turned into an `init` method on the MyViewModel struct if you would prefer.
    fun myViewModel(name: Observable<String>) -> MyViewModel {
        let label = name.map { "Hello \($0)!" }
        return MyViewModel(label: label)
    }