在Swift语言和Combine响应式编程日益成熟的今天,如何构建一套既能保证代码质量,又能提升团队协作效率的iOS应用开发框架,是每个技术团队面临的核心挑战。本文将深入探讨一套基于MVVM模式、可直接用于生产环境的iOS App开发框架设计方案。该框架深度融合了现代iOS开发范式,旨在通过标准化的工程结构、清晰的业务分层和强大的通用能力封装,实现“高内聚、低耦合、易扩展、易维护”的目标,让开发者能够专注于业务逻辑创新,而非重复的基础建设。

一、框架设计哲学:超越传统的MVVM实践

MVVM(Model-View-ViewModel)模式的核心魅力在于其数据驱动和关注点分离的思想。然而,在实际落地过程中,开发者常常会陷入ViewModel过于臃肿、数据流向不清晰、层间依赖复杂的困境。我们设计的这套框架,旨在通过以下五个核心设计原则,规避这些常见陷阱,为应用注入更强大的可维护性和可测试性。这就像为复杂的神经网络设计清晰的层级结构,每一层都有明确的职责和输入输出,从而保证整个系统的稳定与高效。

设计原则具体落地方式
单向数据流View → ViewModel → Model → ViewModel → View,所有数据变更仅通过/传递,禁止 View 直接修改 Model
层间严格隔离View 只依赖 ViewModel,ViewModel 只依赖 Repository/Service,Repository 只依赖 Model / 网络 / 存储,禁止跨层调用
协议化抽象核心能力(网络、存储、定位)通过协议定义,实现类和接口分离,便于单元测试和替换实现
组合优于继承基础能力通过「协议扩展 + 组合」实现,避免深层继承链;ViewModel 通过做轻量封装,而非强继承
响应式优先基于 Combine 实现数据绑定,View 层仅订阅 ViewModel 的发布属性,无手动刷新 UI 的代码

这些原则共同构成了框架的基石,确保开发过程始终沿着清晰、规范的路径前进。[AFFILIATE_SLOT_1]

二、工程骨架:模块化与清晰度并重的结构设计

一个优秀的项目始于一个清晰的结构。我们推荐以下经过验证的标准化工程结构,它适配Xcode 15+,并支持Swift Package Manager或Cocoapods进行依赖管理。这种结构设计借鉴了机器学习项目中常见的模块化思想,将不同功能的代码进行物理隔离,便于独立开发、测试和复用。

YourApp/
├── App/                          # 应用入口(生命周期、全局配置)
│   ├── AppDelegate.swift         # UIKit入口(或SwiftUI的App结构体)
│   ├── SceneDelegate.swift       # 多场景配置(UIKit)
│   └── AppConfiguration.swift    # 全局配置(环境、主题、功能开关)
├── Core/                         # 核心层(框架基础,无业务逻辑)
│   ├── Base/                     # 基础抽象(协议、基类)
│   │   ├── BaseViewModel.swift   # ViewModel基类(封装Combine、加载状态)
│   │   ├── BaseViewController.swift # View基类(UIKit,封装通用UI逻辑)
│   │   ├── BaseView.swift        # 自定义View基类(UIKit/SwiftUI)
│   │   └── BaseRepository.swift  # 数据仓库基类(封装网络/存储通用逻辑)
│   ├── Common/                   # 通用工具(全项目复用)
│   │   ├── Extensions/           # 系统类扩展(UIKit/Swift/Combine)
│   │   ├── Constants/            # 常量(枚举、静态常量)
│   │   ├── Utils/                # 工具类(加密、验证、格式化)
│   │   └── Logger/               # 日志工具(分级打印、埋点)
│   ├── Services/                 # 核心服务(协议+实现,通用能力)
│   │   ├── Network/              # 网络服务(复用之前的Moya/GraphQL封装)
│   │   ├── Storage/              # 存储服务(Keychain/ UserDefaults/ CoreData)
│   │   ├── Location/             # 定位服务(CoreLocation封装)
│   │   ├── Analytics/            # 埋点服务(友盟/GA封装)
│   │   └── Theme/                # 主题服务(暗黑模式、换肤)
│   └── Models/                   # 全局通用模型(如BaseResponse、UserModel)
├── Modules/                      # 业务模块(按功能拆分,高内聚)
│   ├── User/                     # 用户模块(登录、个人中心)
│   │   ├── View/                 # 视图层(VC/View/UI组件)
│   │   ├── ViewModel/            # 视图模型层(业务逻辑)
│   │   ├── Repository/           # 数据仓库层(网络/存储数据聚合)
│   │   ├── Model/                # 模块内私有模型
│   │   └── Router/               # 模块路由(页面跳转)
│   ├── Home/                     # 首页模块
│   ├── Order/                    # 订单模块
│   └── Product/                  # 商品模块
├── Resources/                    # 资源文件(非代码)
│   ├── Assets.xcassets           # 图片/颜色/字体资源
│   ├── Localizable.strings       # 多语言
│   ├── Configs/                  # 配置文件(plist/json)
│   └── Fonts/                    # 自定义字体
└── Tests/                        # 单元测试/UI测试
    ├── UnitTests/                # 单元测试(ViewModel/Repository)
    └── UITests/                  # UI测试(View层)

