Search code examples
reactive-cocoareactive-cocoa-4

ReactiveCocoa 4 - Creating a dependent login request that runs only once


I've mostly been using ReactiveCocoa in the past to simply bind views and view models, and now I'm trying to take the plunge and use it throughout a new project but I'm having trouble getting my head around a couple of things.

What I want to do is this -

  • Have a networked login request that every other network request needs to call first.
  • If multiple requests are made, they all need to wait until log in completes (eg, let's say I've got a Tab Bar Controller and I quickly tap between them before log in finishes; I don't want multiple login requests fired.)

I've spent a bit of time exploring a few options like queues, looking into things like flatMap(.Latest) but if I'm completely honest - I have no idea what I'm doing! :S

Below's a very basic, dumb implementation which is quickly hacked together and very likely badly implemented. If someone could give me a few pointers into what I need to change it'd be enormously appreciated. My doSomething method obviously logs in first, but if multiple calls are made at once they don't wait until the first one finishes as I need them to.

Can I do something with the loginValid property?

(Also, pointers on generally how I should be structuring this stuff would be great - I'm sure I'm doing many stupid things with this code)

Thanks!

class FakeBackend: BackendType {

    private var loginResponse = MutableProperty<LoginResponse?>(nil)
    private let loginValid = MutableProperty<Bool>(false)

    private var loginProducer: SignalProducer<LoginResponse, NSError>! // <-- implicitly unwrapped optional? Yuck

    init() {
        loginValid <~ loginResponse.producer.map { $0 != nil }

        loginProducer = SignalProducer { [weak self] observer, disposable in
            guard let _self = self else { return }

            if let loginResponse = _self.loginResponse.value {
                print("Already have login details")
                observer.sendNext(loginResponse)
                observer.sendCompleted()
            } else {
                print("Don't have login details, go get them")
                _self.logIn().start(observer)
            }
        }
    }

    func doSomething() -> SignalProducer<HomeResponse, NSError> {
        return loginProducer
            .then(SignalProducer<HomeResponse, NSError> { observer, dispoable in

                let homeResponse = HomeResponse(title: "My title is this")

                observer.sendNext(homeResponse)
                observer.sendCompleted()
            })
    }

    private func logIn() -> SignalProducer<LoginResponse, NSError> {
        return SignalProducer { observer, disposable in

            print("Calling network login")
            delayToMainThread(1.0, closure: { [weak self] () -> () in

                guard let _self = self else { return }

                let loginResponse = LoginResponse(accessToken: "MyAccessToken")

                _self.loginResponse.value = loginResponse

                observer.sendNext(loginResponse)
                observer.sendCompleted()
            })
        }
    }
}

Solution

  • The correct way to do this is via replayLazily, which is described at https://spin.atomicobject.com/2014/06/29/replay-replaylast-replaylazily/

    The -replayLazily convenience method returns a new signal that, when subscribed to, will immediately send the subscriber the entire history of values that have come through the source signal, without re-executing the source signal’s subscription code.

    I received a response to my question on the ReactiveCocoa Github Issues page. https://github.com/ReactiveCocoa/ReactiveCocoa/issues/2706

    Essentially, you want to do something like this -

    class FakeBackend: BackendType {
    
        private var login: SignalProducer<LoginResponse, NSError>
    
        init() {
            login = SignalProducer { observer, disposable in
                print("Logging in...")
    
                // we'd actually make a network call here, but for demo purposes
                // let's just return some dummy data.
                let loginResponse = LoginResponse(accessToken: "MyAccessToken")
    
                print("Logged in!")
    
                observer.sendNext(loginResponse)
                observer.sendCompleted()
            }.replayLazily(1)
        }
    
        func loadHomeScreen() -> SignalProducer<HomeResponse, NSError> {
            return login
                .flatMap(.Latest, transform: homeResponse)
        }
    
        private func homeResponse(loginResponse: LoginResponse) -> SignalProducer<HomeResponse, NSError> {
            return SignalProducer<HomeResponse, NSError> { observer, disposable in
                print("Aaaand, we've gotten our HomeResponse.")
    
                let homeResponse = HomeResponse(title: "My title is this")
                observer.sendNext(homeResponse)
                observer.sendCompleted()
            }
        }
    }