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