关键结构解析:

  • Core层:这是框架的“基础设施”,包含所有与业务无关的纯框架代码,可独立抽离为Swift Package,供多个项目复用。
  • Modules层:按“业务域”(如用户、首页、订单)而非技术层进行拆分。每个模块都是高内聚的独立单元,模块间通过Router或协议接口通信,严格禁止直接依赖,有效降低了耦合度。
  • Repository层:作为MVVM的“数据统一入口”,负责聚合网络请求、本地存储及第三方SDK的数据。ViewModel仅依赖Repository接口,这使得单元测试时可以轻松替换实现,极大提升了可测试性。
  • Router层:统一管理页面导航,解耦View/ViewModel与具体的ViewController创建逻辑。

三、核心基石:生产级的基础层封装

Core层是框架能力的集中体现,所有封装均遵循“协议化、响应式、可测试”的原则。下面我们来看几个关键的基类实现,它们就像深度学习框架中的基础算子,为上层复杂的业务逻辑提供稳定支撑。

1. ViewModel基类 (BaseViewModel)
封装了Combine订阅的生命周期管理、统一的加载状态和错误处理机制,避免在每个ViewModel中编写重复代码。

import Foundation
import Combine
/// MVVM核心:ViewModel基类(所有ViewModel的父类)
open class BaseViewModel: ObservableObject {
    // MARK: - 核心属性
    /// 管理Combine订阅,防止内存泄漏(自动跟随ViewModel销毁)
    internal var cancellables = Set()
    /// 加载状态(View层绑定,控制加载动画)
    @Published public var loadingState: LoadingState = .idle
    /// 全局错误(View层绑定,展示错误提示)
    @Published public var error: AppError?
    // MARK: - 生命周期
    public init() {}
    deinit {
        cancellables.removeAll()
        #if DEBUG
        print("[MVVM] \(self) deinit ✅")
        #endif
    }
    // MARK: - 通用方法
    /// 标记请求开始(设置加载状态为loading)
    public func startLoading() {
        loadingState = .loading
    }
    /// 标记请求结束(恢复加载状态为idle)
    public func stopLoading() {
        loadingState = .idle
    }
    /// 处理错误(统一设置error属性,可扩展全局错误处理)
    public func handleError(_ error: AppError) {
        self.error = error
        stopLoading()
        // 可选:全局错误上报/弹窗
        ErrorHandler.shared.handle(error)
    }
}
// MARK: - 加载状态枚举(覆盖常见场景)
public enum LoadingState: Equatable {
    case idle       // 初始状态
    case loading    // 加载中
    case empty      // 加载完成但无数据
    case error      // 加载失败
}
// MARK: - 全局错误枚举(覆盖APP所有错误场景)
public enum AppError: LocalizedError, Equatable {
    case network(NetworkError)      // 网络错误(复用之前的NetworkError)
    case storage(String)            // 存储错误
    case business(code: Int, msg: String) // 业务错误
    case system(String)             // 系统错误(如权限、解析)
    case custom(String)             // 自定义错误
    public var errorDescription: String? {
        switch self {
        case .network(let error):
            return error.localizedDescription
        case .storage(let msg):
            return "存储错误:\(msg)"
        case .business(_, let msg):
            return msg
        case .system(let msg):
            return "系统错误:\(msg)"
        case .custom(let msg):
            return msg
        }
    }
    // Equatable实现(便于比较)
    public static func == (lhs: AppError, rhs: AppError) -> Bool {
        switch (lhs, rhs) {
        case (.network(let l), .network(let r)): return l.localizedDescription == r.localizedDescription
        case (.storage(let l), .storage(let r)): return l == r
        case (.business(let lCode, let lMsg), .business(let rCode, let rMsg)): return lCode == rCode && lMsg == rMsg
        case (.system(let l), .system(let r)): return l == r
        case (.custom(let l), .custom(let r)): return l == r
        default: return false
        }
    }
}
// MARK: - 全局错误处理器(可扩展)
public final class ErrorHandler {
    public static let shared = ErrorHandler()
    private init() {}
    /// 统一处理错误(可扩展:弹窗、埋点、日志)
    public func handle(_ error: AppError) {
        #if DEBUG
        print("[Error] \(error.errorDescription ?? "未知错误")")
        #endif
        // 生产环境:根据错误类型展示吐司/弹窗
        switch error {
        case .network(.noNetwork):
            ToastManager.shared.show(text: "网络连接失败,请检查网络")
        case .network(.tokenExpired):
            NotificationCenter.default.post(name: .tokenExpired, object: nil)
        default:
            ToastManager.shared.show(text: error.errorDescription!)
        }
    }
}
// MARK: - 通知扩展(全局通知)
extension Notification.Name {
    public static let tokenExpired = Notification.Name("kTokenExpired")
}

