跳到主要内容

iOS Clean Architecture 与架构模式

iOS 架构设计深度

1. Clean Architecture 分层

Presentation Layer
- ViewController / SwiftUI View
- ViewModel (MVVM) / Presenter (MVP)

Domain Layer <--Swift, 无任何框架依赖
- Use Case(一个 Use Case 对应一个业务操作)
- Entity(核心业务模型)
- Repository Protocol(数据访问抽象)

Data Layer
- Repository Implementation
- Data Source(Remote API + Local DB
- Data Model Mapper

依赖规则:层之间的依赖只能向内,Domain 层对外无依赖。

2. MVVM 实现

// Domain Layer
struct User {
let id: String
let name: String
let email: String
}

protocol UserRepository {
func fetchUser(id: String) async throws -> User
}

final class FetchUserUseCase {
private let repo: UserRepository
init(repo: UserRepository) { self.repo = repo }

func execute(id: String) async throws -> User {
try await repo.fetchUser(id: id)
}
}

// Presentation Layer
@MainActor
final class UserViewModel: ObservableObject {
@Published private(set) var state: ViewState = .idle

private let useCase: FetchUserUseCase
init(useCase: FetchUserUseCase) { self.useCase = useCase }

func loadUser(id: String) async {
state = .loading
do {
let user = try await useCase.execute(id: id)
state = .success(user)
} catch {
state = .error(error.localizedDescription)
}
}
}

enum ViewState {
case idle, loading
case success(User)
case error(String)
}

3. 依赖注入

// 构建器注入而非全局单例
struct AppDependencies {
let userRepository: UserRepository
let authService: AuthService

static func makeProd() -> AppDependencies {
AppDependencies(
userRepository: RemoteUserRepository(apiClient: URLSessionAPIClient()),
authService: AuthServiceImpl()
)
}

static func makeTest() -> AppDependencies {
AppDependencies(
userRepository: MockUserRepository(),
authService: MockAuthService()
)
}
}

// Factory 模式创建 ViewModel
final class UserViewModelFactory {
private let deps: AppDependencies
init(deps: AppDependencies) { self.deps = deps }

func make(userId: String) -> UserViewModel {
UserViewModel(
useCase: FetchUserUseCase(repo: deps.userRepository),
userId: userId
)
}
}

4. 导航架构设计

// Coordinator 模式解耦导航逻辑
protocol Coordinator: AnyObject {
var navigationController: UINavigationController { get }
func start()
}

final class AppCoordinator: Coordinator {
let navigationController: UINavigationController
private let deps: AppDependencies
private var childCoordinators: [Coordinator] = []

init(nav: UINavigationController, deps: AppDependencies) {
self.navigationController = nav
self.deps = deps
}

func start() {
showHome()
}

private func showHome() {
let vc = HomeViewController(
viewModel: HomeViewModel(deps: deps),
onUserTapped: { [weak self] userId in
self?.showUserDetail(userId: userId)
}
)
navigationController.setViewControllers([vc], animated: false)
}

private func showUserDetail(userId: String) {
let factory = UserViewModelFactory(deps: deps)
let vc = UserDetailViewController(viewModel: factory.make(userId: userId))
navigationController.pushViewController(vc, animated: true)
}
}

5. 错误处理设计

// 领域错误类型
enum UserError: LocalizedError {
case notFound
case unauthorized
case networkError(underlying: Error)

var errorDescription: String? {
switch self {
case .notFound: return "用户不存在"
case .unauthorized: return "没有权限"
case .networkError(let e): return "网络错误: \(e.localizedDescription)"
}
}
}

// Repository 层映射错误
extension RemoteUserRepository {
private func mapError(_ error: Error) -> UserError {
guard let apiError = error as? APIError else {
return .networkError(underlying: error)
}
switch apiError.statusCode {
case 404: return .notFound
case 401: return .unauthorized
default: return .networkError(underlying: apiError)
}
}
}

6. 单元测试

// Domain 层单独测试,无需模拟框架
final class FetchUserUseCaseTests: XCTestCase {
var sut: FetchUserUseCase!
var mockRepo: MockUserRepository!

override func setUp() {
mockRepo = MockUserRepository()
sut = FetchUserUseCase(repo: mockRepo)
}

func test_execute_whenUserExists_returnsUser() async throws {
let expected = User(id: "1", name: "Alice", email: "a@b.com")
mockRepo.stubbedUser = expected

let result = try await sut.execute(id: "1")

XCTAssertEqual(result.id, expected.id)
}

func test_execute_whenUserNotFound_throwsError() async {
mockRepo.stubbedError = UserError.notFound

await XCTAssertThrowsErrorAsync(try await sut.execute(id: "999")) { error in
XCTAssertEqual(error as? UserError, .notFound)
}
}
}

7. 资深面试题

  • MVVM 和 MVP 的核心区别?
    • MVP: Presenter 知道 View,通过接口调用;不依赖 UI 框架,库测试更简单
    • MVVM:ViewModel 不知道 View,通过数据绑定展示;更适合 SwiftUI
  • Coordinator 模式解决什么问题?
    • ViewController 之间不再互相了解,导航逻辑集中在 Coordinator
    • 导航逻辑集中易于测试和复用
  • 如何解决 ViewModel 测试中的异步问题?
    • 使用 async/await + XCTestExpectationXCTAssertThrowsErrorAsync
    • 将依赖的 Clock 抽象,测试时注入可控时间
  • Use Case 是否一定需要?
    • 简单 CRUD 可以省略,直接调用 Repository
    • 包含业务规则、多步骤或需单独测试的操作建议抽取