SwiftUI ios开发中的 MVVM 架构深度解析与最佳实践

发布于:2025-08-06 ⋅ 阅读:(15) ⋅ 点赞:(0)

一、Swift MVVM 核心架构

数据绑定
请求/响应
数据更新
状态更新
API/数据库
导航
View
ViewModel
Model
Service
Router

二、核心组件实现

1. Model 层设计

struct User: Codable {
    let id: Int
    var name: String
    var email: String
    
    // 业务逻辑封装
    func isValidEmail() -> Bool {
        let regex = #"^\S+@\S+\.\S+$"#
        return email.range(of: regex, options: .regularExpression) != nil
    }
}

2. ViewModel 层实现

final class UserViewModel: ObservableObject {
    // MARK: - 发布属性
    @Published var user: User
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    // MARK: - 依赖注入
    private let apiService: APIServiceProtocol
    private let storage: UserStorageProtocol
    
    init(user: User, 
         apiService: APIServiceProtocol = APIService(),
         storage: UserStorageProtocol = UserStorage()) {
        self.user = user
        self.apiService = apiService
        self.storage = storage
    }
    
    // MARK: - 业务逻辑
    func saveUser() {
        guard user.isValidEmail() else {
            errorMessage = "Invalid email format"
            return
        }
        
        isLoading = true
        apiService.updateUser(user) { [weak self] result in
            DispatchQueue.main.async {
                self?.isLoading = false
                switch result {
                case .success(let updatedUser):
                    self?.user = updatedUser
                    self?.storage.saveUser(updatedUser)
                case .failure(let error):
                    self?.errorMessage = error.localizedDescription
                }
            }
        }
    }
}

3. View 层实现 (SwiftUI)

struct UserView: View {
    @StateObject var viewModel: UserViewModel
    @EnvironmentObject var router: Router
    
    var body: some View {
        Form {
            Section(header: Text("Personal Info")) {
                TextField("Name", text: $viewModel.user.name)
                TextField("Email", text: $viewModel.user.email)
                    .keyboardType(.emailAddress)
                    .autocapitalization(.none)
            }
            
            Button("Save") {
                viewModel.saveUser()
            }
            .disabled(viewModel.isLoading)
            
            if let error = viewModel.errorMessage {
                Text(error)
                    .foregroundColor(.red)
            }
        }
        .navigationTitle("Profile")
        .overlay {
            if viewModel.isLoading {
                ProgressView()
            }
        }
    }
}

三、数据绑定机制

1. SwiftUI 绑定方式

// 双向绑定
TextField("Name", text: $viewModel.user.name)

// 状态监听
.onReceive(viewModel.$errorMessage) { error in
    if error != nil {
        showAlert = true
    }
}

// 操作绑定
Button("Save", action: viewModel.saveUser)

2. Combine 高级绑定

class UserViewModel: ObservableObject {
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $user
            .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
            .filter { $0.name.count > 3 }
            .sink { [weak self] _ in
                self?.autoSave()
            }
            .store(in: &cancellables)
    }
}

四、依赖注入方案

1. 构造函数注入

struct UserView_Previews: PreviewProvider {
    static var previews: some View {
        let mockUser = User(id: 1, name: "Test", email: "test@example.com")
        let mockService = MockAPIService()
        let viewModel = UserViewModel(user: mockUser, apiService: mockService)
        
        UserView(viewModel: viewModel)
    }
}

2. 依赖容器

class DIContainer {
    static let shared = DIContainer()
    
    lazy var apiService: APIServiceProtocol = {
        return ProcessInfo.processInfo.isUITesting ? 
            MockAPIService() : APIService()
    }()
    
    lazy var storage: UserStorageProtocol = {
        return UserDefaultsStorage()
    }()
}

// 使用
let viewModel = UserViewModel(
    user: user,
    apiService: DIContainer.shared.apiService,
    storage: DIContainer.shared.storage
)

五、路由导航实现

1. 路由协议

enum Route: Hashable {
    case userDetail(User)
    case settings
    case logout
}

class Router: ObservableObject {
    @Published var path = NavigationPath()
    
    func navigate(to route: Route) {
        path.append(route)
    }
    
    func popToRoot() {
        path.removeLast(path.count)
    }
}

2. ViewModel 中的导航

class UserListViewModel: ObservableObject {
    @Published var users: [User] = []
    private let router: Router
    
    init(router: Router) {
        self.router = router
    }
    
    func selectUser(_ user: User) {
        router.navigate(to: .userDetail(user))
    }
}

六、测试策略

1. ViewModel 单元测试

class UserViewModelTests: XCTestCase {
    var sut: UserViewModel!
    var mockService: MockAPIService!
    
    override func setUp() {
        mockService = MockAPIService()
        let user = User(id: 1, name: "John", email: "test@test.com")
        sut = UserViewModel(user: user, apiService: mockService)
    }
    
    func testValidEmailSavesSuccessfully() {
        // 设置
        mockService.updateUserResult = .success(User(id: 1, name: "John", email: "valid@email.com"))
        
        // 执行
        sut.user.email = "valid@email.com"
        sut.saveUser()
        
        // 验证
        XCTAssertFalse(sut.isLoading)
        XCTAssertNil(sut.errorMessage)
        XCTAssertEqual(sut.user.email, "valid@email.com")
    }
    
