'Am I using firebase api incorrectly?

Whenever paging in tableview, the view model is running fetchDataRx. It works well, but I don't think it's right to sort the entire data every time you paginate and call the addSnapShotListener. If my code is not correct, how can I correct it?

// MARK: ViewController.swift

timeLineTableView.rx.didScroll
        .withLatestFrom(viewModel.activated)
        .subscribe(onNext: { [weak self] isActivated in
            if !isActivated {
                guard let self = self else { return }
                let position = self.timeLineTableView.contentOffset.y
                if position > self.timeLineTableView.contentSize.height - 100 - self.timeLineTableView.frame.size.height {
                    self.viewModel.fetchPosts.onNext(())
                }
            }
        })
        .disposed(by: disposeBag)

//MARK: ViewModel.swift

let fetchPosts: AnyObserver<Void>
let fetching = PublishSubject<Void>()
fetchPosts = fetching.asObserver()
    
    fetching
        .do(onNext: { _ in activating.onNext(true) })
        .withLatestFrom(posts)
        .map { $0.count }
        .flatMap{ (count) -> Observable<[post]> in
                fireBaseService.fetchDataRx(startIdx: count) }
        .map { $0.map { ViewPost(post: $0) } }
        .do(onNext: { _ in activating.onNext(false) })
        .do(onError: { err in error.onNext(err) })
        .subscribe(onNext: { newPosts in
            let oldData = posts.value
            posts.accept(oldData + newPosts)
        })
        .disposed(by: disposeBag)

//MARK: FirebaseService.swift

protocol FirebaseServiceProtocol {
        func fetchDataRx(startIdx: Int) -> Observable<[post]>
        func fetchData(startIdx: Int, completion: @escaping (Result<[post], Error>) -> Void)
    }

class FireBaseService: FirebaseServiceProtocol {
    
    func fetchDataRx(startIdx: Int) -> Observable<[post]> {
        return Observable.create { (observer) -> Disposable in
            
            self.fetchData(startIdx: startIdx) { result in
                switch result {
                case .success(let data):
                    observer.onNext(data)
                case .failure(let error):
                    observer.onError(error)
                }
                observer.onCompleted()
            }
            return Disposables.create()
        }
    }
    func fetchData(startIdx: Int, completion: @escaping (Result<[post], Error>) -> Void) {
        let db = Firestore.firestore()
        
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm"
        
        if startIdx == 0 {
            DispatchQueue.global().async {
                let first = db.collection("lolCourt")
                    .order(by: "date")
                    .limit(to: 8)
                var nextPosts = [post]()
                first.getDocuments() { (querySnapshot, error) in
                    if let error = error {
                        print("Error getting documents: \(error)")
                    } else {
                        for document in querySnapshot!.documents {
                            guard let url = document.data()["url"] as? String else {
                                continue
                            }
                            guard let champion1 = document.data()["champion1"] as? String else {
                                continue
                            }
                            guard let champion1Votes = document.data()["champion1Votes"] as? Double else {
                                continue
                            }
                            guard let champion2 = document.data()["champion2"] as? String else {
                                continue
                            }
                            guard let champion2Votes = document.data()["champion2Votes"] as? Double else {
                                continue
                            }
                            guard let text = document.data()["text"] as? String else {
                                continue
                            }
                            guard let date = document.data()["date"] as? Double else {
                                continue
                            }
                            
                            nextPosts.append(post(url: url,
                                                  champion1: champion1,
                                                  champion1Votes: champion1Votes,
                                                  champion2: champion2,
                                                  champion2Votes: champion2Votes,
                                                  text: text,
                                                  date: formatter.string(from: Date(timeIntervalSince1970: date))))
                        }
                    }
                    completion(.success(nextPosts))
                }
            }
        }
        else {
            DispatchQueue.global().async {
                let first = db.collection("lolCourt")
                    .order(by: "date")
                    .limit(to: startIdx)
                first.addSnapshotListener { (snapshot, error) in
                    guard let snapshot = snapshot else {
                        print("Error retrieving : \(error.debugDescription)")
                        return
                    }
                    guard let lastSnapshot = snapshot.documents.last else {
                        return
                    }
                    let next = db.collection("lolCourt")
                        .order(by: "date")
                        .start(afterDocument: lastSnapshot)
                        .limit(to: 8)
                    var nextPosts = [post]()
                    next.getDocuments() { (querySnapshot, error) in
                        if let error = error {
                            print("Error getting documents: \(error)")
                        } else {
                            for document in querySnapshot!.documents {
                                guard let url = document.data()["url"] as? String else {
                                    continue
                                }
                                guard let champion1 = document.data()["champion1"] as? String else {
                                    continue
                                }
                                guard let champion1Votes = document.data()["champion1Votes"] as? Double else {
                                    continue
                                }
                                guard let champion2 = document.data()["champion2"] as? String else {
                                    continue
                                }
                                guard let champion2Votes = document.data()["champion2Votes"] as? Double else {
                                    continue
                                }
                                guard let text = document.data()["text"] as? String else {
                                    continue
                                }
                                guard let date = document.data()["date"] as? Double else {
                                    continue
                                }
                                
                                nextPosts.append(post(url: url,
                                                      champion1: champion1,
                                                      champion1Votes: champion1Votes,
                                                      champion2: champion2,
                                                      champion2Votes: champion2Votes,
                                                      text: text,
                                                      date: formatter.string(from: Date(timeIntervalSince1970: date))))
                            }
                        }
                        completion(.success(nextPosts))
                    }
                }
            }
        }
    }
    
}


