skip to Main Content

I have a favorite Button, the button isSelected changed by the network data and tap the button. When the isSelected by tap the button, it need to request the favorite or unfavorite API, but when it changed by the network data, it is not need to request api.
It is my code, when the isSelected changed it always request the api.

// in viewmodel
 isFavorite = selectedVideo
            .map { $0.isFavorite ?? false }
            .flatMapLatest({ favorite in
                onTapFavorite.scan(favorite) { acc, _ in !acc }.startWith(favorite)
            })
            .distinctUntilChanged()

// when subscribe
 Observable.combineLatest(viewModel.isFavorite, viewModel.selectedVideo)
            .flatMapLatest({ (isFavorite, video) in
            if isFavorite {
               return APIService.favoriteVideo(videoId: video.videoId)
            } else {
                return APIService.unfavoriteVideo(videoId: video.videoId)
            }
        })
            .subscribe(onNext: { _ in

        }).disposed(by: disposeBag)

2

Answers


  1. You have two features, so you should have two Observable chains…

    The server update chain looks like this:

    func updateServer(selectedVideo: Observable<Video>, onTapFavorite: Observable<Void>) -> Observable<(videoId: Video.ID, isFavorite: Bool)> {
        selectedVideo
            .flatMapLatest { video in
                onTapFavorite
                    .scan(video.isFavorite ?? false) { isFavorite, _ in !isFavorite }
                    .map { (videoId: video.id, isFavorite: $0) }
            }
    }
    

    And is bound like this:

    updateServer(selectedVideo: selectedVideo, onTapFavorite: favoriteButton.rx.tap.asObservable())
        .flatMapLatest {
            $0.isFavorite ? APIService.favoriteVideo(videoId: $0.videoId) : APIService.unfavoriteVideo(videoId: $0.videoId)
        }
        .subscribe(onError: { error in
            // handle error
        })
        .disposed(by: disposeBag)
    

    For the isSelected property, use a different Observable chain:

    func isSelected(selectedVideo: Observable<Video>, onTapFavorite: Observable<Void>) -> Observable<Bool> {
        selectedVideo
            .map { $0.isFavorite ?? false }
            .flatMapLatest { isFavorite in
                onTapFavorite
                    .scan(isFavorite) { isFavorite, _ in !isFavorite }
                    .startWith(isFavorite)
            }
    }
    

    which is bound like this:

    isSelected(selectedVideo: selectedVideo, onTapFavorite: favoriteButton.rx.tap.asObservable())
        .bind(to: favoriteButton.rx.isSelected)
        .disposed(by: disposeBag)
    
    Login or Signup to reply.
  2. In reviewing the requirements for this question. I see there can be a lot of complexity when taking errors into account. I decided to give an answer that uses my Cause-Logic-Effect tools to fully flesh out the answer including error conditions:

    The following code takes care of all of the following:

    • When a new video is selected, (through selectedVideo) it updates the state of the button without making a network call.
    • When the user taps the button it updates the state of the button, then makes the network call. If the call fails, it resets the button to its previous state and the api object notifies error subscribers of the error (in case you want to put up an alert or something letting the user know the error occurred.)
    • If the user taps the button while a network call is in flight, it will cancel the in-flight call and make the new call for the updated state.
    • (An interesting edge case) If the video updates while the network call is in flight and then the network call fails, it guards against updating the new video object with the old video object information.
    struct Video: Identifiable {
        let id: Int
        let isFavorite: Bool
    
        func isFavoriteToggled() -> Video {
            Video(id: id, isFavorite: !isFavorite)
        }
    }
    
    func bind(favoriteButton: UIButton, selectedVideo: Observable<Video>, disposeBag: DisposeBag, api: API) {
        enum Input {
            case update(Video)
            case networkFailure(Video)
            case tap
        }
    
        let state = cycle(
            input: Observable.merge(
                selectedVideo.map(Input.update),
                favoriteButton.rx.tap.map(to: Input.tap)
            ),
            initialState: Video?.none,
            reduce: { state, input in
                switch input {
                case .update(let video):
                    state = video
                case .networkFailure(let video):
                    if state?.id == video.id {
                        state = video
                    }
                case .tap:
                    state = state.map { $0.isFavoriteToggled() }
                }
            },
            reaction: { action in
                action
                    .compactMap { state, input in
                        guard case .tap = input else { return nil }
                        return state
                    }
                    .flatMapLatest { video in
                        (
                            video.isFavorite
                            ? api.successResponse(.unfavoriteVideo(videoId: video.id))
                            : api.successResponse(.favoriteVideo(videoId: video.id))
                        )
                        .filter { !$0 }
                        .map { _ in Input.networkFailure(video) }
                    }
            }
        )
    
        state
            .compactMap { $0?.isFavorite }
            .bind(to: favoriteButton.rx.isSelected)
            .disposed(by: disposeBag)
    }
    
    extension Endpoint where Response == Void {
        static func favoriteVideo(videoId: Video.ID) -> Endpoint { fatalError("create url request here") }
        static func unfavoriteVideo(videoId: Video.ID) -> Endpoint { fatalError("create url request here") }
    }
    

    The above is how I would write the code if it was in a project I was working on. Likely, I would pull the closures out into separate functions that I could test.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search