20

I learn the sample code in RxSwift. In the file GithubSignupViewModel1.swift, the definition of validatedUsername is:

validatedUsername = input.username //the username is a textfiled.rx_text
    .flatMapLatest { username -> Observable<ValidationResult> in
        print("-------->1:")
        return validationService.validateUsername(username)
            .observeOn(MainScheduler.instance)
            .catchErrorJustReturn(.Failed(message: "Error contacting server"))
    }
    .shareReplay(1)

the validateUsername method is finally called the following method:

func usernameAvailable(username: String) -> Observable<Bool> {
    // this is ofc just mock, but good enough
    print("-------->2:")
    let URL = NSURL(string: "https://github.com/\(username.URLEscaped)")!
    let request = NSURLRequest(URL: URL)
    return self.URLSession.rx_response(request)
        .map { (maybeData, response) in
            print("-------->3:")
            return response.statusCode == 404
        }
        .catchErrorJustReturn(false)
}

Here is my confusion:

whenever I input a character quickly in the username textfield, message -------->1:, -------->2: showed, and a little later message -------->3: showed, but only showed one -------->3: message.

When I input characters slower, message -------->1:, -------->2:, -------->3: showed successively.

But when I change the flatMapLatest to flatMap, how many characters I input, I will get the same number of -------->3: message.

So how did the flatMapLatest work here?

How the flatMapLatest filter the early response from NSURLResponse ?


I read some about the flatMapLatest, but none of them will explain my confusion.

What I saw is something like:

let a = Variable(XX)
a.asObservable().flatMapLatest(...)

When changed a.value to another Variable, the Variable(XX) will not influence the subscriber of a.

But the input.username isn't changed, it is always a testfield.rx_text! So how the flatMapLatest work?

leizh00701
  • 1,893
  • 5
  • 21
  • 32

5 Answers5

22

TheDroidsOnDroid's answer is clear for me:

FlatMapLatest diagram

Well, flatMap() gets one value, then performs long task, and when it gets the next value, previous task will still finish even when the new value arrives in the middle of the current task. It isn’t really what we need because when we get a new text in the search bar, we want to cancel the previous request and start another. That’s what flatMapLatest() does.

http://www.thedroidsonroids.com/blog/ios/rxswift-examples-3-networking/

You can use RxMarbles app on Appstore to play around with operators.

dangthaison.91
  • 269
  • 1
  • 5
19

I find this https://github.com/ReactiveX/RxSwift/blob/master/Rx.playground/Pages/Transforming_Operators.xcplaygroundpage/Contents.swift to be useful

Transforms the elements emitted by an Observable sequence into Observable sequences, and merges the emissions from both Observable sequences into a single Observable sequence. This is also useful when, for example, when you have an Observable sequence that itself emits Observable sequences, and you want to be able to react to new emissions from either Observable sequence. The difference between flatMap and flatMapLatest is, flatMapLatest will only emit elements from the most recent inner Observable sequence.

    let disposeBag = DisposeBag()

    struct Player {
        var score: Variable<Int>
    }

    let  = Player(score: Variable(80))
    let  = Player(score: Variable(90))

    let player = Variable()

    player.asObservable()
        .flatMap { $0.score.asObservable() } // Change flatMap to flatMapLatest and observe change in printed output
        .subscribe(onNext: { print($0) })
        .disposed(by: disposeBag)

    .score.value = 85

    player.value = 

    .score.value = 95 // Will be printed when using flatMap, but will not be printed when using flatMapLatest

    .score.value = 100

With flatMap, we get

80
85
90
95
100

With flatMapLatest, we get

80
85
90
100

In this example, using flatMap may have unintended consequences. After assigning to player.value, .score will begin to emit elements, but the previous inner Observable sequence (.score) will also still emit elements. By changing flatMap to flatMapLatest, only the most recent inner Observable sequence (.score) will emit elements, i.e., setting .score.value to 95 has no effect.

flatMapLatest is actually a combination of the map and switchLatest operators.

Also, I find https://www.raywenderlich.com/158205/rxswift-transforming-operators this to be useful

flatMap

keeps up with each and every observable it creates, one for each element added onto the source observable

flatMapLatest

What makes flatMapLatest different is that it will automatically switch to the latest observable and unsubscribe from the the previous one.

onmyway133
  • 45,645
  • 31
  • 257
  • 263
18

It's not clear what your confusion is about. Are you questioning the difference between flatMap and flatMapLatest? flatMap will map to a new Observable, and if it needs to flatMap again, it will in essence merge the two mapped Observables into one. If it needs to flatMap again, it will merge it again, etc.

With flatMapLatest, when a new Observable is mapped, it overwrites the last Observable if there was one. There is no merge.

EDIT: In response to your comment, the reason you aren't seeing any "------>3:" print is because those rx_request Observables were disposed before they could compete, because flatMapLatest received a new element, and this mapped to a new Observable. Upon disposal, rx_request probably cancels the request and will not run the callback where you're printing. The old Observable is disposed because it no longer belongs to anyone when the new one takes its place.

solidcell
  • 7,639
  • 4
  • 40
  • 59
  • the `input.name` isn't changed! There's only one `Observable`. The closure in flatMapLatest called every time, but the network request only returned once. – leizh00701 May 26 '16 at 11:30
  • 3
    I got another explanation about `flatMapLatest`, and it solved my confusion. https://github.com/baconjs/bacon.js/wiki/Diagrams . Thank you. – leizh00701 May 26 '16 at 14:13
8

I think this diagram from Ray Wenderlich tutorial can help.

flatMapLatest diagram

Nominalista
  • 4,632
  • 11
  • 43
  • 102
0

So if we think about an event being emitted from a search box as the user types each time an event is received flatMap would fire off a separate request even if a current request is in flight. flatMapLatest in contrast disposes of the first stream. In the wrapper around URLSession this calls cancel on the request. So if you type really quickly you should see fewer requests returning. There's a brilliant video explaining just this here: https://youtu.be/z8ukiv5flcw . Here's the source to the wrapper around URLSession (notice task.cancel on dispose):

public func response(request: URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)> {
        return Observable.create { observer in

            // smart compiler should be able to optimize this out
            let d: Date?

            if URLSession.rx.shouldLogRequest(request) {
                d = Date()
            }
            else {
               d = nil
            }

            let task = self.base.dataTask(with: request) { data, response, error in

                if URLSession.rx.shouldLogRequest(request) {
                    let interval = Date().timeIntervalSince(d ?? Date())
                    print(convertURLRequestToCurlCommand(request))
                    #if os(Linux)
                        print(convertResponseToString(response, error.flatMap { $0 as NSError }, interval))
                    #else
                        print(convertResponseToString(response, error.map { $0 as NSError }, interval))
                    #endif
                }
                
                guard let response = response, let data = data else {
                    observer.on(.error(error ?? RxCocoaURLError.unknown))
                    return
                }

                guard let httpResponse = response as? HTTPURLResponse else {
                    observer.on(.error(RxCocoaURLError.nonHTTPResponse(response: response)))
                    return
                }

                observer.on(.next((httpResponse, data)))
                observer.on(.completed)
            }

            task.resume()

            return Disposables.create(with: task.cancel)
        }
    }
SmileBot
  • 19,393
  • 7
  • 65
  • 62