SwiftUI模式开发者Skill swiftui-patterns-developer

这个技能专注于SwiftUI视图的结构和模式开发,帮助开发者应用一致的组织和最佳实践,包括视图排序、子视图提取、MVVM模式、ViewModels使用、依赖注入等,适用于重构和组织iOS/macOS应用中的SwiftUI代码。关键词:SwiftUI、视图结构、重构、子视图、MVVM、ViewModels、依赖注入、SwiftUI最佳实践、移动开发。

移动开发 0 次安装 0 次浏览 更新于 3/15/2026

名称: swiftui-patterns-developer 描述: SwiftUI视图结构、组合和最佳实践。用于重构SwiftUI视图、组织视图文件或提取子视图时。

SwiftUI模式开发者(智能路由器)

目的

应用一致的结构和模式到SwiftUI视图,专注于排序、子视图提取和正确组合。

自动激活时机

  • 重构SwiftUI视图结构
  • 组织视图文件布局
  • 将大型视图拆分为子视图
  • 关键词:视图结构、视图排序、拆分视图、提取子视图、大型视图、重构视图

核心指南

0) SwiftUI视图的三种特性(WWDC24)

理解这些基础知识有助于编写更好的SwiftUI代码:

1. 声明式 - 描述您想要什么,而不是如何创建它:

// ✅ 声明式 - 描述结果
List(pets) { pet in
    HStack {
        Text(pet.name)
        Spacer()
        Text(pet.species)
    }
}
// 无需手动添加/删除行 - SwiftUI处理它

2. 组合式 - 从简单的构建块构建复杂UI:

// ViewBuilder闭包定义容器的子视图
HStack {           // 容器视图
    Image(...)     // 子视图1
    VStack {       // 子视图2(也是容器)
        Text(...)  // 嵌套子视图
        Text(...)  // 嵌套子视图
    }
    Spacer()       // 子视图3
}

3. 状态驱动 - 状态变化时UI自动更新:

// SwiftUI跟踪依赖并自动更新视图
@State private var count = 0

var body: some View {
    Button("计数:\(count)") {  // 依赖 `count`
        count += 1                // 状态变化触发重新渲染
    }
}

关键洞察:视图是值类型(结构体),而非长生命周期对象。它们描述当前UI状态,而非随时间接收命令的对象。SwiftUI在后台维护实际UI。

1) 视图排序(从上到下)

遵循Anytype的属性组织来自IOS_DEVELOPMENT_GUIDE.md:

struct ExampleView: View {
    // 1. 属性包装器(@State、@Injected、@Environment)
    @State private var model: ExampleViewModel
    @Injected(\.settingsService) private var settingsService
    @Environment(\.dismiss) private var dismiss

    // 2. 公共属性(let/var)
    let title: String

    // 3. 私有属性
    private var cancellables = Set<AnyCancellable>()

    // 4. 计算属性
    private var hasItems: Bool { !model.items.isEmpty }

    // 5. 初始化器(如果需要)
    init(title: String) {
        self.title = title
        _model = State(wrappedValue: ExampleViewModel(title: title))
    }

    // 6. body
    var body: some View {
        content
            .task { await model.startSubscriptions() }
    }

    // 7. 计算视图构建器
    private var content: some View { ... }

    // 8. 辅助/异步函数
    private func handleTap() { ... }
}

2) ViewModel模式(Anytype标准)

Anytype使用MVVM与ViewModels。始终使用ViewModels处理业务逻辑:

// 视图 - 轻量级,仅UI
struct ChatView: View {
    @State private var model: ChatViewModel

    init(spaceId: String, chatId: String) {
        _model = State(wrappedValue: ChatViewModel(spaceId: spaceId, chatId: chatId))
    }

    var body: some View {
        content
            .task { await model.startSubscriptions() }
    }

    private var content: some View {
        List(model.messages) { message in
            MessageRow(message: message)
        }
    }
}

// ViewModel - 处理业务逻辑
@MainActor
@Observable
final class ChatViewModel {
    var messages: [Message] = []

    @ObservationIgnored
    @Injected(\.chatService) private var chatService

    func startSubscriptions() async {
        // 繁重工作在此处,不在init中
    }

    func sendMessage(_ text: String) async {
        // 业务逻辑
    }
}

