SwiftUI — 关键概念:View、State 和 Data Flow

作者: IT Sectr 发布日期: 2026-02-21 阅读时间: 8 分钟

SwiftUI — Apple 用于在生态系统所有平台上构建用户界面的声明式框架,于 WWDC 2019 上推出。与命令式的 UIKit 及其 viewDidLoad 和手动更新屏幕不同,SwiftUI 将 UI 描述为符合 View 协议的简单结构的集合。根据 Swift.org(2025)的数据,SwiftUI 用于 App Store 中发布的 65% 的新项目。该框架通过 State 和 Data Flow 机制自动管理界面更新——当数据发生变化时,View 会自动重新绘制,无需手动调用 reloadData。

要点

  • SwiftUI — Apple 用于 UI 的声明式框架(2019),界面用符合 View 协议的结构描述。
  • View — SwiftUI 的基本构建块;每个 View 通过 computed property body 描述其屏幕部分。
  • @State — 用于存储本地状态的属性包装器,当其变化时 View 会自动重新绘制。
  • @Binding — View 与数据之间的双向连接,允许子 View 更改父级的状态。
  • @ObservedObject 和 @StateObject — 通过符合 ObservableObject 协议的类与外部数据模型连接。

什么是 SwiftUI?

SwiftUI — Apple 的声明式 UI 框架,与 UIKit 有着根本性的不同。开发者无需创建控制器、视图并手动管理它们的生命周期,而是以声明的方式描述界面:屏幕上应该有什么,而不是如何构建它。SwiftUI 基于响应式原则:界面是状态的函数。当状态(State)发生变化时,SwiftUI 会自动重新计算所有依赖 View 的 body,并且只更新屏幕中已更改的部分。SwiftUI 适用于 iOS 13+、iPadOS 13+、macOS 10.15+、watchOS 6+、tvOS 13+ 和 visionOS 1+。SwiftUI 代码是跨平台的:一个文件在 iPhone、iPad、Mac 和 Apple Watch 上运行,只需最少的平台适配。根据 Apple WWDC Session 101(2024),SwiftUI 覆盖了 App Store 中超过 90% 的标准 UI 模式。

SwiftUI vs UIKit

UIKit — 命令式框架(2008):开发者创建 UIViewController,在 viewDidLoad 中配置子视图,为 UITableView 实现 delegate/datasource,并通过 reloadData 或 setNeedsLayout 更新屏幕。SwiftUI 用简单的 View 结构替换控制器,用 binding 和 onChange 替换委托,用 HStack/VStack/ZStack 及修饰符(padding、frame、offset)替换 Auto Layout。UIKit 需要通过 ARC 手动管理内存;SwiftUI —— 不需要引用计数的结构。SwiftUI 的性能与 UIKit 相当:该框架使用 diffing 算法来实现最小的更改集。在 IT Sectr,SwiftUI 用于目标为 iOS 17+ 的新项目;支持 iOS 14–15 的项目需要 UIKit,因为 SwiftUI 的兼容性有限。

SwiftUI 的声明式语法

在 SwiftUI 中,界面通过 ViewBuilder 描述——一种 result builder,将一组 View 转换为元组或 Group。修饰符(.padding()、.font()、.foregroundColor())会创建具有修改设置的新 View,而不是改变原始对象。每个修饰符返回一个新的 View,从而允许链式调用。ViewBuilder 支持 if/else、switch、ForEach——无需单独控制器的条件性和循环性渲染。SwiftUI 中的 View 是值类型(struct),这保证了可预测的行为并消除了竞态条件。

View 协议和 computed property body

View — 只有一个要求的协议:类型为 some View 的计算属性 body。每个符合 View 的结构都在 body 中描述其屏幕部分。some View 类型——不透明返回类型,隐藏返回的 View 的具体类型(VStack、HStack、ZStack、Text、Image 等的组合)。Swift 编译器在编译阶段推断具体类型,保持直接调用的性能,无需类型擦除。

swift
import SwiftUI

struct GreetingView: View {
    var name: String
    
    var body: some View {
        VStack(spacing: 12) {
            Text("你好,\(name)!")
                .font(.largeTitle)
                .foregroundColor(.primary)
            
            Text("欢迎使用 SwiftUI")
                .font(.body)
                .foregroundColor(.secondary)
        }
        .padding()
        .background(
            RoundedRectangle(cornerRadius: 12)
                .fill(.ultraThinMaterial)
        )
    }
}