2. ViewController基类 (BaseViewController)
为UIKit的View层提供通用模板,处理导航栏、生命周期以及数据绑定的公共逻辑。

import UIKit
import Combine
/// UIKit基础ViewController(所有VC的父类)
open class BaseViewController: UIViewController {
    // MARK: - 核心属性
    /// 关联的ViewModel(子类需初始化)
    public var viewModel: VM!
    /// 管理Combine订阅(View层)
    private var viewCancellables = Set()
    /// 加载动画View(全局复用)
    private lazy var loadingView: LoadingIndicatorView = {
        let view = LoadingIndicatorView(frame: self.view.bounds)
        view.isHidden = true
        return view
    }()
    // MARK: - 生命周期
    public override func viewDidLoad() {
        super.viewDidLoad()
        setupBaseUI()
        setupBindings() // 数据绑定(子类重写)
        setupViewModel() // 初始化ViewModel(子类重写)
        setupNavigation() // 导航栏配置(子类重写)
        setupGesture() // 手势配置(如点击空白处收起键盘)
    }
    public override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.setNavigationBarHidden(navigationBarHidden, animated: animated)
    }
    deinit {
        viewCancellables.removeAll()
        #if DEBUG
        print("[VC] \(self) deinit ✅")
        #endif
    }
    // MARK: - 基础UI配置(子类可重写)
    open func setupBaseUI() {
        view.backgroundColor = .systemBackground
        view.addSubview(loadingView)
        // 绑定ViewModel的加载状态
        viewModel.$loadingState
            .sink { [weak self] state in
                guard let self = self else { return }
                switch state {
                case .loading:
                    self.loadingView.isHidden = false
                    self.loadingView.startAnimating()
                default:
                    self.loadingView.isHidden = true
                    self.loadingView.stopAnimating()
                }
            }
            .store(in: &viewCancellables)
        // 绑定ViewModel的错误
        viewModel.$error
            .compactMap { $0 }
            .sink { [weak self] error in
                self?.handleViewModelError(error)
            }
            .store(in: &viewCancellables)
    }
    // MARK: - 子类需重写的方法(核心)
    /// 初始化ViewModel(必须重写)
    open func setupViewModel() {
        fatalError("子类必须实现setupViewModel()")
    }
    /// 数据绑定(ViewModel → View)
    open func setupBindings() {}
    /// 导航栏配置
    open func setupNavigation() {}
    /// 手势配置
    open func setupGesture() {
        let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
        view.addGestureRecognizer(tap)
    }
    // MARK: - 通用方法
    /// 处理ViewModel的错误(子类可重写)
    open func handleViewModelError(_ error: AppError) {}
    /// 收起键盘
    @objc open func dismissKeyboard() {
        view.endEditing(true)
    }
    /// 是否隐藏导航栏(子类可重写)
    open var navigationBarHidden: Bool { false }
    /// 跳转页面(封装导航跳转,统一管理)
    open func push(_ vc: UIViewController, animated: Bool = true) {
        navigationController?.pushViewController(vc, animated: animated)
    }
    /// 弹出页面
    open func pop(animated: Bool = true) {
        navigationController?.popViewController(animated: animated)
    }
    /// 弹出到根页面
    open func popToRoot(animated: Bool = true) {
        navigationController?.popToRootViewController(animated: animated)
    }
}
// MARK: - 通用加载动画View(可自定义)
final class LoadingIndicatorView: UIView {
    private let activityIndicator = UIActivityIndicatorView(style: .large)
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupUI()
    }
    private func setupUI() {
        backgroundColor = UIColor.black.withAlphaComponent(0.3)
        activityIndicator.center = center
        activityIndicator.color = .white
        addSubview(activityIndicator)
        activityIndicator.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor),
            activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor)
        ])
    }
    func startAnimating() {
        activityIndicator.startAnimating()
    }
    func stopAnimating() {
        activityIndicator.stopAnimating()
    }
}