关键点

  • 在视图中使用 @State private var model: ViewModel
  • 在视图的 init 中初始化ViewModel,使用 _model = State(wrappedValue:)
  • 保持ViewModel初始化廉价,繁重工作在 .task
  • 使用 @Observable 宏(而非 ObservableObject
  • @MainActor 标记ViewModels

3) 观察如何工作(WWDC23)

理解为何 @Observable 工作有助于正确使用它。

属性访问跟踪

  • SwiftUI跟踪您在 body 评估期间访问了哪些属性
  • 仅这些被访问的属性在更改时触发视图失效
  • 未在 body 中读取的属性不会导致重新渲染(与 @Published 不同)
@Observable
final class SettingsViewModel {
    var userName: String = ""      // 在body中访问 → 触发更新
    var isLoading: Bool = false    // 在body中访问 → 触发更新
    var analyticsData: Data = Data()  // 未在body中访问 → 无更新
}

struct SettingsView: View {
    @State private var model: SettingsViewModel

    var body: some View {
        // SwiftUI跟踪:"此视图读取userName和isLoading"
        VStack {
            Text(model.userName)           // ✓ 被跟踪
            if model.isLoading {           // ✓ 被跟踪
                ProgressView()
            }
            // model.analyticsData未读取 → 更改不会使此视图失效
        }
    }
}

每个实例跟踪

  • 数组中的 @Observable 对象高效工作
  • 仅更改的具体实例触发更新
  • 无需用于观察的 identifiable 技巧
@Observable
final class MessageViewModel {
    var text: String
    var isRead: Bool = false
}

// 每个MessageRow仅在其消息更改时更新
List(model.messages) { message in
    MessageRow(message: message)  // 仅当message.isRead更改时此行更新
}

计算属性正常工作

  • 从存储属性组合的计算属性自动被跟踪
  • SwiftUI追踪到基础存储属性
@Observable
final class CartViewModel {
    var items: [Item] = []
    var discount: Double = 0

    // 计算 → 跟踪 `items` 和 `discount`
    var totalPrice: Double {
        items.reduce(0) { $0 + $1.price } - discount
    }
}

性能优势: 使用 @Observable,视图仅在实际读取的属性更改时更新。这比 ObservableObject 更高效,其中任何 @Published 更改都触发所有订阅者的 objectWillChange

4) 属性包装器决策树

何时使用哪个包装器与 @Observable

场景 包装器 原因
视图拥有模型生命周期 @State 视图创建并管理模型
模型在应用范围内共享 @Environment 在应用根注入,随处读取
仅需要绑定($语法) @Bindable 传递给TextField、Toggle等
读取模型 直接属性访问触发跟踪
// 视图拥有模型(创建它)
struct ChatView: View {
    @State private var model: ChatViewModel  // ← @State

    init(chatId: String) {
        _model = State(wrappedValue: ChatViewModel(chatId: chatId))
    }
}

// 从父级传递模型,需要绑定
struct MessageEditor: View {
    @Bindable var draft: DraftMessage  // ← @Bindable 用于 $draft.text

    var body: some View {
        TextField("消息", text: $draft.text)
    }
}

// 仅读取,无需绑定
struct MessageRow: View {
    let message: MessageViewModel  // ← 无!仅读取属性

    var body: some View {
        Text(message.text)
        Image(systemName: message.isRead ? "checkmark.circle.fill" : "circle")
    }
}

从ObservableObject迁移

@StateObject @State
@ObservedObject @Bindable 或无
@EnvironmentObject @Environment

5) 从ObservableObject迁移(WWDC23)

从旧版 ObservableObject 逐步转换:

之前(ObservableObject):

class SettingsViewModel: ObservableObject {
    @Published var userName: String = ""
    @Published var notifications: Bool = true

    private var cancellables = Set<AnyCancellable>()
}

struct SettingsView: View {
    @StateObject private var model = SettingsViewModel()

    var body: some View {
        TextField("名称", text: $model.userName)
        Toggle("通知", isOn: $model.notifications)
    }
}

之后(@Observable):

@Observable
final class SettingsViewModel {
    var userName: String = ""
    var notifications: Bool = true

    @ObservationIgnored
    private var cancellables = Set<AnyCancellable>()
}

struct SettingsView: View {
    @State private var model = SettingsViewModel()

    var body: some View {
        @Bindable var model = model  // 本地绑定用于 $ 语法
        TextField("名称", text: $model.userName)
        Toggle("通知", isOn: $model.notifications)
    }
}

