UIKit MVVM架构下实现类似SwiftUI @EnvironmentObject的全局ViewModel
在UIKit中实现类似SwiftUI @EnvironmentObject的全局ViewModel方案
针对你需要全局访问单例ViewModel、监听用户状态的需求,以下是两种实用实现方案:
方案一:单例模式(最简直接)
通过单例确保ViewModel仅初始化一次,全局可直接访问,适合快速落地需求。
1. 实现单例ViewModel
import Combine import UserNotifications class UserSessionViewModel { // 全局唯一实例 static let shared = UserSessionViewModel() // 当前用户状态,用@Published支持Combine监听 @Published var currentUser: User? // 私有初始化,禁止外部创建实例 private init() { // 初始化时加载本地缓存的用户数据 loadSavedUser() // 监听用户状态变化 setupUserStateObserver() } private func loadSavedUser() { // 示例:从UserDefaults读取用户数据,实际可替换为CoreData/Keychain currentUser = UserDefaults.standard.decode(User.self, forKey: "currentUser") } private func setupUserStateObserver() { // 监听currentUser变为nil时,推送本地通知 $currentUser.sink { [weak self] user in guard user == nil else { return } self?.sendSessionExpiredNotification() }.store(in: &cancellables) } private func sendSessionExpiredNotification() { let content = UNMutableNotificationContent() content.title = "会话已过期" content.body = "请重新登录以继续使用" content.sound = .default let request = UNNotificationRequest( identifier: "session_expired", content: content, trigger: nil ) UNUserNotificationCenter.current().add(request) } // 存储Combine订阅,避免提前释放 private var cancellables = Set<AnyCancellable>() } // 辅助扩展:简化UserDefaults数据解码 extension UserDefaults { func decode<T: Codable>(_ type: T.Type, forKey key: String) -> T? { guard let data = data(forKey: key) else { return nil } return try? JSONDecoder().decode(type, from: data) } }
2. 在SceneDelegate中触发初始化
单例会在第一次访问时自动初始化,你可以在SceneDelegate中提前触发,确保启动时完成数据加载:
class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // 提前初始化单例ViewModel _ = UserSessionViewModel.shared // 常规窗口初始化逻辑 guard let windowScene = scene as? UIWindowScene else { return } window = UIWindow(windowScene: windowScene) window?.rootViewController = UINavigationController(rootViewController: HomeViewController()) window?.makeKeyAndVisible() } }
3. 在任意ViewController中访问
class HomeViewController: UIViewController { // 直接获取单例实例 let sessionVM = UserSessionViewModel.shared private var cancellables = Set<AnyCancellable>() override func viewDidLoad() { super.viewDidLoad() // 监听用户状态变化,自动跳转登录页 sessionVM.$currentUser.sink { [weak self] user in if user == nil { self?.navigationController?.setViewControllers([LoginViewController()], animated: true) } }.store(in: &cancellables) } }
方案二:依赖注入+全局容器(解耦易测)
如果需要降低耦合、方便单元测试,可通过全局容器管理ViewModel,支持构造注入。
1. 实现全局依赖容器
class AppContainer { // 全局容器实例 static let shared = AppContainer() // 容器管理的ViewModel实例 let userSessionVM: UserSessionViewModel private init() { // 初始化所有依赖 userSessionVM = UserSessionViewModel() } } // ViewModel无需单例,由容器控制生命周期 class UserSessionViewModel { @Published var currentUser: User? private var cancellables = Set<AnyCancellable>() init() { loadSavedUser() setupUserStateObserver() } // 其余方法同方案一... }
2. 在ViewController中注入依赖
class LoginViewController: UIViewController { private let sessionVM: UserSessionViewModel // 构造注入ViewModel init(sessionVM: UserSessionViewModel) { self.sessionVM = sessionVM super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() // 使用sessionVM处理登录逻辑... } }
3. 在SceneDelegate中传递依赖
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { let container = AppContainer.shared let loginVC = LoginViewController(sessionVM: container.userSessionVM) window?.rootViewController = UINavigationController(rootViewController: loginVC) // ... }
方案对比
- 单例模式:优点是实现简单、全局访问便捷;缺点是耦合度高,单元测试时难以替换模拟对象。
- 依赖注入+容器:优点是解耦性强、支持测试,可扩展性好;缺点是需要手动传递依赖,实现稍繁琐。
内容的提问来源于stack exchange,提问作者Santiago Padilla
相关产品推荐
相关产品推荐

