SwiftUI 基础 - 给 React 开发者的入门指南
知识结构
SwiftUI 是什么
SwiftUI 是 Apple 在 2019 年推出的声明式 UI 框架,用于构建 iOS、macOS、watchOS 和 tvOS 应用。
SwiftUI helps you build great-looking apps across all Apple platforms with the power of Swift - and surprisingly little code.
如果你熟悉 React,SwiftUI 会感觉非常亲切:
| 概念 | React | SwiftUI |
|---|---|---|
| UI 范式 | 声明式 | 声明式 |
| 核心单元 | Component | View (struct) |
| 状态管理 | useState / Context | @State / @Environment |
| 副作用 | useEffect | .onAppear / .task |
| 语言 | JavaScript / TypeScript | Swift |
Swift 语言快速入门
在学习 SwiftUI 之前,先了解一些 Swift 基础语法。
变量与常量
// Swift 使用 let 声明常量,var 声明变量let name = "Alice" // 常量,不可变var count = 0 // 变量,可变count += 1
// 类型推断,也可显式声明let age: Int = 25let price: Double = 9.99let isActive: Bool = true对比 JavaScript:
// JavaScriptconst name = "Alice"; // 常量let count = 0; // 变量count += 1;字符串插值
// Swift 使用 \() 进行字符串插值let name = "World"let greeting = "Hello, \(name)!" // "Hello, World!"
// 多行字符串let multiline = """ This is a multiline string """对比 JavaScript:
// JavaScript 使用 ${}const name = "World";const greeting = `Hello, ${name}!`;可选类型 (Optionals)
Swift 的可选类型是其安全特性的核心,类似 TypeScript 的 T | null:
// 可选类型:值可能存在,也可能是 nilvar username: String? = nilusername = "Alice"
// 安全解包方式 1: if letif let name = username { print("Hello, \(name)")} else { print("No username")}
// 安全解包方式 2: guard let (推荐,避免嵌套)func greet() { guard let name = username else { print("No username") return // 必须退出当前作用域 } // name 在这里可以直接使用 print("Hello, \(name)")}
// 空合并运算符let displayName = username ?? "Anonymous"
// 强制解包 (危险,仅在确定有值时使用)let forcedName = username!The guard statement is a core feature for writing safe, readable Swift code. It helps enforce early exits, unwrap optionals, and validate input with minimal nesting.
函数
// 基础函数func greet(name: String) -> String { return "Hello, \(name)!"}
// 调用时需要参数标签greet(name: "Alice")
// 省略参数标签用 _func greet(_ name: String) -> String { return "Hello, \(name)!"}greet("Alice")
// 默认参数值func greet(name: String = "World") -> String { return "Hello, \(name)!"}
// 闭包 (类似箭头函数)let multiply = { (a: Int, b: Int) -> Int in return a * b}
// 尾随闭包简写let numbers = [1, 2, 3, 4, 5]let doubled = numbers.map { $0 * 2 } // [2, 4, 6, 8, 10]结构体与类
// Struct (值类型,推荐用于数据模型)struct User { let id: Int var name: String
func greet() -> String { return "Hello, \(name)!" }}
var user = User(id: 1, name: "Alice")user.name = "Bob" // 可以修改 var 属性
// Class (引用类型)class UserManager { var users: [User] = []
func addUser(_ user: User) { users.append(user) }}协议 (Protocol)
类似 TypeScript 的 interface:
// 定义协议protocol Drawable { func draw()}
// 遵循协议struct Circle: Drawable { var radius: Double
func draw() { print("Drawing circle with radius \(radius)") }}异步编程
Swift 的 async/await 与 JavaScript 非常相似:
// 异步函数func fetchUser() async throws -> User { let url = URL(string: "https://api.example.com/user")! let (data, _) = try await URLSession.shared.data(from: url) return try JSONDecoder().decode(User.self, from: data)}
// 调用异步函数Task { do { let user = try await fetchUser() print(user.name) } catch { print("Error: \(error)") }}
// 并发执行多个任务async let user1 = fetchUser(id: 1)async let user2 = fetchUser(id: 2)let users = try await [user1, user2] // 并发获取React vs SwiftUI 核心对比
组件 vs View
在 React 中,UI 由组件构成。在 SwiftUI 中,一切皆 View。
flowchart LR
subgraph React
FC["函数组件<br/>Function Component"]
Hooks["Hooks<br/>useState, useEffect"]
end
subgraph SwiftUI
SV["struct View"]
PW["Property Wrappers<br/>@State, @Binding"]
end
FC --> SV
Hooks --> PW
React 函数组件 vs SwiftUI View
// React 函数组件function Greeting({ name }) { return <h1>Hello, {name}!</h1>;}// SwiftUI Viewstruct Greeting: View { let name: String
var body: some View { Text("Hello, \(name)!") .font(.title) }}React useState vs SwiftUI @State
// React with useStatefunction Counter() { const [count, setCount] = useState(0);
return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Add</button> </div> );}// SwiftUI with @Statestruct Counter: View { @State private var count = 0
var body: some View { VStack { Text("Count: \(count)") Button("Add") { count += 1 } } }}@State creates local storage for a value in a View. SwiftUI manages the storage and updates the view when the value changes.
Props vs 参数传递
React 通过 props 传递数据,SwiftUI 通过属性:
// React<CustomCard index={1} onPress={() => console.log('pressed')} />// SwiftUICustomCard(index: 1, onPress: { print("pressed") })SwiftUI View 定义:
struct CustomCard: View { let index: Int let onPress: () -> Void
var body: some View { Button(action: onPress) { Text("Card \(index)") } }}状态绑定对比
SwiftUI 使用 @Binding 实现双向数据绑定,类似 React 的 “受控组件” 模式:
// React 受控组件function Parent() { const [isOn, setIsOn] = useState(false);
return <Toggle isOn={isOn} onChange={setIsOn} />;}
function Toggle({ isOn, onChange }) { return ( <button onClick={() => onChange(!isOn)}> {isOn ? 'ON' : 'OFF'} </button> );}// SwiftUI @Bindingstruct ParentView: View { @State private var isOn = false
var body: some View { Toggle(isOn: $isOn) // $ 创建 Binding }}
struct Toggle: View { @Binding var isOn: Bool // 接收 Binding
var body: some View { Button(isOn ? "ON" : "OFF") { isOn.toggle() // 直接修改,自动同步到父组件 } }}A binding creates a two-way connection between a view and its underlying model. SwiftUI watches for changes and updates any affected views automatically.
生命周期对比
| React Hooks | SwiftUI |
|---|---|
useEffect(() => {}, []) | .onAppear { } |
useEffect(() => { return cleanup }, []) | .onDisappear { } |
useEffect(() => {}, [dep]) | .onChange(of: dep) { } |
useEffect with async | .task { } |
// Reactfunction UserProfile({ userId }) { const [user, setUser] = useState(null);
useEffect(() => { fetchUser(userId).then(setUser); }, [userId]);
useEffect(() => { console.log('Component mounted'); return () => console.log('Component unmounted'); }, []);
return <div>{user?.name}</div>;}// SwiftUIstruct UserProfile: View { let userId: Int @State private var user: User?
var body: some View { Text(user?.name ?? "Loading...") .task { // 自动在 view 出现时执行,消失时取消 user = await fetchUser(userId) } .onAppear { print("View appeared") } .onDisappear { print("View disappeared") } .onChange(of: userId) { oldValue, newValue in // userId 变化时触发 print("userId changed from \(oldValue) to \(newValue)") } }}布局系统
SwiftUI 布局 vs CSS Flexbox
SwiftUI 的布局系统与 CSS Flexbox 非常相似:
| CSS Flexbox | SwiftUI |
|---|---|
display: flex; flex-direction: row; | HStack |
display: flex; flex-direction: column; | VStack |
position: absolute (层叠) | ZStack |
justify-content | alignment 参数 |
align-items | alignment 参数 |
gap | spacing 参数 |
flex: 1 | Spacer() |
Stack 布局
// 水平排列 (类似 flex-direction: row)HStack(alignment: .center, spacing: 16) { Image(systemName: "star.fill") Text("Hello") Spacer() // 弹性空间,类似 flex: 1 Text("World")}
// 垂直排列 (类似 flex-direction: column)VStack(alignment: .leading, spacing: 8) { Text("Title") .font(.headline) Text("Subtitle") .font(.subheadline) .foregroundColor(.secondary)}
// 层叠排列 (类似 position: absolute)ZStack(alignment: .bottomTrailing) { Image("background") .resizable() Text("Overlay") .padding() .background(Color.black.opacity(0.7))}修饰符 (Modifiers)
SwiftUI 使用修饰符链来设置样式,类似 CSS 但更加类型安全:
Text("Hello, SwiftUI!") .font(.title) // 字体 .fontWeight(.bold) // 字重 .foregroundColor(.blue) // 文字颜色 .padding() // 内边距 .background(Color.yellow) // 背景色 .cornerRadius(8) // 圆角 .shadow(radius: 4) // 阴影注意:修饰符顺序很重要!
// 不同的顺序产生不同效果Text("A") .padding() .background(Color.red) // padding 在红色背景内
Text("B") .background(Color.red) .padding() // padding 在红色背景外Frame 和尺寸
// 固定尺寸Text("Fixed Size") .frame(width: 200, height: 100)
// 最大/最小尺寸Text("Flexible") .frame(maxWidth: .infinity) // 占满可用宽度 .frame(minHeight: 44) // 最小高度
// 对齐Text("Aligned") .frame(maxWidth: .infinity, alignment: .leading) // 左对齐常用组件
按钮
// 基础按钮Button("Click Me") { print("Button tapped")}
// 自定义样式按钮Button(action: { print("Custom button tapped")}) { HStack { Image(systemName: "star.fill") Text("Favorite") } .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(8)}
// 使用 ButtonStyleButton("Styled") { } .buttonStyle(.borderedProminent)输入框
struct LoginForm: View { @State private var email = "" @State private var password = ""
var body: some View { VStack(spacing: 16) { TextField("Email", text: $email) .textFieldStyle(.roundedBorder) .keyboardType(.emailAddress) .autocapitalization(.none)
SecureField("Password", text: $password) .textFieldStyle(.roundedBorder)
Button("Login") { print("Login with \(email)") } .disabled(email.isEmpty || password.isEmpty) } .padding() }}列表
// 静态列表List { Text("Item 1") Text("Item 2") Text("Item 3")}
// 动态列表struct TodoList: View { let items = ["Buy groceries", "Walk the dog", "Read a book"]
var body: some View { List(items, id: \.self) { item in Text(item) } }}
// 带自定义行的列表struct UserList: View { let users: [User]
var body: some View { List(users) { user in // User 需要遵循 Identifiable HStack { AsyncImage(url: user.avatarURL) { image in image.resizable() } placeholder: { ProgressView() } .frame(width: 40, height: 40) .clipShape(Circle())
VStack(alignment: .leading) { Text(user.name) .font(.headline) Text(user.email) .font(.caption) .foregroundColor(.secondary) } } } }}图片
// SF Symbols (系统图标)Image(systemName: "heart.fill") .foregroundColor(.red) .font(.title)
// 本地图片Image("logo") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 100)
// 网络图片 (iOS 15+)AsyncImage(url: URL(string: "https://example.com/image.jpg")) { phase in switch phase { case .empty: ProgressView() case .success(let image): image .resizable() .aspectRatio(contentMode: .fill) case .failure: Image(systemName: "photo") .foregroundColor(.gray) @unknown default: EmptyView() }}.frame(width: 200, height: 200).clipShape(RoundedRectangle(cornerRadius: 12))导航
NavigationStack (iOS 16+)
struct ContentView: View { var body: some View { NavigationStack { List { NavigationLink("Profile", value: "profile") NavigationLink("Settings", value: "settings") } .navigationTitle("Home") .navigationDestination(for: String.self) { value in switch value { case "profile": ProfileView() case "settings": SettingsView() default: Text("Unknown") } } } }}NavigationStack manages a stack of views and allows users to navigate forward and backward through them.
编程式导航
struct ContentView: View { @State private var path = NavigationPath()
var body: some View { NavigationStack(path: $path) { VStack { Button("Go to Profile") { path.append("profile") }
Button("Go to Detail") { path.append(123) // 传递 Int } } .navigationDestination(for: String.self) { value in Text("String destination: \(value)") } .navigationDestination(for: Int.self) { id in Text("Detail for ID: \(id)") } } }}传递参数
// 定义数据模型struct Product: Identifiable, Hashable { let id: Int let name: String let price: Double}
struct ProductList: View { let products: [Product]
var body: some View { NavigationStack { List(products) { product in NavigationLink(value: product) { Text(product.name) } } .navigationTitle("Products") .navigationDestination(for: Product.self) { product in ProductDetail(product: product) } } }}
struct ProductDetail: View { let product: Product
var body: some View { VStack { Text(product.name) .font(.title) Text("$\(product.price, specifier: "%.2f")") } .navigationTitle(product.name) }}状态管理
@State (局部状态)
类似 React 的 useState,用于 View 内部的简单状态:
struct ToggleExample: View { @State private var isOn = false
var body: some View { Toggle("Enable Feature", isOn: $isOn) .padding() }}@Observable (iOS 17+)
iOS 17 引入的新状态管理方案,大幅简化了跨组件状态共享:
The @Observable Macro simplifies code at the implementation level and increases the performance of SwiftUI views by preventing unnecessary redraws.
import Observation
// 1. 定义可观察类@Observableclass UserStore { var currentUser: User? var isLoggedIn: Bool { currentUser != nil }
func login(email: String, password: String) async { // 登录逻辑 currentUser = User(name: "Alice", email: email) }
func logout() { currentUser = nil }}
// 2. 在 View 中使用struct ProfileView: View { var store: UserStore // 直接作为属性,无需 @ObservedObject
var body: some View { if let user = store.currentUser { VStack { Text("Hello, \(user.name)!") Button("Logout") { store.logout() } } } else { Text("Please login") } }}
// 3. 需要修改时使用 @Bindablestruct SettingsView: View { @Bindable var store: UserStore // 允许创建 Binding
var body: some View { if let user = store.currentUser { TextField("Name", text: Binding( get: { user.name }, set: { store.currentUser?.name = $0 } )) } }}@Environment (环境值)
类似 React 的 Context,用于向下传递数据:
// 在 App 入口注入@mainstruct MyApp: App { @State private var userStore = UserStore()
var body: some Scene { WindowGroup { ContentView() .environment(userStore) } }}
// 在任意子 View 中获取struct DeepNestedView: View { @Environment(UserStore.self) var userStore
var body: some View { Text(userStore.currentUser?.name ?? "Guest") }}状态管理方案对比
| 方案 | 类比 React | 适用场景 |
|---|---|---|
@State | useState | 单个 View 内部状态 |
@Binding | props + callback | 父子组件双向绑定 |
@Observable | Context + Zustand | 跨组件共享 (iOS 17+) |
@Environment | Context | 全局配置、依赖注入 |
@ObservableObject | Context (旧版) | iOS 13-16 兼容 |
旧版方案 (iOS 13-16)
如果需要支持 iOS 17 以下版本:
// 使用 ObservableObject + @Publishedclass UserStore: ObservableObject { @Published var currentUser: User? @Published var isLoading = false}
// 在 View 中使用struct ContentView: View { @StateObject private var store = UserStore() // 创建并持有
var body: some View { ChildView() .environmentObject(store) // 注入环境 }}
struct ChildView: View { @EnvironmentObject var store: UserStore // 从环境获取
var body: some View { Text(store.currentUser?.name ?? "Guest") }}主题与样式
颜色
// 系统颜色 (自动适配 Light/Dark Mode)Text("Primary").foregroundColor(.primary)Text("Secondary").foregroundColor(.secondary)Text("Accent").foregroundColor(.accentColor)
// 语义化颜色Color.redColor.blueColor(.systemBackground) // UIKit 颜色Color(.secondarySystemBackground)
// 自定义颜色Color(red: 0.2, green: 0.5, blue: 0.8)Color(hex: 0x3498db) // 需要扩展
// Assets 中的颜色Color("BrandColor") // 在 Assets.xcassets 中定义Dark Mode 适配
SwiftUI 自动适配 Dark Mode,但可以手动控制:
struct ContentView: View { @Environment(\.colorScheme) var colorScheme
var body: some View { Text("Current mode: \(colorScheme == .dark ? "Dark" : "Light")") .foregroundColor(colorScheme == .dark ? .white : .black) }}
// 强制特定模式ContentView() .preferredColorScheme(.dark) // 强制 Dark Mode自定义 ViewModifier
类似 React 中抽取复用样式的高阶组件:
// 定义修饰符struct CardStyle: ViewModifier { func body(content: Content) -> some View { content .padding() .background(Color(.systemBackground)) .cornerRadius(12) .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4) }}
// 扩展 View 方便调用extension View { func cardStyle() -> some View { modifier(CardStyle()) }}
// 使用Text("Card Content") .cardStyle()项目结构
MySwiftUIApp/├── MySwiftUIApp.swift # App 入口 (@main)├── ContentView.swift # 主视图├── Models/ # 数据模型│ └── User.swift├── Views/ # 视图组件│ ├── Components/ # 可复用组件│ │ └── CustomButton.swift│ └── Screens/ # 页面视图│ ├── HomeView.swift│ └── ProfileView.swift├── ViewModels/ # 视图模型 / Store│ └── UserStore.swift├── Services/ # 网络请求、业务逻辑│ └── APIService.swift├── Utilities/ # 工具函数、扩展│ └── Extensions.swift├── Resources/ # 资源文件│ └── Assets.xcassets└── Preview Content/ # 预览资源常见问题
何时使用 @State vs @Observable
flowchart TD
A["状态是否只在单个 View 内使用?"] -->|Yes| B["@State"]
A -->|No| C["需要支持 iOS 17 以下吗?"]
C -->|Yes| D["@ObservableObject"]
C -->|No| E["@Observable"]
为什么 SwiftUI View 是 struct
React 组件是函数,SwiftUI View 是 struct (值类型),原因:
- 性能:值类型在栈上分配,无引用计数开销
- 不可变性:struct 默认不可变,状态变更通过 @State 等包装器
- 简洁:无需担心
this绑定问题
Preview 不工作怎么办
// 确保 Preview 提供必要的环境#Preview { ContentView() .environment(UserStore()) // 注入依赖}
// 多个预览#Preview("Light Mode") { ContentView() .preferredColorScheme(.light)}
#Preview("Dark Mode") { ContentView() .preferredColorScheme(.dark)}下一步学习
- Swift 语言进阶:泛型、协议、并发
- SwiftUI 动画与手势
- Core Data / SwiftData 数据持久化
- Combine 响应式编程
参考资料
- SwiftUI - Apple Developer - 官方主页
- SwiftUI Tutorials - 官方教程
- SwiftUI Documentation - API 文档
- Learning SwiftUI - 概念教程
- Migrating to @Observable - Observable 迁移指南
- SwiftUI vs React Comparison - 框架对比
- SwiftUI from React Developer Perspective - React 开发者视角