怠慢プログラマーの備忘録

怠慢でナマケモノなプログラマーの備忘録です。

【SwiftUI】FirestoreのラッパーをCombineで書いてみた[備忘録]

メモ程度なので雑に書いてます。

Firestore+Combine.swift
import Foundation
import Combine
import FirebaseFirestore

extension Firestore {
    
    func fetchAll<T: EntityProtocol>(collectionKey: String) -> AnyPublisher<[T], Error> {
        return Future<[T], Error> { [weak self] promise in
            self?.collection(collectionKey).getDocuments() { snapshot, error in
                if let err = error {
                    promise(.failure(err))
                }
                if let snapshot = snapshot {
                    let entities: [T] = snapshot.documents.compactMap { T.decode(document: $0.data()) }
                    promise(.success(entities))
                }
            }
        }.eraseToAnyPublisher()
    }
}
Repository.swift
import Foundation
import Combine
import FirebaseFirestore

protocol RepositoryProtocol {
    func fetchAll() -> AnyPublisher<[Entity], Error>
}

final class Repository: RepositoryProtocol {
    func fetchAll() -> AnyPublisher<[Entity], Error> {
        return Firestore.firestore().fetchAll(collectionKey: "sample")
    }
}

※EntityProtocolにはdecodeとdocument変換([String: Any])を定義しています。

ViewModel.swift
import Foundation
import SwiftUI
import Combine

final class ViewModel: ObservableObject {
    @Published private(set) var entities: [Entity] = []
    var cancellables: Set<AnyCancellable> = []

    init(with repository: RepositoryProtocol = Repository()) {        
        repository.fetchAll()
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { _ in },
                receiveValue: { [weak self] entities in
                    self?.entities = entities
                }).store(in: &cancellables)
    }
    deinit {
        cancellables.forEach { $0.cancel() }
    }
}
ContentView.swift
import SwiftUI

struct ContentView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        NavigationView {
            ScrollView {
                ForEach(viewModel.subscriptions, id: \.id) { entity in
                    HStack {
                        VStack {
                            Text(entity.title)
                                .font(.system(size: 18, weight: .medium, design: .default))                  
                            Text(entity.description)
                                .font(.system(size: 14, weight: .medium, design: .default))
                        }
                    }
                }
            }
            .navigationTitle("Content View")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(viewModel: .init())
    }
}