How to emit inital data and then combine with new using RxSwift?

47 Views Asked by At

I use 3 apis to get datas for my tableview. First, feedRepository.findOtherFeed which returns Observable of Tuple containing data array and Bool. For each element in the data array I call userRepository.findProfileImg which fetches the profile image for each data. And each data has a property called userFeed.imageIdList, and for each id I call feedRepository.findFeedImage to get the image for the data.

These datas are going to be displayed in tableView. I want to show the users as fast as I can meaning I can't wait for all data's imageIdList calling feedRepository.findFeedImage. Whenever userRepository.findProfileImg is done I want to show the users first with what I've got and then when feedRepository.findFeedImage is all done I want to update the datas.

I'm not that fluent using RxSwift, so I am struggling with how I can implement this.

    private func fetchAndProcessFeedsFinal(setSortOption: SortOption, page: Int?) -> Observable<Mutation> {
        
        if page != nil {
            resetPagination()
        } else if isLastPage {
            return .empty()
        } else {
            pages += 1
        }
        
        let nickname = "lemon""
    
        return feedRepository.findOtherFeed(request: FindOtherFeedRequest(nickname: nickname, date: requestDate, page: pages, size: 7)) 
            .flatMap { [weak self] (userFeeds, isLast) -> Observable<Mutation> in
                guard let self = self else { return .empty() }
    
                isLastPage = isLast

                if userFeeds.isEmpty { return Observable.just(.updateDataSource([])) }
                
                // MARK: inside processFeedWithProfileImage function it calls findProfileImg 
                let observables: [Observable<UserFeedSection.Item>] = userFeeds.map {[weak self] feed in
                    guard let self = self else { return Observable.just(.feed(FeedReactor(userFeed: feed, feedRepository: FeedRepository(), userRepository: UserRepository()))) }

                    return processFeedWithProfileImage(feed)
                } 
                
              
                let feedWithImage = userFeeds.flatMap { userFeed in
                    userFeed.imageIdList.map { [weak self] imageId in
                        return self?.feedRepository.findFeedImage(request: FindFeedImageRequest(imageId: imageId))
                    }
                }
             
                return Observable.zip(observables)
                    .map { items in
                        return Mutation.updateDataSource(items)
                    }
                
            
            }
    }

I've tried zip and combineLatest but I think I didn't use it the right way..

2

There are 2 best solutions below

0
Daniel T. On

This is a pretty complex ask, especially because of pagination. There isn't enough info in your sample to handle the full ask.

Here is how I would likely do something like this using my CLE system.

The basic idea is as follows:

  • whenever the table view reaches the bottom, attempt to get a page of UserFeeds. Start with an initial request for the first page.
  • whenever a page of UserFeeds comes in, request all the profile images.
  • also when a page of UserFeeds comes in, request all the feedImages.
  • whenever a profile image comes in, store it in the state.
  • whenever a feedImage comes in, store it in the state.

The idea then is to follow the output of the example(api:tableView:) function. Whenever a page or image comes in, the state will be updated. Its items array will contain all of the UserFeeds downloaded so far and all the images for each userFeed object inside a CellDisplayable object.

enum Input {
    case receivePage(Int, [UserFeed])
    case addProfileImage(UserFeed, UIImage)
    case addFeedImage(String, UIImage)
    case getPage
}

struct CellDisplayable {
    let userFeed: UserFeed
    let profileImage: UIImage?
    let feedImages: [UIImage]
}

struct State {
    var pages: [Int: [UserFeed]] = [:]
    var profileImages: [UserFeed: UIImage] = [:]
    var feedImages: [String: UIImage] = [:]
    var items: [CellDisplayable] {
        pages.sorted { $0.key < $1.key }
            .flatMap { userFeeds in
                userFeeds.value.map {
                    CellDisplayable(
                        userFeed: $0,
                        profileImage: profileImages[$0],
                        feedImages: $0.imageIdList.compactMap { feedImages[$0] }
                    )
                }
            }
    }
}