3. Repository基类 (BaseRepository)
定义数据仓库的通用行为,是实现ViewModel与数据源解耦的关键。ViewModel通过它获取数据,而不关心数据来自网络还是数据库。

import Foundation
import Combine
/// 数据仓库基类(所有Repository的父类)
open class BaseRepository {
    // MARK: - 核心属性
    /// 管理Combine订阅
    internal var cancellables = Set()
    /// 网络服务(通过协议抽象,便于替换)
    internal let networkService: NetworkServiceProtocol
    /// 存储服务(通过协议抽象)
    internal let storageService: StorageServiceProtocol
    // MARK: - 初始化
    public init(
        networkService: NetworkServiceProtocol = NetworkService.shared,
        storageService: StorageServiceProtocol = StorageService.shared
    ) {
        self.networkService = networkService
        self.storageService = storageService
    }
    deinit {
        cancellables.removeAll()
        #if DEBUG
        print("[Repository] \(self) deinit ✅")
        #endif
    }
    // MARK: - 通用方法
    /// 将NetworkError转换为AppError
    internal func mapNetworkError(_ error: NetworkError) -> AppError {
        return .network(error)
    }
    /// 将存储错误转换为AppError
    internal func mapStorageError(_ msg: String) -> AppError {
        return .storage(msg)
    }
    /// 业务错误转换
    internal func mapBusinessError(code: Int, msg: String) -> AppError {
        return .business(code: code, msg: msg)
    }
}
// MARK: - 网络服务协议(抽象接口,与实现分离)
public protocol NetworkServiceProtocol {
    /// 发起RESTful请求(复用之前的Moya封装)
    func request(_ api: BaseAPI, config: RESTfulRequestConfig) -> AnyPublisher
    /// 发起无数据返回的请求
    func requestWithoutData(_ api: BaseAPI, config: RESTfulRequestConfig) -> AnyPublisher
}
// MARK: - 存储服务协议
public protocol StorageServiceProtocol {
    /// 存储数据到UserDefaults
    func set(_ value: T, forKey key: String) throws
    /// 从UserDefaults读取数据
    func get(forKey key: String) -> T?
    /// 存储数据到Keychain
    func setToKeychain(_ value: String, forKey key: String) throws
    /// 从Keychain读取数据
    func getFromKeychain(forKey key: String) -> String?
    /// 删除数据
    func remove(forKey key: String)
}
// MARK: - 网络服务实现(适配协议)
final class NetworkService: NetworkServiceProtocol {
    public static let shared = NetworkService()
    private init() {}
    func request(_ api: BaseAPI, config: RESTfulRequestConfig) -> AnyPublisher {
        return RESTfulRequestTool.request(api, config: config)
    }
    func requestWithoutData(_ api: BaseAPI, config: RESTfulRequestConfig) -> AnyPublisher {
        return RESTfulRequestTool.requestWithoutData(api, config: config)
    }
}
// MARK: - 存储服务实现(适配协议)
final class StorageService: StorageServiceProtocol {
    public static let shared = StorageService()
    private let userDefaults = UserDefaults.standard
    private let keychain = KeychainSwift() // 需引入pod 'KeychainSwift'
    private init() {}
    func set(_ value: T, forKey key: String) throws {
        let data = try JSONEncoder().encode(value)
        userDefaults.set(data, forKey: key)
    }
    func get(forKey key: String) -> T? {
        guard let data = userDefaults.data(forKey: key) else { return nil }
        return try? JSONDecoder().decode(T.self, from: data)
    }
    func setToKeychain(_ value: String, forKey key: String) throws {
        guard keychain.set(value, forKey: key) else {
            throw NSError(domain: "StorageService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Keychain存储失败"])
        }
    }
    func getFromKeychain(forKey key: String) -> String? {
        return keychain.get(key)
    }
    func remove(forKey key: String) {
        userDefaults.removeObject(forKey: key)
        keychain.delete(key)
    }
}

此外,Core层还包含丰富的通用工具,例如简化Auto Layout的UI扩展、增强Combine易用性的操作符、以及全局的Toast管理器等,这些工具能显著提升开发效率。