迁移步骤

  1. 移除 ObservableObject 一致性,添加 @Observable
  2. 从所有属性移除 @Published(观察自动)
  3. 对不应触发更新的属性添加 @ObservationIgnored
  4. 在视图中更改 @StateObject@State
  5. 对于 $ 绑定语法,在body中使用 @Bindable var model = model
  6. 替换 @EnvironmentObject@Environment

注意:Anytype已使用 @Observable - 此部分用于迁移期间理解遗留代码。

6) 依赖注入(Factory)

Anytype使用Factory DI,而非SwiftUI Environment用于服务:

// ✅ 正确 - Factory DI
@Injected(\.chatService) private var chatService

// ❌ 错误 - Environment用于服务
@Environment(ChatService.self) private var chatService

Environment用于

  • 系统值:@Environment(\.dismiss)@Environment(\.colorScheme)
  • SwiftUI提供的上下文

@Injected用于

  • 应用服务:@Injected(\.chatService)
  • 仓库:@Injected(\.userRepository)
  • 任何业务逻辑依赖

7) 视图修改器和顺序(WWDC24)

视图修改器创建分层结构。顺序重要 - 修改器按顺序应用:

// 每个修改器包装先前结果
Image("whiskers")
    .clipShape(Circle())      // 1. 先剪裁为圆形
    .shadow(radius: 4)        // 2. 为剪裁形状添加阴影
    .overlay(                 // 3. 在阴影上叠加
        Circle().stroke(.green, lineWidth: 2)
    )

效果层次和顺序由修改器的确切顺序定义。链式修改器使结果产生和自定义方式清晰。

8) 自适应视图(WWDC24)

SwiftUI视图描述目的,而非确切视觉构造。这实现自适应:

按钮 - 相同目的(标签化操作),不同上下文:

// 适应于:无边、有边、突出样式
// 适应于:滑动操作、菜单、表单
Button("编辑", action: handleEdit)

// 在滑动操作中
.swipeActions {
    Button("删除", role: .destructive) { delete() }
    Button("归档") { archive() }
}

切换 - 开关、复选框或切换按钮取决于上下文:

// 自动显示适合平台/上下文的样式
Toggle("通知", isOn: $notificationsEnabled)

可搜索 - 描述能力,SwiftUI处理惯用表示:

// iOS:覆盖列表,macOS:下拉菜单
List(filteredItems) { ... }
    .searchable(text: $searchText)
    .searchSuggestions {
        ForEach(suggestions) { Text($0) }
    }

9) 拆分大型Body

如果 body 超过屏幕,拆分为较小子视图:

// 计算视图属性(相同文件)
var body: some View {
    List {
        header
        filters
        results
    }
}

private var header: some View { ... }
private var filters: some View { ... }
private var results: some View { ... }
// 提取子视图(可重用或复杂)
struct HeaderSection: View {
    let title: String
    let subtitle: String?

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            AnytypeText(title, style: .heading)
            if let subtitle {
                AnytypeText(subtitle, style: .bodyRegular)
            }
        }
    }
}

10) ViewState枚举模式

对于具有加载/错误/加载状态的视图:

enum ViewState {
    case loading
    case error(String)
    case loaded
}

@MainActor
@Observable
final class FeedViewModel {
    var viewState: ViewState = .loading
    var posts: [Post] = []

    func loadPosts() async {
        do {
            posts = try await feedService.getFeed()
            viewState = .loaded
        } catch {
            viewState = .error(error.localizedDescription)
        }
    }
}

struct FeedView: View {
    @State private var model: FeedViewModel

    var body: some View {
        content
            .task { await model.loadPosts() }
    }

    @ViewBuilder
    private var content: some View {
        switch model.viewState {
        case .loading:
            ProgressView()
        case .error(let message):
            ErrorView(message: message, retry: { Task { await model.loadPosts() } })
        case .loaded:
            List(model.posts) { post in
                PostRow(post: post)
            }
        }
    }
}

11) State、Binding和真理源(WWDC24)

@State 创建视图内部数据源:

struct RatingView: View {
    @State private var rating = 0  // 视图拥有此状态

    var body: some View {
        HStack {
            Text("\(rating)")
            Button("+") { rating += 1 }
            Button("-") { rating -= 1 }
        }
    }
}

@Binding 创建双向引用指向他处拥有的状态:

