Search code examples
iosswiftrx-swiftflatmap

how to connect working steps using RxSwift


class MainViewController: UIViewController {
    let button = UIButton()
    let mainViewModel = MainViewModel()
    let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(button)
        button.frame = CGRect(x: 50, y: 50, width: 100, height: 30)
        
        bind()
    }
    
    func bind() {
        button.rx.tap
            .bind(to: mainViewModel.doAction)
            .disposed(by: disposeBag)
        
        mainViewModel.somethingDoneObservable
            .observeOn(MainScheduler.instance)
            .flatMapLatest { [weak self] value -> Observable<String> in
                print(value)
                
                let vc = Work1ViewController()
                
                self?.present(vc, animated: true, completion: nil)
                
                return vc.work1ViewModel.work1DoneObservable
            }
            .flatMapLatest { [weak self] value -> Observable<String> in
                print(value)

                let vc = Work2ViewController()

                self?.present(vc, animated: true, completion: nil)

                return vc.work2ViewModel.work2DoneObservable
            }
            .subscribe(onNext: { newValue in
                // do something
                // all done
                print(newValue)
            })
            .disposed(by: disposeBag)
    }
}

class MainViewModel {
    let doAction = PublishSubject<Void>()
    let somethingDoneObservable = PublishSubject<String>()
    let disposeBag = DisposeBag()
    
    init() {
        doAction
            .subscribe(onNext: {
                // do something
                let when = DispatchTime.now() + DispatchTimeInterval.milliseconds(5000)
                
                DispatchQueue.main.asyncAfter(deadline: when) {
                    self.somethingDoneObservable.onNext("Something Done!!")
                }
            })
            .disposed(by: disposeBag)
    }
}

// ----------------------------------------------------------------------

class Work1ViewController: UIViewController {
    let button = UIButton()
    let work1ViewModel = Work1ViewModel()
    let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(button)
        button.frame = CGRect(x: 50, y: 50, width: 100, height: 30)
        
        bind()
    }
    
    func bind() {
        button.rx.tap
            .bind(to: work1ViewModel.doAction)
            .disposed(by: disposeBag)

        work1ViewModel.work1DoneObservable
            .subscribe(onNext: { [weak self] _ in
                self?.dismiss(animated: false, completion: nil)
            })
            .disposed(by: disposeBag)
    }
}

class Work1ViewModel {
    let doAction = PublishSubject<Void>()
    let work1DoneObservable = PublishSubject<String>()
    let disposeBag = DisposeBag()
    
    init() {
        doAction
            .subscribe(onNext: {
                // do work1
                let when = DispatchTime.now() + DispatchTimeInterval.milliseconds(5000)
                
                DispatchQueue.main.asyncAfter(deadline: when) {
                    self.work1DoneObservable.onNext("Work1 Done!!")
                }
            })
            .disposed(by: disposeBag)
    }
}

(Work1, Work2ViewController can be dismissed without doAction event.)

as you see, I want to connet several work steps.(work1 -> work2 -> mainVc)

in this case, I used flatMapLatest.

  1. is it correct way? if not, is there common way or other elegant way?

  2. if it is correct, is there any problem?(memory leak or ..)

  3. if I tap MainViewController'button later, is it ok?


Solution

  • There are problems with this specific implementation, but you are on the right track. Consider using my CLE library which takes care of all the gotchas involved. It can be installed using CocoaPods or SPM.

    1. Using flatMapLatest is okay but I think flatMapFirst is better. The difference is in how the system interprets subsequent taps of the button. The flatMapLatest operator will dispose and resubscribe to the inner observable, which in this case will attempt to present viewController1 on top of itself. You will see this as a problem if you tap the button a couple of times before the new view controller presents itself.

    2. There are other problems. If the user dismisses view controller 1 any way other than tapping the button (by swiping down, for example,) there will be a resource leak. Also, you never emit completed events when in your view controllers dismiss which causes resource leaks as well.

    3. No.

    With my library, all the possible ways to dismiss a view controller are accounted for and there are no leaks. The code would look like this:

    final class MainViewController: UIViewController {
        let button = UIButton()
        let disposeBag = DisposeBag()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .white
            view.addSubview(button)
            button.frame = CGRect(x: 50, y: 50, width: 100, height: 30)
            button.backgroundColor = .blue
        }
    }
    
    extension MainViewController {
        func bind() {
            let somethingDone = button.rx.tap
                .flatMapFirst {
                    doAction()
                }
    
            let actionDone = somethingDone
                .flatMapFirst(presentScene(animated: true, scene: { _ in actionFlow() }))
    
            actionDone
                .subscribe(onNext: {
                    print("all done!")
                })
                .disposed(by: disposeBag)
        }
    }
    
    func doAction() -> Observable<String> {
        Observable.create { observer in
            let when = DispatchTime.now() + DispatchTimeInterval.milliseconds(5000)
            DispatchQueue.main.asyncAfter(deadline: when) {
                observer.onSuccess("Something Done!!")
            }
            return Disposables.create()
        }
    }
    
    func actionFlow() -> Scene<Void> {
        let work1 = Work1ViewController().scene { $0.bind() }
        let work2 = work1.action
            .flatMapFirst(presentScene(animated: true, scene: { _ in
                Work2ViewController().scene { $0.bind() }
            }))
            .take(1)
    
        return Scene(controller: work1.controller, action: work2)
    }
    
    //------------------------------------------------------------------------------
    final class Work1ViewController: UIViewController {
        let button = UIButton()
        let disposeBag = DisposeBag()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .gray
            view.addSubview(button)
            button.frame = CGRect(x: 50, y: 50, width: 100, height: 30)
            button.backgroundColor = .blue
        }
    }
    
    extension Work1ViewController {
        func bind() -> Observable<String> {
            return button.rx.tap
                .flatMapFirst { doAction() }
        }
    }
    
    final class Work2ViewController: UIViewController {
        let button = UIButton()
        let disposeBag = DisposeBag()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .darkGray
            view.addSubview(button)
            button.frame = CGRect(x: 50, y: 50, width: 100, height: 30)
            button.backgroundColor = .blue
        }
    }
    
    extension Work2ViewController {
        func bind() -> Observable<Void> {
            button.rx.tap.asObservable()
        }
    }