You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为Decodable数据创建可复用的Firestore快照监听器函数

消除Firestore快照监听器的代码重复

问题背景

在ObservableObject子类Model中,为四个Firestore集合分别实现了快照监听器,代码高度重复——仅目标数组属性和数据类型存在差异。尝试用枚举定义集合,但不知道如何避免冗长的if/switch语句来映射数据类型与对应数组。

原始代码:

class Model: ObservableObject {
    @Published var tasks: [Task] = []
    @Published var notes: [Note] = []
    @Published var projects: [Project] = []
    @Published var notebooks: [Notebook] = []
    
    private var db = Firestore.firestore()
    
    init() {
        let _ = db.collection("tasks").whereField("userID", isEqualTo: Auth.auth().currentUser?.uid ?? "")
            .addSnapshotListener { querySnapshot, error in
                guard let documents = querySnapshot?.documents else {
                    print("No documents in 'tasks' collection")
                    return
                }
                self.tasks = documents.compactMap { queryDocumentSnapshot in
                    try? queryDocumentSnapshot.data(as: Task.self)
                }
            }
        let _ = db.collection("projects").whereField("userID", isEqualTo: Auth.auth().currentUser?.uid ?? "")
            .addSnapshotListener { querySnapshot, error in
                guard let documents = querySnapshot?.documents else {
                    print("No documents in 'projects' collection")
                    return
                }
                self.projects = documents.compactMap { queryDocumentSnapshot in
                    try? queryDocumentSnapshot.data(as: Project.self)
                }
            }
        let _ = db.collection("notes").whereField("userID", isEqualTo: Auth.auth().currentUser?.uid ?? "")
            .addSnapshotListener { querySnapshot, error in
                guard let documents = querySnapshot?.documents else {
                    print("No documents in 'notes' collection")
                    return
                }
                self.notes = documents.compactMap { queryDocumentSnapshot in
                    try? queryDocumentSnapshot.data(as: Note.self)
                }
            }
        let _ = db.collection("notebooks").whereField("userID", isEqualTo: Auth.auth().currentUser?.uid ?? "")
            .addSnapshotListener { querySnapshot, error in
                guard let documents = querySnapshot?.documents else {
                    print("No documents in 'notebooks' collection")
                    return
                }
                self.notebooks = documents.compactMap { queryDocumentSnapshot in
                    try? queryDocumentSnapshot.data(as: Notebook.self)
                }
            }
    }
}

尝试的枚举方案:

enum Collection: String, CaseIterable {
    case tasks = "tasks",
    case notes = "notes",
    case projects = "projects",
    case notebooks = "notebooks"
}

for collection in Collection.allCases {
    // Add snapshot listener
}

解决方案

1. 统一数据模型协议

确保所有Firestore数据模型(Task/Note/Project/Notebook)遵循Codable(Firestore的data(as:)方法依赖此协议),可以用一个空协议统一标识:

protocol FirestoreModel: Codable {}

// 扩展每个模型以遵守协议
extension Task: FirestoreModel {}
extension Note: FirestoreModel {}
extension Project: FirestoreModel {}
extension Notebook: FirestoreModel {}

2. 封装泛型监听器逻辑

在Model中编写一个泛型方法,将重复的监听器逻辑抽离出来,接收集合名称、目标数组的KeyPath和数据类型:

private func setupSnapshotListener<T: FirestoreModel>(for collectionName: String, updating keyPath: ReferenceWritableKeyPath<Model, [T]>) {
    guard let userId = Auth.auth().currentUser?.uid else {
        print("未认证用户,跳过\(collectionName)集合监听器")
        return
    }
    
    db.collection(collectionName)
        .whereField("userID", isEqualTo: userId)
        .addSnapshotListener { [weak self] snapshot, error in
            guard let self = self else { return }
            
            if let error = error {
                print("获取\(collectionName)数据失败:\(error.localizedDescription)")
                return
            }
            
            guard let documents = snapshot?.documents else {
                print("\(collectionName)集合无文档")
                return
            }
            
            // 解析文档并更新对应@Published属性
            self[keyPath: keyPath] = documents.compactMap {
                try? $0.data(as: T.self)
            }
        }
}

3. 批量初始化监听器

直接定义包含所有集合元数据的数组,遍历调用泛型方法,彻底消除重复代码:

class Model: ObservableObject {
    @Published var tasks: [Task] = []
    @Published var notes: [Note] = []
    @Published var projects: [Project] = []
    @Published var notebooks: [Notebook] = []
    
    private var db = Firestore.firestore()
    
    init() {
        // 集合元数据:(集合名称, 目标数组KeyPath)
        let collectionConfigs = [
            ("tasks", \.tasks),
            ("notes", \.notes),
            ("projects", \.projects),
            ("notebooks", \.notebooks)
        ]
        
        // 遍历配置,批量设置监听器
        for (name, keyPath) in collectionConfigs {
            setupSnapshotListener(for: name, updating: keyPath)
        }
    }
    
    // 泛型监听器方法(同上)
    private func setupSnapshotListener<T: FirestoreModel>(for collectionName: String, updating keyPath: ReferenceWritableKeyPath<Model, [T]>) {
        // ... 实现代码 ...
    }
}

可选:用枚举优化集合管理

如果希望用枚举统一管理集合名称,可修改枚举为带关联值的类型,直接绑定数据类型和KeyPath:

enum FirestoreCollection {
    case tasks(ReferenceWritableKeyPath<Model, [Task]>)
    case notes(ReferenceWritableKeyPath<Model, [Note]>)
    case projects(ReferenceWritableKeyPath<Model, [Project]>)
    case notebooks(ReferenceWritableKeyPath<Model, [Notebook]>)
    
    var name: String {
        switch self {
        case .tasks: return "tasks"
        case .notes: return "notes"
        case .projects: return "projects"
        case .notebooks: return "notebooks"
        }
    }
    
    static var allCases: [FirestoreCollection] {
        [
            .tasks(\.tasks),
            .notes(\.notes),
            .projects(\.projects),
            .notebooks(\.notebooks)
        ]
    }
}

然后在init中遍历枚举:

init() {
    for collection in FirestoreCollection.allCases {
        switch collection {
        case .tasks(let path):
            setupSnapshotListener(for: collection.name, updating: path)
        case .notes(let path):
            setupSnapshotListener(for: collection.name, updating: path)
        case .projects(let path):
            setupSnapshotListener(for: collection.name, updating: path)
        case .notebooks(let path):
            setupSnapshotListener(for: collection.name, updating: path)
        }
    }
}

这种方式更便于统一管理集合名称,后续新增集合只需在枚举中添加一个case即可。

内容的提问来源于stack exchange,提问作者user19555877

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 18:48:23