import UIKit
extension UIView {
    /// 添加子视图(批量)
    func addSubviews(_ views: UIView...) {
        views.forEach { addSubview($0) }
    }
    /// 取消所有约束
    func removeAllConstraints() {
        translatesAutoresizingMaskIntoConstraints = false
        removeConstraints(constraints)
        superview?.removeConstraints(superview?.constraints.filter {
            $0.firstItem as? UIView == self || $0.secondItem as? UIView == self
        } ?? [])
    }
    /// 快速布局(边缘贴合父视图)
    func pinToSuperview(insets: UIEdgeInsets = .zero) {
        guard let superview = superview else { return }
        translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: insets.left),
            trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: -insets.right),
            topAnchor.constraint(equalTo: superview.topAnchor, constant: insets.top),
            bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: -insets.bottom)
        ])
    }
}
import Combine
extension AnyPublisher {
    /// 简化订阅(适配AppError)
    func sinkToResult(
        _ cancellables: inout Set,
        onSuccess: @escaping (Output) -> Void,
        onFailure: @escaping (AppError) -> Void
    ) where Failure == AppError {
        self.sink(
            receiveCompletion: { completion in
                if case .failure(let error) = completion {
                    onFailure(error)
                }
            },
            receiveValue: { value in
                onSuccess(value)
            }
        )
        .store(in: &cancellables)
    }
    /// 简化订阅(适配NetworkError,转换为AppError)
    func sinkToResult(
        _ cancellables: inout Set,
        onSuccess: @escaping (Output) -> Void,
        onFailure: @escaping (AppError) -> Void
    ) where Failure == NetworkError {
        self.mapError { AppError.network($0) }
            .sinkToResult(&cancellables, onSuccess: onSuccess, onFailure: onFailure)
    }
}
import Foundation
/// 全局常量
enum AppConstants {
    /// 网络相关
    enum Network {
        static let timeout: TimeInterval = 20
        static let baseURLDev = "https://dev-api.xxx.com/v1"
        static let baseURLTest = "https://test-api.xxx.com/v1"
        static let baseURLProd = "https://api.xxx.com/v1"
    }
    /// 存储相关
    enum Storage {
        static let userTokenKey = "kUserToken"
        static let userInfoKey = "kUserInfo"
        static let themeKey = "kAppTheme"
    }
    /// UI相关
    enum UI {
        static let screenWidth = UIScreen.main.bounds.width
        static let screenHeight = UIScreen.main.bounds.height
        static let navBarHeight: CGFloat = {
            let window = UIApplication.shared.windows.first
            let statusBarHeight = window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 20
            return statusBarHeight + 44
        }()
    }
}
import UIKit
final class ToastManager {
    public static let shared = ToastManager()
    private let toastView = UILabel()
    private var toastTimer: Timer?
    private init() {
        setupToastView()
    }
    private func setupToastView() {
        toastView.backgroundColor = UIColor.black.withAlphaComponent(0.7)
        toastView.textColor = .white
        toastView.font = UIFont.systemFont(ofSize: 14)
        toastView.textAlignment = .center
        toastView.layer.cornerRadius = 8
        toastView.clipsToBounds = true
        toastView.numberOfLines = 0
        toastView.isHidden = true
    }
    public func show(text: String, duration: TimeInterval = 2) {
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            // 停止之前的定时器
            self.toastTimer?.invalidate()
            // 更新文本和尺寸
            self.toastView.text = text
            self.toastView.sizeToFit()
            let width = min(self.toastView.bounds.width + 32, AppConstants.UI.screenWidth - 64)
            let height = self.toastView.bounds.height + 16
            self.toastView.frame = CGRect(
                x: (AppConstants.UI.screenWidth - width) / 2,
                y: AppConstants.UI.screenHeight - 100,
                width: width,
                height: height
            )
            // 添加到窗口
            if let window = UIApplication.shared.windows.first {
                window.addSubview(self.toastView)
            }
            // 显示并定时隐藏
            self.toastView.isHidden = false
            self.toastTimer = Timer.scheduledTimer(withTimeInterval: duration, repeats: false) { _ in
                UIView.animate(withDuration: 0.3) {
                    self.toastView.alpha = 0
                } completion: { _ in
                    self.toastView.isHidden = true
                    self.toastView.alpha = 1
                    self.toastView.removeFromSuperview()
                }
            }
        }
    }
}

四、业务层规范:以用户模块为例的MVVM落地

理论需要实践来验证。我们以“用户模块”为例,展示如何将上述框架规范应用到具体业务中。其他业务模块均可完全复用此模式,确保项目代码风格的高度统一。

一个典型的业务模块内部结构如下:

