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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 07:27:32