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.

SwiftUI - Apple Developer

如果你熟悉 React,SwiftUI 会感觉非常亲切:

概念ReactSwiftUI
UI 范式声明式声明式
核心单元ComponentView (struct)
状态管理useState / Context@State / @Environment
副作用useEffect.onAppear / .task
语言JavaScript / TypeScriptSwift

Swift 语言快速入门

在学习 SwiftUI 之前,先了解一些 Swift 基础语法。

变量与常量

// Swift 使用 let 声明常量,var 声明变量
let name = "Alice" // 常量,不可变
var count = 0 // 变量,可变
count += 1
// 类型推断,也可显式声明
let age: Int = 25
let price: Double = 9.99
let isActive: Bool = true

对比 JavaScript:

// JavaScript
const 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

// 可选类型:值可能存在,也可能是 nil
var username: String? = nil
username = "Alice"
// 安全解包方式 1: if let
if 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.

Swift Guard Statement

函数

// 基础函数
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 View
struct Greeting: View {
let name: String
var body: some View {
Text("Hello, \(name)!")
.font(.title)
}
}

React useState vs SwiftUI @State

// React with useState
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Add</button>
</div>
);
}
// SwiftUI with @State
struct 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.

Apple Developer Documentation

Props vs 参数传递

React 通过 props 传递数据,SwiftUI 通过属性:

// React
<CustomCard index={1} onPress={() => console.log('pressed')} />
// SwiftUI
CustomCard(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 @Binding
struct 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.

Apple Developer Documentation

生命周期对比

React HooksSwiftUI
useEffect(() => {}, []).onAppear { }
useEffect(() => { return cleanup }, []).onDisappear { }
useEffect(() => {}, [dep]).onChange(of: dep) { }
useEffect with async.task { }
// React
function 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>;
}
// SwiftUI
struct 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 FlexboxSwiftUI
display: flex; flex-direction: row;HStack
display: flex; flex-direction: column;VStack
position: absolute (层叠)ZStack
justify-contentalignment 参数
align-itemsalignment 参数
gapspacing 参数
flex: 1Spacer()

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)
}
// 使用 ButtonStyle
Button("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))

导航

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.

Apple Developer Documentation

编程式导航

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.

Observable Macro in SwiftUI

import Observation
// 1. 定义可观察类
@Observable
class 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. 需要修改时使用 @Bindable
struct 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 入口注入
@main
struct 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适用场景
@StateuseState单个 View 内部状态
@Bindingprops + callback父子组件双向绑定
@ObservableContext + Zustand跨组件共享 (iOS 17+)
@EnvironmentContext全局配置、依赖注入
@ObservableObjectContext (旧版)iOS 13-16 兼容

旧版方案 (iOS 13-16)

如果需要支持 iOS 17 以下版本:

// 使用 ObservableObject + @Published
class 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.red
Color.blue
Color(.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 (值类型),原因:

  1. 性能:值类型在栈上分配,无引用计数开销
  2. 不可变性:struct 默认不可变,状态变更通过 @State 等包装器
  3. 简洁:无需担心 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 响应式编程

参考资料

Read Next

OpenCode 架构设计与核心技术深度解析

Read Previous

Claude Agent SDK 完全指南 - 从入门到精通