名称: 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)
}
}
迁移步骤:
- 移除
ObservableObject一致性,添加@Observable宏 - 从所有属性移除
@Published(观察自动) - 对不应触发更新的属性添加
@ObservationIgnored - 在视图中更改
@StateObject→@State - 对于
$绑定语法,在body中使用@Bindable var model = model - 替换
@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”会话。