Solution 1:[1]

I think you are doing it wrong... I would expect to see something more like this:

class FireBaseService {
    func getPage<T>(query: Query? =  nil, build: @escaping ([String: Any]) -> T) -> Observable<([T], nextPage: Query?)> {
        Observable.create { observer in
            let db = Firestore.firestore()
            let page = query ?? db.collection("lolCourt")
                .order(by: "date")
                .limit(to: 8)

            let listener = page
                .addSnapshotListener { snapshot, error in
                    guard let snapshot = snapshot else { observer.onError(error ?? RxError.unknown); return }
                    let items = snapshot.documents.map { build($0.data()) }
                    if let lastSnapshot = snapshot.documents.last {
                        let next = page
                            .start(afterDocument: lastSnapshot)
                        observer.onSuccess((items, nextPage: next))
                    }
                    else {
                        observer.onSuccess((items, nextPage: nil))
                    }
                }

            return Disposables.create { listener.remove() }
        }
    }
}

Use the above in your favorite state machine system. Here is an example using my CLE library.

// in view controller
let fireBaseService = FireBaseService()
let activityIndicator = ActivityIndicator()
let errorRouter = ErrorRouter()

func getPage(nextPage: Query?) -> Observable<([Post?], nextPage: Query?)> {
    fireBaseService.getPage(query: nextPage, build: Post.init(dict:))
        .rerouteError(errorRouter)
        .trackActivity(activityIndicator)
}

let posts = cycle(
    inputs: [
        getPage(nextPage: nil).map(ViewModel.Input.response),
        timeLineTableView.rx.reachedBottom(offset: 20).map(to: ViewModel.Input.next)
    ],
    initialState: ([Post?](), nextPage: Query?.none),
    environment: getPage(nextPage:),
    reduce: ViewModel.reduce(state:input:getPage:)
)
    .map { $0.0.compactMap { $0 } }

and the view model:

enum ViewModel {
    enum Input {
        case response([Post?], nextPage: Query?)
        case next
    }

    static func reduce(state: inout ([Post?], nextPage: Query?), input: Input, getPage: @escaping (Query) -> Observable<([Post?], nextPage: Query?)>) -> Observable<Input> {
        switch input {
        case let .response(posts, nextPage):
            state.0 += posts
            state.nextPage = nextPage
        case .next:
            guard let nextPage = state.nextPage else { break }
            return getPage(nextPage)
                .map(Input.response)
        }
        return .empty()
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1