func example(api: API, tableView: UITableView) -> Observable<State> {
    cycle(
        input: tableView.rx.reachedBottom().startWith(()).map(to: Input.getPage),
        initialState: State(),
        reduce: { state, input in
            switch input {
            case let .receivePage(page, userFeeds):
                state.pages = [page: userFeeds]
            case let .addProfileImage(userFeed, image):
                state.profileImages[userFeed] = image
            case let .addFeedImage(imageId, image):
                state.feedImages[imageId] = image
            case .getPage:
                break
            }
        },
        reactions: [
            getProfileImages(api: api),
            getFeedImages(api: api),
            getPage(api: api)
        ]
    )
}

func getProfileImages(api: API) -> (Observable<(State, Input)>) -> Observable<Input> {
    { reaction in
        reaction
            .flatMap { userFeeds(from: $1) }
            .flatMap { userFeed in
                api.response(.findProfileImage(feed: userFeed))
                    .map { (userFeed, $0) }
            }
            .map { Input.addProfileImage($0.0, $0.1) }
    }
}

func getFeedImages(api: API) -> (Observable<(State, Input)>) -> Observable<Input> {
    { reaction in
        reaction
            .flatMap { userFeeds(from: $1) }
            .flatMap { Observable.from($0.imageIdList) }
            .flatMap { imageId in
                api.response(.findFeedImage(imageId: imageId))
                    .map { (imageId, $0) }
            }
            .map { Input.addFeedImage($0.0, $0.1) }
    }
}

func getPage(api: API) -> (Observable<(State, Input)>) -> Observable<Input> {
    { reaction in
        reaction
            .compactMap { neededPage(state: $0, input: $1) }
            .flatMap { page in
                api.response(.findOtherFeed(nickname: "lemon", date: Date(), page: page, size: 7))
                    .map { (page, $0.0) }
            }
            .map { Input.receivePage($0.0, $0.1) }
    }
}

func userFeeds(from input: Input) -> Observable<UserFeed> {
    guard case let .receivePage(_, userFeeds) = input else { return .empty() }
    return .from(userFeeds)
}

func neededPage(state: State, input: Input) -> Int? {
    guard case .getPage = input else { return nil }
    return (state.pages.keys.max() ?? 0) + 1
}
0
Daniel T. On

Here is an answer that doesn't require a feedback system, but it can only pull down one page. You will have to update it to be able to pull down additional pages.

The key to this idea is the use of merge (you don't want to use zip or combineLatest.) After each network request completes, the result Observable will emit an array containing all the information retrieved so far for the page in question.

struct CellDisplayable {
    let userFeed: UserFeed
    var profileImage: UIImage?
    var feedImages: [UIImage?]
}

func example(api: API) -> Observable<[CellDisplayable]> {
    enum Input {
        case userFeeds([UserFeed])
        case profileImage(UserFeed, UIImage)
        case feedImage(String, UIImage)
    }

    let userFeeds = api.response(.findOtherFeed(nickname: "lemon", date: Date(), page: 1, size: 7))
        .share()

    let profileImages = userFeeds
        .flatMap { _, userFeeds in
            Observable.merge(userFeeds.map { userFeed in
                api.response(.findProfileImage(feed: userFeed))
                    .map { (userFeed, $0) }
            })
        }

    let feedImages = userFeeds
        .flatMap { _, userFeeds in
            Observable.merge(
                userFeeds
                    .flatMap { $0.imageIdList }
                    .map { imageId in
                        api.response(.findFeedImage(imageId: imageId))
                            .map { (imageId, $0) }
                    }
            )
        }

    return Observable<Input>.merge(
        userFeeds.map { Input.userFeeds($1) },
        profileImages.map { Input.profileImage($0.0, $0.1) },
        feedImages.map { Input.feedImage($0.0, $0.1) }
    )
        .scan(into: [CellDisplayable]()) { state, next in
            switch next {
            case let .userFeeds(userFeeds):
                state = userFeeds.map { CellDisplayable(userFeed: $0, profileImage: nil, feedImages: .init(repeating: nil, count: $0.imageIdList.count)) }
            case let .profileImage(userFeed, image):
                if let index = state.firstIndex(where: { $0.userFeed == userFeed }) {
                    state[index].profileImage = image
                }
            case let .feedImage(imageId, image):
                if let index = state.firstIndex(where: { $0.userFeed.imageIdList.contains(imageId) }),
                   let imageIndex = state[index].userFeed.imageIdList.firstIndex(of: imageId) {
                    state[index].feedImages[imageIndex] = image
                }
            }
        }
}