SwiftUI的核心概念:View与修饰器
SwiftUI的一切界面元素都基于View
协议——每个可展示的内容(文本、按钮、图片)都是View
,甚至布局容器(如HStack
)也是View
。与UIKit不同,SwiftUI的View
是值类型(结构体),这意味着它更轻量、线程安全,且易于优化。

修饰器(Modifier)是SwiftUI的另一大核心:通过链式调用修改View
的外观或行为,每次调用都会返回一个新的View
实例(原View
不会被修改)。例如:
Text("Hello, SwiftUI!")
.font(.title) // 设置字体为标题样式
.foregroundColor(.blue) // 文字颜色改为蓝色
.padding(.vertical, 10) // 上下内边距10点
.background(Color.gray.opacity(0.2)) // 浅灰色背景
上述代码中,Text
是基础View
,后续的.font
、.foregroundColor
等修饰器依次给它添加属性,最终生成一个“带蓝色标题字体、浅灰背景的文本”View
。
需要注意:修饰器的顺序会影响效果——比如先加padding
再加background
,背景会覆盖内边距区域;反之则背景只覆盖文本本身。
布局基础:Stack与Spacer的灵活运用
SwiftUI的布局系统基于弹性盒子模型,核心容器是三个Stack
:
– HStack
:水平方向排列子View
– VStack
:垂直方向排列子View
– ZStack
:层叠排列子View
(类似UIKit的UIStackView
或CALayer
)
以登录界面为例,用VStack
快速搭建垂直布局:
VStack(spacing: 20) { // 子View之间的间距20点
Image(systemName: "person.circle.fill")
.font(.system(size: 80))
.foregroundColor(.blue)
TextField("用户名", text: $username)
.textFieldStyle(.roundedBorder) // 圆角边框样式
.padding(.horizontal, 30) // 左右内边距30点
SecureField("密码", text: $password)
.textFieldStyle(.roundedBorder)
.padding(.horizontal, 30)
Button("登录") {
// 处理登录逻辑
}
.buttonStyle(.borderedProminent) // 突出显示的按钮样式
.padding(.top, 10)
Spacer() // 占据剩余空间,将内容顶到顶部
}
.padding() // 整个VStack的内边距
其中Spacer()
是“弹性空格”——它会自动拉伸填满剩余空间,常用于调整子View
的对齐方式(比如将按钮推到底部,或让输入框居中)。
状态管理:@State与@Binding的实战技巧
SwiftUI是数据驱动的框架:界面的变化由数据状态决定,而非手动修改控件属性。@State
和@Binding
是最基础的状态管理工具。
@State:单个View的私有状态
@State
用于标记View内部的可变状态(比如开关的开启状态、滑块的位置)。它是一个属性包装器,会将值类型(如Bool
、String
)转换为“可观察对象”——当值变化时,SwiftUI会自动重新计算body
,更新界面。
例如,一个带切换状态的按钮:
struct ToggleView: View {
@State private var isOn = false // 私有状态,初始值false
var body: some View {
Button(action: {
isOn.toggle() // 点击时切换状态
}) {
Text(isOn ? "开启" : "关闭")
.padding(10)
.background(isOn ? Color.green : Color.gray)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
当isOn
变化时,按钮的文本、背景色会自动更新——无需手动调用setNeedsDisplay
!
@Binding:父子View的状态同步
如果需要将状态从父View
传递给子View
并允许修改,就需要@Binding
。它相当于状态的“引用”,让子View
能修改父View
的状态。
例如,父View
有一个开关状态,子View
是开关按钮:
// 子View:开关按钮
struct SwitchButton: View {
@Binding var isEnabled: Bool // 绑定父View的状态
var body: some View {
Button(action: {
isEnabled.toggle()
}) {
Image(systemName: isEnabled ? "toggle.on" : "toggle.off")
.font(.title)
.foregroundColor(isEnabled ? .blue : .gray)
}
}
}
// 父View:使用子View
struct ParentView: View {
@State private var isDarkMode = false // 父View的私有状态
var body: some View {
VStack {
Text("深色模式:(isDarkMode ? "开启" : "关闭")")
SwitchButton(isEnabled: $isDarkMode) // 传递绑定($符号获取投影值)
}
}
}
这里的$isDarkMode
是@State
的投影属性,它返回Binding<Bool>
类型,让子View
能修改父View
的isDarkMode
状态。
数据驱动:List与ForEach的动态列表构建
iOS应用中最常见的界面是“动态列表”(如联系人、商品列表),SwiftUI用List
和ForEach
实现这一需求——数据变化时,列表自动更新。
基础用法:静态列表
List
可以直接包含多个View
,适合静态内容:
List {
Text("第一项")
Text("第二项")
Button("第三项", action: {})
}
动态列表:ForEach与Identifiable
当列表项来自数组时,需要用ForEach
遍历——但数组元素必须遵循Identifiable
协议(提供唯一id
),或手动指定id
参数。
例如,展示一个“水果列表”:
// 数据模型:遵循Identifiable
struct Fruit: Identifiable {
let id = UUID() // 自动生成唯一ID
let name: String
let icon: String
}
// 列表View
struct FruitListView: View {
@State private var fruits = [
Fruit(name: "苹果", icon: "applelogo"),
Fruit(name: "香蕉", icon: "banana"),
Fruit(name: "橙子", icon: "orange")
]
var body: some View {
List(fruits) { fruit in // ForEach的简化写法(List直接接收Identifiable数组)
HStack {
Image(systemName: fruit.icon)
.font(.title)
.foregroundColor(.orange)
Text(fruit.name)
.font(.body)
}
}
}
}
如果数组元素没有id
,可以用ForEach(fruits, id: .name)
(假设name
唯一),但更推荐遵循Identifiable
——它更规范、不易出错。
编辑功能:滑动删除与排序
SwiftUI为List
内置了编辑功能,只需添加EditButton()
和onDelete
/onMove
修饰器:
List {
ForEach($fruits) { $fruit in // 用$绑定,允许修改子项
HStack {
Image(systemName: fruit.icon)
TextField("水果名称", text: $fruit.name) // 可编辑文本
}
}
.onDelete(perform: deleteFruit) // 滑动删除
.onMove(perform: moveFruit) // 长按拖动排序
}
.toolbar {
EditButton() // 右上角编辑按钮
}
// 删除逻辑
func deleteFruit(at offsets: IndexSet) {
fruits.remove(atOffsets: offsets)
}
// 排序逻辑
func moveFruit(from source: IndexSet, to destination: Int) {
fruits.move(fromOffsets: source, toOffset: destination)
}
上述代码中,$fruits
让ForEach
获得Binding<Fruit>
,从而允许修改列表项的内容;onDelete
和onMove
则实现了“滑动删除”和“拖动排序”的核心功能——无需像UIKit那样写复杂的代理方法!
交互进阶:手势与动画的流畅实现
SwiftUI的交互设计强调“自然、流畅”,核心是手势(Gesture)和动画(Animation)的结合。
手势识别:Tap、Drag与LongPress
SwiftUI支持常见的手势类型,通过.gesture()
修饰器添加:
– 点击手势(TapGesture):用于按钮外的点击交互
– 拖动手势(DragGesture):用于移动或缩放View
– 长按手势(LongPressGesture):用于弹出菜单或编辑模式
例如,一个可拖动的“浮球”:
struct DraggableBall: View {
@State private var position = CGPoint(x: 150, y: 300) // 初始位置
var body: some View {
Circle()
.fill(.red.opacity(0.8))
.frame(width: 80, height: 80)
.position(position)
.gesture(
DragGesture()
.onChanged { value in
position = value.location
}
)
}
}
动画:让交互更自然
SwiftUI的动画是声明式的——只需标记“需要动画的状态变化”,框架会自动处理过渡效果。
基础动画:withAnimation
用withAnimation
包裹状态变化,即可触发动画:
struct AnimatedButton: View {
@State private var scale = 1.0 // 缩放比例
var body: some View {
Button("点击放大") {
withAnimation(.easeInOut(duration: 0.3)) {
scale *= 1.2 // 状态变化时自动动画
}
}
.scaleEffect(scale) // 应用缩放效果
}
}
withAnimation
的参数可以是预定义的动画(如.spring()
弹性动画、.linear()
线性动画),也可以自定义时长和曲线。
隐式动画:animation修饰器
如果希望View
的所有状态变化都带动画,可以用.animation()
修饰器(隐式动画):
struct ImplicitAnimationView: View {
@State private var rotation = 0.0
var body: some View {
Rectangle()
.fill(.blue)
.frame(width: 100, height: 100)
.rotationEffect(.degrees(rotation))
.animation(.spring(), value: rotation) // 当rotation变化时触发动画
Button("旋转") {
rotation += 90 // 无需withAnimation,隐式动画自动生效
}
}
}
隐式动画适合“状态变化即需要动画”的场景,减少重复代码。
实战案例:Todo List应用完整构建
接下来,我们用SwiftUI构建一个完整的Todo List应用,覆盖“添加、删除、修改、标记完成”核心功能。
1. 定义数据模型
首先创建TodoItem
结构体,遵循Identifiable
(用于列表遍历):
struct TodoItem: Identifiable {
let id = UUID()
var title: String // 任务标题
var isCompleted: Bool // 是否完成
}
2. 构建主界面
主界面包含三个部分:
– 导航栏:标题+编辑按钮
– 列表:展示Todo项(带复选框和可编辑文本)
– 底部输入栏:添加新任务
struct TodoListView: View {
@State private var todoItems = [TodoItem]() // 存储所有任务
@State private var newTodoTitle = "" // 新任务输入框内容
var body: some View {
NavigationStack { // iOS 16+推荐用NavigationStack替代NavigationView
List {
ForEach($todoItems) { $item in // 绑定到每个TodoItem
HStack {
// 复选框按钮
Button(action: {
item.isCompleted.toggle()
}) {
Image(systemName: item.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundColor(item.isCompleted ? .green : .gray)
}
.buttonStyle(.borderless) // 移除按钮默认样式
// 可编辑任务标题(完成时划线)
TextField("未命名任务", text: $item.title)
.strikethrough(item.isCompleted)
.foregroundColor(item.isCompleted ? .gray : .primary)
}
}
.onDelete(perform: deleteTodo) // 滑动删除
.onMove(perform: reorderTodo) // 拖动排序
}
.navigationTitle("Todo List")
.toolbar {
EditButton() // 编辑按钮(开启删除/排序模式)
}
// 底部输入栏(用safeAreaInset避免被键盘遮挡)
.safeAreaInset(edge: .bottom) {
HStack(spacing: 10) {
TextField("输入新任务", text: $newTodoTitle)
.textFieldStyle(.roundedBorder)
.padding(.horizontal)
Button("添加") {
guard !newTodoTitle.isEmpty else { return } // 避免空任务
let newItem = TodoItem(title: newTodoTitle, isCompleted: false)
todoItems.append(newItem)
newTodoTitle = "" // 清空输入框
}
.padding(.trailing)
.disabled(newTodoTitle.isEmpty) // 空内容时禁用按钮
}
.padding(.vertical, 8)
.background(Color(.systemBackground)) // 背景色与界面一致
}
}
}
// 删除任务逻辑
func deleteTodo(at offsets: IndexSet) {
todoItems.remove(atOffsets: offsets)
}
// 重新排序逻辑
func reorderTodo(from source: IndexSet, to destination: Int) {
todoItems.move(fromOffsets: source, toOffset: destination)
}
}
3. 运行效果与优化
- 添加任务:在底部输入栏输入文字,点击“添加”按钮,任务会自动出现在列表顶部。
- 标记完成:点击复选框,任务标题会划灰线,表示已完成。
- 编辑任务:直接点击任务标题即可修改内容(TextField自动激活)。
- 删除/排序:点击右上角“编辑”按钮,可滑动删除或拖动排序。
4. 扩展功能(可选)
- 添加“全部标记完成”按钮:在导航栏添加
ToolbarItem
,点击时将所有todoItems
的isCompleted
设为true
。 - 分类筛选:添加
Picker
选择“全部/未完成/已完成”,过滤列表显示。 - 数据持久化:用
UserDefaults
或Core Data
保存任务(避免重启应用丢失数据)。
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/258