Modules/
└── User/
    ├── View/
    │   ├── LoginViewController.swift   # 登录页面
    │   ├── PersonalViewController.swift # 个人中心页面
    │   └── UI/                         # 模块内UI组件(如LoginInputView)
    ├── ViewModel/
    │   ├── LoginViewModel.swift        # 登录ViewModel
    │   └── PersonalViewModel.swift     # 个人中心ViewModel
    ├── Repository/
    │   └── UserRepository.swift        # 用户数据仓库
    ├── Model/
    │   ├── UserModel.swift             # 用户模型
    │   └── LoginModel.swift            # 登录模型
    └── Router/
        └── UserRouter.swift            # 用户模块路由

数据层:UserRepository
Repository负责所有与用户相关的数据获取逻辑,包括登录、注册、获取用户信息等。它屏蔽了底层网络库或数据库的具体实现细节。

import Foundation
import Combine
/// 用户数据仓库(聚合网络/存储数据)
final class UserRepository: BaseRepository {
    /// 登录(网络请求)
    func login(account: String, password: String) -> AnyPublisher {
        let api = UserAPI.login(account: account, password: password)
        let config = RESTfulRequestConfig(retryCount: 1)
        return networkService.request(api, config: config)
            .mapError { self.mapNetworkError($0) }
            .eraseToAnyPublisher()
    }
    /// 获取用户信息(先读缓存,再请求网络更新)
    func fetchUserInfo() -> AnyPublisher {
        // 1. 先读取本地缓存
        if let cachedUser = storageService.get(UserModel.self, forKey: AppConstants.Storage.userInfoKey) {
            // 2. 缓存存在则先返回缓存,再请求网络更新
            return networkService.request(UserAPI.fetchUser(id: cachedUser.id), config: .init())
                .handleEvents(receiveOutput: { [weak self] user in
                    // 3. 网络请求成功后更新缓存
                    try? self?.storageService.set(user, forKey: AppConstants.Storage.userInfoKey)
                })
                .mapError { self.mapNetworkError($0) }
                .prepend(cachedUser) // 先发送缓存数据
                .eraseToAnyPublisher()
        } else {
            // 无缓存则直接请求网络
            return Fail(error: AppError.custom("请先登录"))
                .eraseToAnyPublisher()
        }
    }
    /// 保存用户Token(Keychain)
    func saveToken(_ token: String) -> AnyPublisher {
        return Future { promise in
            do {
                try self.storageService.setToKeychain(token, forKey: AppConstants.Storage.userTokenKey)
                // 更新全局网络请求头
                NetworkConfig.shared.updateGlobalHeaders(["Token": token])
                NetworkManager.shared.reloadProvider()
                promise(.success(true))
            } catch {
                promise(.failure(self.mapStorageError(error.localizedDescription)))
            }
        }.eraseToAnyPublisher()
    }
    /// 退出登录(清除缓存+Token)
    func logout() -> AnyPublisher {
        return networkService.requestWithoutData(UserAPI.logout)
            .handleEvents(receiveOutput: { [weak self] _ in
                // 清除本地缓存
                self?.storageService.remove(forKey: AppConstants.Storage.userTokenKey)
                self?.storageService.remove(forKey: AppConstants.Storage.userInfoKey)
                // 清空全局Token
                NetworkConfig.shared.clearGlobalHeaders()
                NetworkManager.shared.reloadProvider()
            })
            .mapError { self.mapNetworkError($0) }
            .eraseToAnyPublisher()
    }
}

逻辑层:LoginViewModel
ViewModel专注于业务逻辑,如参数校验、调用Repository、数据格式转换等。它通过Combine的@Published等发布者将状态传递给View,自身不包含任何UI代码。

import Foundation
import Combine
/// 登录ViewModel(纯业务逻辑,无UI依赖)
final class LoginViewModel: BaseViewModel {
    // MARK: - 发布属性(View层绑定)
    /// 账号输入
    @Published var account: String = ""
    /// 密码输入
    @Published var password: String = ""
    /// 登录按钮是否可用
    @Published var loginButtonEnabled: Bool = false
    /// 登录成功后的用户Token
    @Published var loginToken: String?
    // MARK: - 依赖注入(便于单元测试)
    private let userRepository: UserRepository
    // MARK: - 初始化
    init(userRepository: UserRepository = UserRepository()) {
        self.userRepository = userRepository
        super.init()
        setupValidations() // 初始化参数校验
    }
    // MARK: - 核心业务逻辑
    /// 登录操作(View层调用)
    func login() {
        // 1. 参数校验
        guard !account.isEmpty, !password.isEmpty else {
            handleError(AppError.custom("账号或密码不能为空"))
            return
        }
        // 2. 标记加载状态
        startLoading()
        // 3. 调用Repository获取数据
        userRepository.login(account: account, password: password)
            .flatMap { [weak self] loginModel -> AnyPublisher in
                // 4. 登录成功后保存Token
                guard let self = self else { return Fail(error: AppError.custom("ViewModel已释放")).eraseToAnyPublisher() }
                return self.userRepository.saveToken(loginModel.token)
                    .map { _ in loginModel.token }
                    .mapError { $0 }
                    .eraseToAnyPublisher()
            }
            .sinkToResult(&cancellables) { [weak self] token in
                // 5. 登录成功
                self?.stopLoading()
                self?.loginToken = token
            } onFailure: { [weak self] error in
                // 6. 处理错误
                self?.handleError(error)
            }
    }
    // MARK: - 私有方法
    /// 初始化参数校验(账号/密码非空时按钮可用)
    private func setupValidations() {
        Publishers.CombineLatest($account, $password)
            .map { account, password in
                !account.isEmpty && !password.isEmpty && password.count >= 6
            }
            .assign(to: &$loginButtonEnabled)
    }
}

