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

SwiftUI中MVVM能否使用单例解决Firestore Repository多实例问题?

问题1解答

可以确认你的问题完全由Repository多实例化导致。你在初始化每个ViewModel时都通过构造器直接生成新的Repository实例,每个实例内部都持有独立的Snapshot监听器。你登出时仅停止了部分实例的监听器,其余未被关联的实例的监听器仍在后台运行,当用户登出后认证状态失效,这些残留的监听器继续请求Firestore就会触发权限拒绝错误。

问题2解答

针对中大型SwiftUI+Firebase项目的MVVM落地实践可以按照以下方案改造,完全规避你遇到的问题:

1. 引入轻量化依赖容器统一管理Repository实例

不需要使用耦合度高的业务单例,只需要创建一个无业务逻辑的依赖容器,全局唯一持有所有Repository的实例,确保整个应用共用相同的Repository对象:

class DIContainer {
    // 容器本身用单例,仅负责依赖管理,无业务耦合,不属于不良设计
    static let shared = DIContainer()
    
    // 所有Repository仅初始化一次
    let userRepository = UserRepository()
    let listingRepository = ListingRepository()
    let offerRepository = OfferRepository()
    let orderRepository = OrderRepository()
    let businessAddressRepository = BusinessAddressRepository()
    let bankAccountRepository = BankAccountRepository()
    let productRepository = ProductRepository()
    let seafoodRepository = SeafoodRepository()
    
    private init() {}
}

2. 改造入口处ViewModel初始化逻辑

所有ViewModel都从依赖容器中取已初始化的Repository实例,避免重复创建:

@main
struct Global_Seafood_ExchangeApp: App {
    private let diContainer = DIContainer.shared
    
    // 全局需要的StateObject统一初始化,传入相同的Repository实例
    @StateObject private var authListener: AuthSession
    @StateObject private var marketplaceViewModel: MarketplaceViewModel
    @StateObject private var createListingViewModel: CreateListingViewModel
    @StateObject private var accountViewModel: AccountViewModel
    @StateObject private var listingHistoryViewModel: ListingHistoryViewModel
    @StateObject private var offerHistoryViewModel: OfferHistoryViewModel
    @StateObject private var orderHistoryViewModel: OrderHistoryViewModel
    @StateObject private var businessAddressViewModel: BusinessAddressViewModel
    @StateObject private var bankAccountViewModel: BankAccountViewModel
    
    init() {
        _authListener = StateObject(wrappedValue: AuthSession(
            userRepository: diContainer.userRepository,
            productRepository: diContainer.productRepository,
            listingRepository: diContainer.listingRepository,
            offerRepository: diContainer.offerRepository,
            orderRepository: diContainer.orderRepository,
            businessAddressRepository: diContainer.businessAddressRepository,
            bankAccountRepository: diContainer.bankAccountRepository
        ))
        _marketplaceViewModel = StateObject(wrappedValue: MarketplaceViewModel(
            listingRepository: diContainer.listingRepository
        ))
        _createListingViewModel = StateObject(wrappedValue: CreateListingViewModel(
            listingRepository: diContainer.listingRepository,
            productRepository: diContainer.seafoodRepository
        ))
        _accountViewModel = StateObject(wrappedValue: AccountViewModel(
            listingRepository: diContainer.listingRepository,
            offerRepository: diContainer.offerRepository,
            orderRepository: diContainer.orderRepository
        ))
        _listingHistoryViewModel = StateObject(wrappedValue: ListingHistoryViewModel(
            listingRepository: diContainer.listingRepository
        ))
        _offerHistoryViewModel = StateObject(wrappedValue: OfferHistoryViewModel(
            offerRepository: diContainer.offerRepository
        ))
        _orderHistoryViewModel = StateObject(wrappedValue: OrderHistoryViewModel(
            orderRepository: diContainer.orderRepository
        ))
        _businessAddressViewModel = StateObject(wrappedValue: BusinessAddressViewModel(
            businessAddressRepository: diContainer.businessAddressRepository
        ))
        _bankAccountViewModel = StateObject(wrappedValue: BankAccountViewModel(
            bankAccountRepository: diContainer.bankAccountRepository
        ))
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(authListener)
                .environmentObject(marketplaceViewModel)
                .environmentObject(createListingViewModel)
                .environmentObject(accountViewModel)
                .environmentObject(listingHistoryViewModel)
                .environmentObject(offerHistoryViewModel)
                .environmentObject(orderHistoryViewModel)
                .environmentObject(businessAddressViewModel)
                .environmentObject(bankAccountViewModel)
        }
    }
}

注:仅需要全局共享的ViewModel才注入到根视图,页面级ViewModel比如AddBusinessAddressViewModel、AddBankAccountViewModel完全可以在对应页面内部用@StateObject初始化,需要的Repository从环境获取或者从容器取,可大幅减少根视图注入的对象数量。

3. 优化Repository监听器逻辑

去掉Repository初始化方法中启动监听器的逻辑,改由AuthSession在用户登录成功后统一调用所有Repository的startSnapshotListener方法,登出时统一调用stopSnapshotListener方法,确保所有监听器的生命周期和用户认证状态绑定:

// 改造后的ListingRepository初始化方法
init() {
    // 不要在这里启动监听器
}
// AuthSession中登录成功后的逻辑
func loginSuccess() {
    diContainer.userRepository.startSnapshotListener()
    diContainer.listingRepository.startSnapshotListener()
    diContainer.offerRepository.startSnapshotListener()
    // 其他需要启动的监听器
}

// 登出时的逻辑
func logout() {
    diContainer.userRepository.stopSnapshotListener()
    diContainer.listingRepository.stopSnapshotListener()
    diContainer.offerRepository.stopSnapshotListener()
    // 其他需要停止的监听器
    // 可选:清空所有Repository的已缓存数据,避免显示旧数据
}

以上方案完全符合MVVM设计规范,没有不合理的耦合,同时解决了Repository多实例、监听器泄漏的问题,适合中大型SwiftUI+Firebase项目使用。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 17:15:09