    func testInvalidEmailShowsError() {
        // 执行
        sut.user.email = "invalid-email"
        sut.saveUser()
        
        // 验证
        XCTAssertEqual(sut.errorMessage, "Invalid email format")
    }
}

2. UI 快照测试

func testUserViewLoadingState() {
    let viewModel = UserViewModel(user: .mock)
    viewModel.isLoading = true
    
    let view = UserView(viewModel: viewModel)
    assertSnapshot(of: view, as: .image(layout: .device(config: .iPhone13)))
}

七、性能优化技巧

1. 高效数据绑定

// 使用@Published优化
class UserViewModel: ObservableObject {
    // 仅发布必要属性
    @Published private(set) var user: User
    @Published private(set) var isLoading = false
    
    // 内部使用非发布属性
    private var temporaryData: String = ""
}

2. 资源管理

// 使用Task管理异步
func loadData() {
    Task { @MainActor in
        isLoading = true
        do {
            user = try await apiService.fetchUser()
        } catch {
            errorMessage = error.localizedDescription
        }
        isLoading = false
    }
}

八、常见问题解决方案

1. 循环引用处理

func fetchData() {
    apiService.getData { [weak self] result in
        guard let self = self else { return }
        // 使用self安全
    }
}

// 或者使用捕获列表
func fetchData() {
    apiService.getData { [unowned self] result in
        // 无主引用
    }
}

2. 复杂状态管理

enum UserState {
    case loading
    case loaded(User)
    case error(Error)
    case editing
}

class UserViewModel: ObservableObject {
    @Published var state: UserState = .loading
    
    func loadUser() {
        state = .loading
        apiService.fetchUser { result in
            switch result {
            case .success(let user):
                state = .loaded(user)
            case .failure(let error):
                state = .error(error)
            }
        }
    }
}

九、SwiftUI 最佳实践

1. 视图优化

struct UserView: View {
    @ObservedObject var viewModel: UserViewModel
    
    var body: some View {
        VStack {
            // 条件渲染
            if case .loaded(let user) = viewModel.state {
                UserProfileView(user: user)
            }
            
            // 解耦子视图
            SaveButton(viewModel: viewModel)
        }
    }
}

// 独立按钮组件
struct SaveButton: View {
    @ObservedObject var viewModel: UserViewModel
    
    var body: some View {
        Button("Save") {
            viewModel.saveUser()
        }
        .disabled(viewModel.isSaving)
    }
}

2. 预览优化

struct UserView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            // 正常状态
            UserView(viewModel: .previewLoaded)
            
            // 加载状态
            UserView(viewModel: .previewLoading)
            
            // 错误状态
            UserView(viewModel: .previewError)
        }
    }
}

extension UserViewModel {
    static var previewLoaded: UserViewModel {
        let vm = UserViewModel(user: .mock)
        vm.state = .loaded(.mock)
        return vm
    }
}

十、架构演进建议

1. 中型项目增强

View
ViewModel
UseCase
Repository
API
Database

2. 大型项目方案

// 模块化架构
App/
├── Feature/
│   ├── User/
│   │   ├── Presentation/  # View+ViewModel
│   │   ├── Domain/        # Model+UseCase
│   │   └── Data/          # Repository+DTO
│   └── Settings/
├── Core/
│   ├── Networking/
│   ├── Storage/
│   └── DI/
└── Shared/
    ├── Utilities/
    └── DesignSystem/

十一、性能监控工具

1. Instruments 关键指标

指标 正常范围 警告阈值 优化方案
内存占用 <50MB >100MB 检查强引用循环
CPU使用率 <30% >70% 优化数据绑定
刷新频率 60fps <50fps 简化复杂视图
启动时间 <1s >2s 延迟加载VM

2. 自定义监控

class PerformanceMonitor {
    static func track<T>(_ operation: String, _ block: () throws -> T) rethrows -> T {
        let start = CFAbsoluteTimeGetCurrent()
        defer {
            let diff = CFAbsoluteTimeGetCurrent() - start
            print("⏱ $operation) took $diff)s")
            if diff > 0.1 {
                // 报告性能问题
            }
        }
        return try block()
    }
}

// 使用
func loadData() {
    PerformanceMonitor.track("UserFetch") {
        viewModel.loadUser()
    }
}

十二、Swift 生态工具链

1. 推荐库

库名 用途 集成方式
Combine 响应式编程 原生支持
Factory 依赖注入 SPM
Quick/Nimble 单元测试 SPM
ViewInspector SwiftUI 测试 SPM

2. 模板生成

# 安装
brew install mint
mint install swiftmvvm/template

# 生成模块
swiftmvvm generate UserFeature

总结:Swift MVVM 最佳实践

  1. 严格分层:
    • View:只处理UI展示
    • ViewModel:处理视图逻辑
    • Model:封装业务逻辑
  2. 单向数据流:
用户操作
View
ViewModel
Model
  1. 测试策略:
    • ViewModel:100%单元测试
    • View:快照测试
    • Model:业务逻辑测试
  2. 性能优化:
    • 使用@Published精确控制
    • 避免在View中处理业务逻辑
    • 使用Task管理异步
  3. 架构演进:
    • 小型项目:基础MVVM
    • 中型项目:MVVM+UseCase
    • 大型项目:模块化MVVM

黄金法则:保持View的"笨拙",让ViewModel成为唯一的真相源,让Model保持纯净的业务逻辑封装。


网站公告

今日签到

点亮在社区的每一天
去签到