展示层:LoginViewController
View层极其“轻薄”,仅负责UI元素的布局、用户交互的响应,并通过订阅ViewModel发布的属性来更新界面。这种单向数据流使得UI状态变得可预测且易于调试。

import UIKit
import Combine
/// 登录页面(纯UI,无业务逻辑)
final class LoginViewController: BaseViewController {
    // MARK: - UI组件
    private let accountTF = UITextField()
    private let pwdTF = UITextField()
    private let loginBtn = UIButton(type: .system)
    // MARK: - 初始化ViewModel(必须实现)
    override func setupViewModel() {
        viewModel = LoginViewModel()
    }
    // MARK: - 数据绑定(核心)
    override func setupBindings() {
        super.setupBindings()
        // 1. View → ViewModel(用户输入传递给ViewModel)
        accountTF.textPublisher
            .assign(to: &viewModel.$account)
        pwdTF.textPublisher
            .assign(to: &viewModel.$password)
        // 2. ViewModel → View(按钮状态绑定)
        viewModel.$loginButtonEnabled
            .sink { [weak self] enabled in
                self?.loginBtn.isEnabled = enabled
                self?.loginBtn.backgroundColor = enabled ? .systemBlue : .lightGray
            }
            .store(in: &viewCancellables)
        // 3. ViewModel → View(登录成功跳转)
        viewModel.$loginToken
            .compactMap { $0 }
            .sink { [weak self] _ in
                ToastManager.shared.show(text: "登录成功")
                // 通过路由跳转首页
                UserRouter.shared.gotoHome()
            }
            .store(in: &viewCancellables)
    }
    // MARK: - 导航栏配置
    override func setupNavigation() {
        title = "登录"
        navigationItem.hidesBackButton = true
    }
    // MARK: - UI布局
    override func setupBaseUI() {
        super.setupBaseUI()
        setupSubviews()
        setupConstraints()
        setupUIAppearance()
    }
    private func setupSubviews() {
        accountTF.placeholder = "请输入账号"
        pwdTF.placeholder = "请输入密码"
        pwdTF.isSecureTextEntry = true
        loginBtn.setTitle("登录", for: .normal)
        loginBtn.addTarget(self, action: #selector(loginBtnClick), for: .touchUpInside)
        view.addSubviews(accountTF, pwdTF, loginBtn)
    }
    private func setupConstraints() {
        let margin: CGFloat = 32
        let itemHeight: CGFloat = 50
        accountTF.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            accountTF.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 64),
            accountTF.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin),
            accountTF.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin),
            accountTF.heightAnchor.constraint(equalToConstant: itemHeight)
        ])
        pwdTF.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            pwdTF.topAnchor.constraint(equalTo: accountTF.bottomAnchor, constant: 16),
            pwdTF.leadingAnchor.constraint(equalTo: accountTF.leadingAnchor),
            pwdTF.trailingAnchor.constraint(equalTo: accountTF.trailingAnchor),
            pwdTF.heightAnchor.constraint(equalToConstant: itemHeight)
        ])
        loginBtn.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            loginBtn.topAnchor.constraint(equalTo: pwdTF.bottomAnchor, constant: 32),
            loginBtn.leadingAnchor.constraint(equalTo: accountTF.leadingAnchor),
            loginBtn.trailingAnchor.constraint(equalTo: accountTF.trailingAnchor),
            loginBtn.heightAnchor.constraint(equalToConstant: itemHeight)
        ])
    }
    private func setupUIAppearance() {
        accountTF.borderStyle = .roundedRect
        pwdTF.borderStyle = .roundedRect
        loginBtn.layer.cornerRadius = 8
        loginBtn.setTitleColor(.white, for: .normal)
        loginBtn.isEnabled = false
    }
    // MARK: - 事件处理
    @objc private func loginBtnClick() {
        // 调用ViewModel的登录方法,无任何业务逻辑
        viewModel.login()
    }
}
// MARK: - UITextField扩展(获取文本变化)
extension UITextField {
    var textPublisher: AnyPublisher {
        NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: self)
            .compactMap { $0.object as? UITextField }
            .map { $0.text ?? "" }
            .eraseToAnyPublisher()
    }
}

