在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的等发布者将状态传递给View,自身不包含任何UI代码。@Published
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)、注释和代码风格(4空格缩进,使用UserModel)。[weak self] - Git分支策略:采用Git Flow变体,包含
(生产)、main(开发)、develop(功能分支)等。feature/xxx - 单元测试:重点测试ViewModel和Repository,追求核心业务逻辑覆盖率≥80%。
六、框架优势与落地实践建议
综上所述,这套框架的核心优势在于其彻底的解耦、响应式数据绑定、模块化设计和强大的可扩展性。它通过协议抽象核心能力,使得替换网络库或存储方案等底层服务时,业务层代码几乎不受影响。
⚠️ 给团队的落地建议:
- 小步快跑:先在一个独立且完整的业务模块(如登录流程)中实践,验证框架的可行性。
- 基建先行:项目初期投入资源夯实Core层,沉淀通用组件。
- 测试驱动:为核心业务逻辑编写单元测试,保障代码质量与重构安全。
- 定期重构:随着迭代,及时重构臃肿的ViewModel,保持代码简洁。
- 避免过度设计:对于中小型项目,可酌情简化如Router层等设计,优先保障开发效率。
[AFFILIATE_SLOT_2]
总结
本框架的精髓在于“标准化、解耦与响应式”。它提供了一套从工程结构到代码规范的完整解决方案,使团队能够有章可循地开发。通过Repository抽象数据层,并结合Combine实现单向数据流,它成功地将View层解放为纯粹的状态订阅者。这套设计既能为中小型应用提供快速开发的脚手架,其模块化特性也能从容应对中大型应用的复杂性与扩展性需求,真正让开发者回归业务价值创造本身。
@PublishedPassthroughSubjectBaseViewModel
浙公网安备 33010602011771号