深入理解 SwiftUI 的声明式 UI 范式和响应式数据流,构建现代化 iOS 应用界面。
1. SwiftUI 核心理念
SwiftUI 是 Apple 在 2019 年推出的声明式 UI 框架,它彻底改变了 iOS 开发的方式。
声明式 vs 命令式
// 命令式 (UIKit)
let label = UILabel()
label.text = "Hello"
label.textColor = .blue
view.addSubview(label)
// 声明式 (SwiftUI)
Text("Hello")
.foregroundColor(.blue)
核心优势
- 代码即界面:所见即所得的开发体验
- 自动更新:状态变化自动触发 UI 刷新
- 跨平台:一套代码适配 iOS、macOS、watchOS、tvOS
- 实时预览:Xcode Preview 即时查看效果
2. 状态管理体系
SwiftUI 提供了多种属性包装器来管理不同场景的状态。
@State - 视图私有状态
用于视图内部的简单状态,值类型优先:
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 20) {
Text("计数: \(count)")
.font(.largeTitle)
HStack(spacing: 40) {
Button("减少") { count -= 1 }
Button("增加") { count += 1 }
}
.buttonStyle(.borderedProminent)
}
}
}
@Binding - 双向数据绑定
在父子视图间共享状态:
struct ParentView: View {
@State private var isOn = false
var body: some View {
VStack {
Text(isOn ? "开启" : "关闭")
ToggleView(isOn: $isOn) // 传递绑定
}
}
}
struct ToggleView: View {
@Binding var isOn: Bool // 接收绑定
var body: some View {
Toggle("开关", isOn: $isOn)
}
}
@StateObject vs @ObservedObject
class UserViewModel: ObservableObject {
@Published var name = ""
@Published var isLoading = false
func loadUser() async {
isLoading = true
defer { isLoading = false }
// 加载用户数据...
}
}
struct UserView: View {
// ✅ 创建并拥有对象的生命周期
@StateObject private var viewModel = UserViewModel()
var body: some View {
UserDetailView(viewModel: viewModel)
}
}
struct UserDetailView: View {
// ✅ 引用外部传入的对象
@ObservedObject var viewModel: UserViewModel
var body: some View {
Text(viewModel.name)
}
}
@EnvironmentObject - 跨层级共享
适合全局状态如主题、用户信息等:
class AppSettings: ObservableObject {
@Published var isDarkMode = false
@Published var fontSize: CGFloat = 16
}
// 在根视图注入
@main
struct MyApp: App {
@StateObject private var settings = AppSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(settings)
}
}
}
// 在任意子视图使用
struct SettingsView: View {
@EnvironmentObject var settings: AppSettings
var body: some View {
Toggle("深色模式", isOn: $settings.isDarkMode)
}
}
3. @Observable 宏 (iOS 17+)
iOS 17 引入的新观察机制,大幅简化代码:
// 旧方式 - ObservableObject
class OldViewModel: ObservableObject {
@Published var name = ""
@Published var email = ""
@Published var age = 0
}
// 新方式 - @Observable
@Observable
class NewViewModel {
var name = "" // 自动追踪
var email = "" // 自动追踪
var age = 0 // 自动追踪
}
struct ProfileView: View {
var viewModel = NewViewModel() // 无需 @StateObject
var body: some View {
Form {
TextField("姓名", text: $viewModel.name)
TextField("邮箱", text: $viewModel.email)
Stepper("年龄: \(viewModel.age)", value: $viewModel.age)
}
}
}
@Observable 的优势
| 特性 | ObservableObject | @Observable |
|---|---|---|
| 属性标记 | 每个需要 @Published | 自动追踪所有属性 |
| 精确更新 | 任意属性变化都刷新 | 只刷新使用的属性 |
| 绑定语法 | 需要 $ 前缀 | 需要 @Bindable |
| 嵌套观察 | 不支持 | 自动支持 |
4. 视图生命周期
onAppear / onDisappear
struct DataView: View {
@State private var data: [Item] = []
var body: some View {
List(data) { item in
Text(item.name)
}
.onAppear {
loadData()
}
.onDisappear {
cleanup()
}
}
}
task 修饰符
用于异步操作,自动处理取消:
struct AsyncView: View {
@State private var user: User?
var body: some View {
Group {
if let user {
Text(user.name)
} else {
ProgressView()
}
}
.task {
// 视图消失时自动取消
user = try? await fetchUser()
}
.task(id: userId) {
// userId 变化时重新执行
user = try? await fetchUser(id: userId)
}
}
}
5. 列表与导航
List 性能优化
struct OptimizedListView: View {
@State private var items: [Item] = []
var body: some View {
List {
ForEach(items) { item in
ItemRow(item: item)
}
.onDelete(perform: deleteItems)
.onMove(perform: moveItems)
}
.listStyle(.insetGrouped)
.refreshable {
await loadItems()
}
.searchable(text: $searchText)
}
}
NavigationStack (iOS 16+)
struct NavigationExample: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List(items) { item in
NavigationLink(value: item) {
Text(item.name)
}
}
.navigationDestination(for: Item.self) { item in
ItemDetailView(item: item)
}
.navigationDestination(for: Category.self) { category in
CategoryView(category: category)
}
.navigationTitle("首页")
}
}
// 编程式导航
func navigateToItem(_ item: Item) {
path.append(item)
}
func popToRoot() {
path.removeLast(path.count)
}
}
6. 自定义视图修饰符
ViewModifier 协议
struct CardModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.white)
.cornerRadius(16)
.shadow(color: .black.opacity(0.1), radius: 10, x: 0, y: 5)
}
}
extension View {
func cardStyle() -> some View {
modifier(CardModifier())
}
}
// 使用
Text("卡片内容")
.cardStyle()
条件修饰符
extension View {
@ViewBuilder
func `if`<Transform: View>(
_ condition: Bool,
transform: (Self) -> Transform
) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
// 使用
Text("Hello")
.if(isHighlighted) { view in
view.foregroundColor(.red)
}
7. 动画系统
隐式动画
struct AnimatedView: View {
@State private var isExpanded = false
var body: some View {
VStack {
Rectangle()
.fill(.blue)
.frame(width: isExpanded ? 200 : 100,
height: isExpanded ? 200 : 100)
.animation(.spring(duration: 0.5), value: isExpanded)
Button("切换") {
isExpanded.toggle()
}
}
}
}
显式动画
Button("动画") {
withAnimation(.easeInOut(duration: 0.3)) {
isExpanded.toggle()
}
}
转场动画
struct TransitionView: View {
@State private var showDetail = false
var body: some View {
VStack {
if showDetail {
DetailView()
.transition(.asymmetric(
insertion: .slide.combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))
}
Button("显示详情") {
withAnimation {
showDetail.toggle()
}
}
}
}
}
8. 最佳实践
✅ 推荐做法
// 1. 保持视图简单,逻辑放入 ViewModel
struct GoodView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
content
.task { await viewModel.load() }
}
}
// 2. 使用 @ViewBuilder 提取复杂视图
@ViewBuilder
private var content: some View {
if viewModel.isLoading {
ProgressView()
} else {
dataView
}
}
// 3. 合理拆分子视图
struct ItemRow: View {
let item: Item
var body: some View { /* ... */ }
}
❌ 避免的做法
// 1. 不要在 body 中执行副作用
var body: some View {
// ❌ 错误
let _ = print("body called")
fetchData() // ❌ 不要这样做
Text("Hello")
}
// 2. 不要过度使用 @State
// ❌ 复杂对象应该用 @StateObject
@State private var viewModel = ComplexViewModel()
// 3. 不要在父视图创建 @ObservedObject
// ❌ 每次刷新都会重建
@ObservedObject var vm = ViewModel()
9. 调试技巧
打印视图更新
extension View {
func debugPrint(_ value: Any) -> some View {
#if DEBUG
let _ = print(value)
#endif
return self
}
}
// 使用
Text("Hello")
.debugPrint("Text 视图更新了")
Self._printChanges()
var body: some View {
let _ = Self._printChanges() // 打印触发更新的属性
Text("Hello")
}