协调层:UserRouter
Router负责模块内外的页面跳转,进一步解耦页面间的依赖关系。

import UIKit
/// 用户模块路由(统一管理页面跳转)
final class UserRouter {
    public static let shared = UserRouter()
    private init() {}
    /// 跳转到登录页
    func gotoLogin() {
        let loginVC = LoginViewController()
        let nav = UINavigationController(rootViewController: loginVC)
        if let window = UIApplication.shared.windows.first {
            window.rootViewController = nav
        }
    }
    /// 跳转到个人中心
    func gotoPersonal() {
        let personalVC = PersonalViewController()
        if let nav = UIApplication.shared.windows.first?.rootViewController as? UINavigationController {
            nav.pushViewController(personalVC, animated: true)
        }
    }
    /// 跳转到首页(跨模块)
    func gotoHome() {
        // 此处可通过协议/通知/第三方路由库(如URLNavigator)实现跨模块跳转
        let homeVC = HomeViewController()
        let nav = UINavigationController(rootViewController: homeVC)
        if let window = UIApplication.shared.windows.first {
            window.rootViewController = nav
        }
    }
}

五、工程化与团队协作:保障项目长期健康

一套好的框架不仅需要优秀的技术设计,还需要配套的工程配置和团队规范来支撑其长期运行。这类似于一个成功的AI项目,不仅需要先进的算法,还需要完善的数据流水线、实验管理和团队协作流程。

工程配置要点:

  • 配置多环境(Debug/Test/Release),管理不同的API地址和功能开关。
  • 使用Swift Compiler Flags(如-D DEBUG)区分调试与生产逻辑。
  • 利用Asset Catalog和UIColor(named:)/UIImage(named:)安全地管理资源。
  • 使用Swift Package Manager管理依赖,核心库参考如下:
# Podfile示例
platform :ios, '15.0'
use_frameworks!
target 'YourApp' do
    # 网络
    pod 'Moya', '~> 15.0'
    pod 'Moya/Combine'
    # 存储
    pod 'KeychainSwift', '~> 2.0'
    # 图片加载
    pod 'Kingfisher', '~> 7.0'
    # 布局
    pod 'SnapKit', '~> 5.0' # 可选,替代纯AutoLayout
    # 测试
    pod 'Quick', '~> 6.0'
    pod 'Nimble', '~> 12.0'
end

团队协作规范:

  • 代码规范:统一命名(如LoginViewController, UserModel)、注释和代码风格(4空格缩进,使用[weak self])。
  • Git分支策略:采用Git Flow变体,包含main(生产)、develop(开发)、feature/xxx(功能分支)等。
  • 单元测试:重点测试ViewModel和Repository,追求核心业务逻辑覆盖率≥80%。

六、框架优势与落地实践建议

综上所述,这套框架的核心优势在于其彻底的解耦、响应式数据绑定、模块化设计和强大的可扩展性。它通过协议抽象核心能力,使得替换网络库或存储方案等底层服务时,业务层代码几乎不受影响。

⚠️ 给团队的落地建议:

  1. 小步快跑:先在一个独立且完整的业务模块(如登录流程)中实践,验证框架的可行性。
  2. 基建先行:项目初期投入资源夯实Core层,沉淀通用组件。
  3. 测试驱动:为核心业务逻辑编写单元测试,保障代码质量与重构安全。
  4. 定期重构:随着迭代,及时重构臃肿的ViewModel,保持代码简洁。
  5. 避免过度设计:对于中小型项目,可酌情简化如Router层等设计,优先保障开发效率。

[AFFILIATE_SLOT_2]

总结
本框架的精髓在于“标准化、解耦与响应式”。它提供了一套从工程结构到代码规范的完整解决方案,使团队能够有章可循地开发。通过Repository抽象数据层,并结合Combine实现单向数据流,它成功地将View层解放为纯粹的状态订阅者。这套设计既能为中小型应用提供快速开发的脚手架,其模块化特性也能从容应对中大型应用的复杂性与扩展性需求,真正让开发者回归业务价值创造本身。

@PublishedPassthroughSubjectBaseViewModel