Search code examples
swiftrx-swiftchaining

Chain requests and return both results with RxSwift


I have a ContentService that makes a request for an article. That article response contains an authorId property.

I have a ProfileService that allows me to request a user profile by an userId.

I am trying to request an article from the ContentService, chain on a request once that completes to the ProfileService using the authorId property, I would then like to return a ContentArticleViewModel that contains both the article and profile information.

My ArticleInteractor looks something like this -

final class ArticleInteractor: ArticleInteractorInputProtocol {

    let fetchArticleTrigger = PublishSubject<String>()

    private lazy var disposeBag = DisposeBag()

    weak var output: ArticleInteractorOutputProtocol? {
        didSet {
            configureSubscriptions()
        }
    }

    private func configureSubscriptions() {
        guard let output = output else { return }

        fetchArticleTrigger
            .bind(to: dependencies.contentSvc.fetchContentByIdTrigger)
            .disposed(by: disposeBag)

        dependencies.contentSvc.fetchContentByIdResponse
            .bind(to: output.fetchArticleResponse)
            .disposed(by: disposeBag)
    }
}

Quite simply fetchArticleTrigger starts a request, I then bind on dependencies.contentSvc.fetchContentByIdResponse and pick up the response.

The method on my ContentService is -

    // MARK:- FetchContentById
    // @params: id - String
    // return: PublishSubject<ContentArticle>

        fetchContentByIdTrigger
            .flatMapLatest { [unowned self] in self.client.request(.getContentById(id: $0)) }
            .map { (resp: Result<ContentArticle>) in
                guard case .success(let props) = resp else { return ContentArticle() }
                return props
        }
        .bind(to: fetchContentByIdResponse)
        .disposed(by: disposeBag)

I have a very similair setup on my ProfileService -

    // MARK:- FetchUserProfileById
    // @params: id - String
    // return: PublishSubject<User>
        fetchUserProfileByIdTrigger
            .flatMapLatest { [unowned self] in self.client.request(.getProfileByUserId(id: $0)) }
            .map { (resp: Result<User>) in
                guard case .success(let props) = resp else { return User() }
                return props
        }
        .bind(to: fetchUserProfileByIdResponse)
        .disposed(by: disposeBag)

I imagine I will create a model for the article, something like -

struct ContentArticleViewModel {
    var post: ContentArticle
    var user: User
}

I was imaging something like this pseudo code within my ArticleInteractor-

    dependencies.contentSvc.fetchContentByIdResponse
        .flatMapLatest { article in
             /* fetch profile using `article.authorId */
        }.map { article, profile in
            return ContentArticleViewModel(post: article, user: profile)
        }
        .bind(to: output.fetchArticleResponse)
        .disposed(by: disposeBag)

But I am completely lost how best to handle this. I have seen a number of articles on chaining requests but am struggling to apply anything successfully.

EDIT

I have something working currently -

    private func configureSubscriptions() {
        guard let output = output else { return }

        fetchArticleTrigger
            .bind(to: dependencies.contentSvc.fetchContentByIdTrigger)
            .disposed(by: disposeBag)

        dependencies.contentSvc.fetchContentByIdResponse
            .do(onNext: { [unowned self] article in self.dependencies.profileSvc.fetchUserProfileByIdTrigger.onNext(article.creator.userId)})
            .bind(to: fetchArticleResponse)
            .disposed(by: disposeBag)

        let resp = Observable.combineLatest(fetchArticleResponse, dependencies.profileSvc.fetchUserProfileByIdResponse)

        resp
            .map { [unowned self] in self.enrichArticleAuthorProps(article: $0, user: $1) }
            .bind(to: output.fetchArticleResponse)
            .disposed(by: disposeBag)
    }

    private func enrichArticleAuthorProps(article: ContentArticle, user: User) -> ContentArticle {
        var updatedArticle = article
        updatedArticle.creator = user
        return updatedArticle
    }

I am not sure this is correct however.


Solution

  • I'm not sure why you have so much code for such a small job. Below is an example that does what you describe (downloads the article, the downloads the author profile and emits both) with far less code and even it is more code than I would normally use.

    protocol ContentService {
        func requestArticle(id: String) -> Observable<Article>
        func requestProfile(id: String) -> Observable<User>
    }
    
    class Example {
        let service: ContentService
        init(service: ContentService) {
            self.service = service
        }
    
        func bind(trigger: Observable<String>) -> Observable<(Article, User)> {
            let service = self.service
            return trigger
                .flatMapLatest { service.requestArticle(id: $0) }
                .flatMapLatest {
                    Observable.combineLatest(Observable.just($0), service.requestProfile(id: $0.authorId))
                }
        }
    }
    

    Or maybe you want to display the article while waiting for the author profile to download. In that case something like this:

    func bind(trigger: Observable<String>) -> (article: Observable<Article>, author: Observable<User>) {
        let service = self.service
        let article = trigger
            .flatMapLatest { service.requestArticle(id: $0) }
            .share(replay: 1)
    
        let author = article
            .flatMapLatest {
                service.requestProfile(id: $0.authorId)
        }
    
        return (article, author)
    }