Firestore Optimization

Paginazione Efficiente e Query Complesse in Cloud Firestore con Cursori in Swift

In una collezione Cloud Firestore contenente decine di migliaia di documenti, scaricare l'intera lista in un'unica richiesta causa rallentamenti nella UI ed un consumo eccessivo di operazioni di lettura fatturate su Firebase. La soluzione risiede nella **paginazione basata su cursori di documenti** (DocumentSnapshot).

1. Come Funzionano i Cursori `start(afterDocument:)`

A differenza dei database SQL tradizionali dove si usa OFFSET (che richiede comunque la scansione dei record precedenti), Firestore utilizza l'ultimo DocumentSnapshot recuperato come punto di partenza per la pagina successiva.

2. ViewModel di Paginazione Infinita in Swift

Ecco l'implementazione pulita di un ViewModel SwiftUI che gestisce il caricamento a blocchi di 15 elementi per volta:

import SwiftUI
import FirebaseFirestore

class PaginatedFeedViewModel: ObservableObject {
    @Published var posts: [Post] = []
    @Published var isLoading: Bool = false
    @Published var canLoadMore: Bool = true
    
    private var lastDocument: DocumentSnapshot?
    private let pageSize = 15
    private let db = Firestore.firestore()
    
    func fetchNextPage() {
        guard !isLoading && canLoadMore else { return }
        isLoading = true
        
        var query: Query = db.collection("posts")
            .order(by: "createdAt", descending: true)
            .limit(to: pageSize)
        
        // Se esiste un ultimo documento caricato, parte da quello
        if let lastDocument = lastDocument {
            query = query.start(afterDocument: lastDocument)
        }
        
        query.getDocuments { [weak self] snapshot, error in
            guard let self = self, let documents = snapshot?.documents else {
                self?.isLoading = false
                return
            }
            
            if documents.count < self.pageSize {
                self.canLoadMore = false
            }
            
            if let last = documents.last {
                self.lastDocument = last
            }
            
            let newPosts = documents.compactMap { try? $0.data(as: Post.self) }
            DispatchQueue.main.async {
                self.posts.append(contentsOf: newPosts)
                self.isLoading = false
            }
        }
    }
}

3. Infinite Scroll in SwiftUI con Trigger Automatico

Nella vista SwiftUI scateniamo il caricamento della pagina successiva quando l'utente arriva in prossimità dell'ultimo elemento della lista:

struct FeedListView: View {
    @StateObject private var viewModel = PaginatedFeedViewModel()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.posts) { post in
                    PostRow(post: post)
                        .onAppear {
                            if post == viewModel.posts.last {
                                viewModel.fetchNextPage()
                            }
                        }
                }
                
                if viewModel.isLoading {
                    ProgressView()
                        .frame(maxWidth: .infinity, alignment: .center)
                }
            }
            .navigationTitle("Feed Notizie")
            .onAppear {
                if viewModel.posts.isEmpty {
                    viewModel.fetchNextPage()
                }
            }
        }
    }
}

Conclusione

L'uso corretto dei cursori Firestore assicura tempi di risposta immediati ed un controllo rigoroso sui costi di lettura. Per ottimizzare le query del tuo database Firestore, contatta diallooyunus@gmail.com.

Yunus Diallo (DialloDev)

Fondatore di Dywtal Digital a Parma (Italia). Specializzato in architetture database ad altissime prestazioni per iOS.