struct RatingContainerView: View {
    @State private var rating = 0  // 单一真理源

    var body: some View {
        VStack {
            Gauge(value: Double(rating), in: 0...10) {}
            RatingEditor(rating: $rating)  // 传递绑定
        }
    }
}

struct RatingEditor: View {
    @Binding var rating: Int  // 双向引用容器状态

    var body: some View {
        Button("+") { rating += 1 }  // 更新容器状态
    }
}

关键原则:一个真理源。当多个视图需要相同数据时,将状态提升到共同祖先并向下传递绑定。

12) 状态变化动画(WWDC24)

withAnimation 包装状态变化以动画化结果视图更新:

Button("评分") {
    withAnimation {
        rating += 1  // 在动画块内状态变化
    }
}

为特定视图自定义过渡:

Text("\(rating)")
    .contentTransition(.numericText())  // 平滑数字过渡

SwiftUI动画基于相同数据驱动更新 - 当状态变化,视图更新,withAnimation 使这些更新动画化。

13) Task和onChange用法

// 初始加载
.task {
    await model.startSubscriptions()
}

// 响应状态变化
.task(id: searchText) {
    guard !searchText.isEmpty else { return }
    await model.search(query: searchText)
}

.onChange(of: selectedTab) { oldValue, newValue in
    // 处理标签更改
}

14) 大型视图文件组织

当文件超过约300行时:

struct LargeView: View {
    // 此处属性和body
}

// MARK: - 子视图
private extension LargeView {
    var header: some View { ... }
    var content: some View { ... }
}

// MARK: - 操作
private extension LargeView {
    func loadData() async { ... }
    func handleTap() { ... }
}

15) UIKit/AppKit互操作性(WWDC24)

SwiftUI提供与UIKit和AppKit的无缝互操作 - 无需期望应用完全为SwiftUI。

在SwiftUI中嵌入UIKit - 使用 UIViewRepresentable

struct MapView: UIViewRepresentable {
    func makeUIView(context: Context) -> MKMapView {
        MKMapView()
    }

    func updateUIView(_ uiView: MKMapView, context: Context) {
        // 用新数据更新视图
    }
}

// 像任何SwiftUI视图一样使用
var body: some View {
    VStack {
        MapView()
        Button("居中") { ... }
    }
}

在UIKit中嵌入SwiftUI - 使用 UIHostingController

// 在UIKit视图控制器中
let swiftUIView = ProfileView(user: user)
let hostingController = UIHostingController(rootView: swiftUIView)
addChild(hostingController)
view.addSubview(hostingController.view)

增量采用哲学:Apple自己的应用使用这些工具逐步采用SwiftUI - 无论是将SwiftUI带入现有应用还是将UIKit视图纳入新SwiftUI应用。所有方法均有效。

常见错误

使用@Environment用于服务

// ❌ 错误 - Environment用于应用服务
@Environment(FeedService.self) private var feedService

// ✅ 正确 - Factory DI
@Injected(\.feedService) private var feedService

复杂视图缺少ViewModel

// ❌ 错误 - 业务逻辑在视图中
struct FeedView: View {
    @State private var posts: [Post] = []

    private func loadPosts() async {
        // 业务逻辑直接在视图中
    }
}

// ✅ 正确 - ViewModel处理业务逻辑
struct FeedView: View {
    @State private var model: FeedViewModel
    // ViewModel处理loadPosts()
}

避免Group与条件+生命周期修改器

// ❌ 错误 - onAppear可能多次触发
var body: some View {
    Group {
        if model.isLoading {
            ProgressView()
        } else {
            content
        }
    }
    .onAppear { model.onAppear() }
}

// ✅ 正确 - 使用@ViewBuilder
var body: some View {
    loadingContent
        .onAppear { model.onAppear() }
}

@ViewBuilder
private var loadingContent: some View {
    if model.isLoading {
        ProgressView()
    } else {
        content
    }
}

相关技能

  • ios-dev-guidelines -> 完整MVVM/协调器模式、代码风格
  • swiftui-performance-developer -> 性能优化
  • design-system-developer -> 图标、排版、颜色

导航:此技能提供SwiftUI结构模式。完整架构指南见 IOS_DEVELOPMENT_GUIDE.md

归属:视图结构模式改编自Dimillian/Skills,与Anytype MVVM架构对齐。WWDC24洞察来自“SwiftUI Essentials”会话。