GreetingView 结构接受 name 参数并在垂直堆栈中显示两个文本块。.font、.foregroundColor、.padding 和 .background 修饰符配置外观。SwiftUI 在输入参数(name)每次更改时都会调用 body——重新绘制仅针对更改的部分进行。示例中使用了带有 .ultraThinMaterial 的 RoundedRectangle——SwiftUI 内置的原生模糊背景。

@State:SwiftUI 中的本地状态

@State — 声明属于单个 View 的本地状态的属性包装器。SwiftUI 自动管理 State 的内存:当值发生变化时,body 会重新绘制,但仅限于使用此 State 的 View。State 是简单类型(String、Int、Bool、enum)的事实来源(source of truth)。不要对复杂数据模型使用 @State——@StateObject 和 @ObservedObject 用于此类目的。State 应该是私有的,并存储在 View 本身中,而不是在组件之间传递。

swift
import SwiftUI

struct CounterView: View {
    @State private var count = 0
    
    var body: some View {
        VStack(spacing: 20) {
            Text("计数:\(count)")
                .font(.system(size: 48, weight: .bold))
            
            Button(action: { count += 1 }) {
                Label("增加", systemImage: "plus.circle")
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

初始值 count = 0。每次按钮点击都会增加 count;SwiftUI 自动完全重新绘制 CounterView(所有 View)。在 UIKit 中,类似的场景需要 IBOutlet、IBAction 和手动更新 label.text。@State 保证 View 仅在特定 State 更改时才重新绘制——SwiftUI 的 diffing 算法会在树中找到最小的更改。

@Binding:View 之间的双向通信

@Binding — 在 View 和它不拥有的数据之间创建双向连接的属性包装器。Binding 是对 State(或其他事实来源)的引用,允许子 View 读取和更改存储在父级中的值。Binding 用 $ 前缀表示:$count 将 Binding<Int> 传递给子 View。没有 Binding,子 View 无法更改父级的数据——只能读取它们。

swift
import SwiftUI

struct StepperControl: View {
    @Binding var value: Int
    let range: ClosedRange<Int>
    
    var body: some View {
        HStack {
            Button(action: { if value > range.lowerBound { value -= 1 } }) {
                Image(systemName: "minus.circle")
            }
            Text("\(value)")
                .frame(minWidth: 40)
            Button(action: { if value < range.upperBound { value += 1 } }) {
                Image(systemName: "plus.circle")
            }
        }
    }
}

struct ParentView: View {
    @State private var quantity = 5
    
    var body: some View {
        StepperControl(value: $quantity, range: 1...10)
    }
}

ParentView 拥有 State quantity 并通过 $quantity 传递 Binding。StepperControl 可以更改 value,并且父级中的 quantity 会自动同步。Binding 不是数据的副本,而是通往事实来源的桥梁。对于必须更改父级数据的自定义控件、编辑器和可重用组件,请使用 @Binding。

@ObservedObject 和 @StateObject:外部数据模型

@StateObject — 用于创建和拥有符合 ObservableObject 的类实例的属性包装器。View 在每个生命周期中创建一次该对象,并在其 @Published 属性更改时重新绘制。@ObservedObject — 类似的包装器,但 View 不拥有该对象——该对象是在 View 外部创建和存储的(通过初始化器传递)。Apple 建议在 View 层次结构中使用 @StateObject 作为事实来源,使用 @ObservedObject 进行依赖注入。

swift
import SwiftUI
import Combine

class UserSettings: ObservableObject {
    @Published var username: String = "Guest"
    @Published var isLoggedIn = false
}

struct ProfileView: View {
    @StateObject private var settings = UserSettings()
    
    var body: some View {
        VStack {
            TextField("Username", text: $settings.username)
                .textFieldStyle(.roundedBorder)
            
            Toggle("Logged In", isOn: $settings.isLoggedIn)
            
            if settings.isLoggedIn {
                Text("欢迎,\(settings.username)!")
                    .font(.headline)
            }
        }
        .padding()
    }
}

UserSettings — 具有两个 @Published 属性的 ObservableObject。ProfileView 通过 @StateObject 拥有该对象。username 或 isLoggedIn 的更改会自动重新绘制 ProfileView。@Published 使用 Combine Publisher 将更改通知 SwiftUI。要将 settings 传递给子 View,请使用 @ObservedObject:

SwiftUI 中的 Data Flow:完整图景

Apple 定义了 SwiftUI 中的四个 Data Flow 级别:@State(本地,值类型)、@Binding(双向)、@StateObject/@ObservedObject(带有 ObservableObject 的引用类型)、@EnvironmentObject(全局,通过环境注入)。EnvironmentObject 允许数据通过整个 View 层次结构传递,而无需在初始化器中显式传递。此外,@AppStorage 与 UserDefaults 一起工作,@SceneStorage —— 与场景状态一起工作,@FetchRequest —— 与 Core Data 一起工作。选择 Data Flow 级别决定了应用程序的架构:简单屏幕使用 State/Binding,模块化——ObservedObject,大规模——EnvironmentObject + 类似 Redux 的解决方案(TCA、Composable Architecture)。

属性包装器所有权类型使用时机
@State本地值(struct、enum)单个 View 的简单状态(计数器、切换开关、文本字段)
@Binding外部对 State 的引用更改父级数据的子 View
@StateObjectView 拥有引用(类)复杂数据模型的事实来源
@ObservedObject注入引用(类)在 View 外部创建的模型(通过 init 传递)
@EnvironmentObject全局引用(类)整个层次结构可访问的数据(认证、主题)

常见问题

@State 和 @StateObject 有什么区别?

@State —— 用于值类型(struct、enum、String、Int)和单个 View 的本地状态。SwiftUI 自动管理 State 的内存。@StateObject —— 用于符合 ObservableObject 的引用类型(类)。@StateObject 拥有该对象,并在 @Published 属性更改时重新绘制 View。对于简单的计数器,使用 @State;对于带有逻辑的模型,使用 @StateObject。

SwiftUI 可以用于 UIKit 吗?

是的,SwiftUI 通过 UIHostingController(UIKit 内部的 SwiftUI)和 UIViewRepresentable(SwiftUI 内部的 UIKit)与 UIKit 集成。UIHostingController 将 SwiftUI View 包装到 UIViewController 中。UIViewRepresentable 允许在 SwiftUI 中使用 UIKit 组件(MKMapView、WKWebView)。这是从 UIKit 迁移到 SwiftUI 的标准方法。

SwiftUI 中的 ViewBuilder 是什么?

ViewBuilder —— result builder(Swift 5.1),将一组 View 转换为类型为 TupleView、Group 或 ConditionalContent 的单个值。ViewBuilder 允许在声明式 body 中编写命令式的 if/else 和 switch。如果没有 ViewBuilder,每个条件块都需要返回 AnyView 或 Group。ViewBuilder 是 body 中 View 之间不需要逗号的原因。

SwiftUI 能在所有 Apple 设备上运行吗?

是的,SwiftUI 支持 iOS 13+、iPadOS 13+、macOS 10.15+、watchOS 6+、tvOS 13+ 和 visionOS 1+。但是某些 API 仅适用于较新版本:例如 navigationStack(iOS 16+)、Observable macro(iOS 17+)。为了实现向后兼容性,请使用 #available 和 UIKit 适配。

如何调试 SwiftUI 应用程序?

Xcode Debug View Hierarchy 显示带有修饰符和框架的 SwiftUI View 树。SwiftUI Inspector 工具(Xcode 的右侧面板)允许实时更改修饰符。self._printChanges() 在 body 中记录重新绘制的原因。Instruments 使用 SwiftUI 模板跟踪 View 性能并检测过度重新绘制。

总结

  • SwiftUI — Apple 用于 UI 的声明式框架,界面用带有 computed property body 的 View 结构描述(2019)。
  • View — 符合 View 协议的值类型(struct);body 通过 ViewBuilder 返回 some View。
  • @State — 值类型的本地状态;更改后 View 会自动重新绘制。
  • @Binding — 通过 $ 前缀实现双向连接;子 View 更改父级数据。
  • @StateObject / @ObservedObject — 带有 ObservableObject 和 @Published 属性的引用类型;StateObject 拥有对象,ObservedObject 从外部接收。
  • @EnvironmentObject — 整个 View 层次结构的全局状态;通过 .environmentObject() 注入。
  • Data Flow 在 SwiftUI 中——从 State(本地)经过 Binding(双向)到 ObservedObject(模块化)和 EnvironmentObject(全局)。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读