使用 Vertical Slice Architecture 组织代码

了解如何使用垂直切片架构(Vertical Slice Architecture)来组织 SwiftUI 应用代码,提高代码复用性并更好地适应 iOS 17 新框架。

使用 Vertical Slice Architecture 组织代码

随着 SwiftUI 的普及,MVVM (Model-View-ViewModel) 毫无疑问是最常用的架构模式。然而,随着 iOS 17 引入 Observation 框架和 SwiftData,MVVM 架构模式面临新的挑战。

我们需要重新思考代码组织方式,以更好地适应这些变化,并提高代码的复用性。

什么是垂直切片架构 (VSA)?

垂直切片架构是一种按功能组织代码的方式,而非按技术层次。

每个功能模块(或"切片")包含实现该功能所需的所有组件,从用户界面到业务逻辑再到数据访问。

传统分层 vs 垂直切片

传统架构(如 MVC、MVVM)是"水平分层"的,通常按技术角色划分:

UI 层
↓
业务逻辑层
↓
数据访问层
传统水平分层架构

而 VSA 则是垂直切分的:

功能1    功能2    功能3
UI       UI       UI
业务逻辑  业务逻辑  业务逻辑
数据     数据      数据
垂直切片架构

为什么现在考虑 VSA?

iOS 17 带来的变化

  1. SwiftData 与传统 MVVM 的不一致
    • SwiftData 鼓励在 View 中直接进行 CRUD 操作
    • 例如:modelContext.insert(item)modelContext.delete(item)
    • 这与 MVVM 中"View 不应直接操作数据"的原则相悖
  2. Observation 框架简化了状态管理
    • @Observable 宏减少了对传统 ViewModel 样板代码的需求
    • 状态更新变得更加直接和简洁

代码复用的困难

在实际开发中,我需要在不同项目间复用功能。

传统 MVVM 架构下:

  • ViewModel 通常与特定应用的业务逻辑紧密耦合
  • 不同项目间移植功能时,往往需要大量修改
  • 依赖关系复杂,难以提取独立功能

VSA 在 SwiftUI 中的实现

VSA 项目文件结构

VSA 的典型文件结构如下:

Features/
  Login/                  // 登录功能切片
    LoginView.swift       // 视图
    LoginViewModel.swift  // 视图逻辑(如需要)
    LoginModels.swift     // 数据模型
    
  Bookmark/               // 书签功能切片
    BookmarkView.swift
    BookmarkModel.swift   // SwiftData 模型
    BookmarkManager.swift // 业务逻辑
    
  // 其他功能切片...

Shared/                   // 共享组件
  Styles/
  Components/
  Utils/
VSA项目结构

View-Model-Manager 模式

View-Model-Manager 的优势

由于 iOS17 带来的新特征,传统 MVVM 的一些假设被打破:

  • 使用 @Observable 后,无需 ObservableObject 和 @Published
  • SwiftData 鼓励直接在 View 中使用 modelContext。

使用 View-Model-Manager ,具有更加适合最新 SwiftUI 特征的职责划分:

  • Model:负责数据结构。和 MVVM 一样。
  • View:除了负责界面之外,还会进行简单的数据操作。使用 modelContext 直接更新 SwiftData 数据。
  • Manager:负责复杂业务逻辑和状态管理。使用 Observation 宏,并在 App 中实例化后使用 Environment 依赖注入。

因此,我认为在垂直切片内部使用 View、Model 和 Manager 的组织方式可能比传统 MVVM 更合理。

VSA + MVM

让我们看一个简单的书签功能实现,展示 VSA 的组织方式:

// Features/Bookmark/BookmarkModel.swift
import SwiftData

@Model
final class Bookmark {
    var id: UUID
    var title: String
    var url: String
    var createdAt: Date
    
    init(title: String, url: String) {
        self.id = UUID()
        self.title = title
        self.url = url
        self.createdAt = Date()
    }
}
// Features/Bookmark/BookmarkManager.swift
import Foundation
import Observation

@Observable
final class BookmarkManager {
    var isLoading = false
    var errorMessage: String?
    
    func fetchBookmarks(from url: URL) async {
        isLoading = true
        defer { isLoading = false }
        
        do {
            // 获取书签的逻辑
        } catch {
            errorMessage = "获取书签失败: \(error.localizedDescription)"
        }
    }
    
    func validateURL(_ urlString: String) -> Bool {
        // URL 验证逻辑
        guard let url = URL(string: urlString),
              UIApplication.shared.canOpenURL(url) else {
            return false
        }
        return true
    }
}
// Features/Bookmark/BookmarkListView.swift
import SwiftUI
import SwiftData

struct BookmarkListView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var bookmarks: [Bookmark]
    @State private var manager = BookmarkManager()
    
    var body: some View {
        List {
            ForEach(bookmarks) { bookmark in
                BookmarkRow(bookmark: bookmark)
            }
            .onDelete(perform: deleteBookmarks)
        }
        .overlay {
            if bookmarks.isEmpty {
                ContentUnavailableView("暂无书签", 
                                      systemImage: "bookmark.slash")
            }
        }
    }
    
    private func deleteBookmarks(at offsets: IndexSet) {
        for index in offsets {
            modelContext.delete(bookmarks[index])
        }
    }
}

这种组织方式的关键是,整个功能所需的所有组件(模型、管理器、视图)都在同一个功能文件夹内,形成一个完整的"切片"。