第13章:SwiftUI 基础 - 声明式 UI
13.1 SwiftUI 是什么?
SwiftUI 是 Apple 在 2019 年推出的声明式 UI 框架,用来替代 UIKit。它的核心理念是:
**告诉 SwiftUI "你想要什么界面",而不是 "怎么创建界面"。 **
对比:
-
**UIKit(命令式) **:创建视图 → 添加到父视图 → 设置约束 → 手动更新
-
**SwiftUI(声明式) **:描述界面状态,SwiftUI 自动处理更新
13.2 你的第一个 SwiftUI 视图
在 Xcode 中创建 SwiftUI 项目,替换 ContentView.swift:
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.font(.largeTitle)
.foregroundColor(.blue)
.padding()
}
}
#Preview {
ContentView()
}
**关键概念: **
-
View协议:所有 UI 元素都遵循这个协议 -
body:计算属性,返回视图层级 -
修饰符(modifier):用
.链式调用,修改视图样式
13.3 状态管理 - @State
SwiftUI 中,视图是函数:输入相同的状态,输出相同的界面。
struct CounterView: View {
// @State 标记可变状态
@State private var count = 0
var body: some View {
VStack(spacing: 20) {
Text("Count: \(count)")
.font(.largeTitle)
Button("Increment") {
count += 1 // 修改状态,自动刷新界面
}
.buttonStyle(.borderedProminent)
}
}
}
** @State 的特点: **
-
用于视图内部状态
-
SwiftUI 自动管理存储
-
值改变时自动重绘视图
13.4 布局系统 - Stacks
struct LayoutDemoView: View {
var body: some View {
VStack(spacing: 20) { // 垂直堆栈
Text("上方")
HStack(spacing: 20) { // 水平堆栈
Text("左")
.frame(maxWidth: .infinity)
.background(Color.red.opacity(0.3))
Text("中")
.frame(maxWidth: .infinity)
.background(Color.green.opacity(0.3))
Text("右")
.frame(maxWidth: .infinity)
.background(Color.blue.opacity(0.3))
}
ZStack { // 重叠堆栈
Circle()
.fill(Color.yellow)
.frame(width: 100, height: 100)
Text("Z")
.font(.largeTitle)
}
Text("下方")
}
.padding()
}
}
**三种 Stack: **
-
VStack- 垂直排列 -
HStack- 水平排列 -
ZStack- 前后叠加
13.5 常用控件
struct ControlsView: View {
@State private var text = ""
@State private var isOn = false
@State private var sliderValue = 50.0
@State private var selectedDate = Date()
@State private var selectedColor = Color.red
var body: some View {
Form { // 表单,自动处理滚动和布局
Section("输入") {
TextField("请输入", text: $text)
SecureField("密码", text: $text) // 密码输入
TextEditor(text: $text) // 多行文本
}
Section("选择") {
Toggle("开关", isOn: $isOn)
Slider(value: $sliderValue, in: 0...100) {
Text("滑动条")
}
DatePicker("日期", selection: $selectedDate)
ColorPicker("颜色", selection: $selectedColor)
}
Section("按钮") {
Button("普通按钮") {}
Button(action: {}) {
Label("带图标的按钮", systemImage: "star.fill")
}
Button("主要按钮") {}
.buttonStyle(.borderedProminent)
Button("胶囊按钮") {}
.buttonStyle(.bordered)
.controlSize(.large)
.tint(.green)
}
}
}
}
注意 **$** 符号: $text 是 text 的绑定(Binding),双向数据流。
13.6 列表与导航
// 数据模型
struct Restaurant: Identifiable {
let id = UUID()
let name: String
let cuisine: String
let rating: Double
}
struct RestaurantRow: View {
let restaurant: Restaurant
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(restaurant.name)
.font(.headline)
Text(restaurant.cuisine)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
HStack {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
Text(String(format: "%.1f", restaurant.rating))
}
}
}
}
struct RestaurantListView: View {
let restaurants = [
Restaurant(name: "川味轩", cuisine: "川菜", rating: 4.5),
Restaurant(name: "金鼎轩", cuisine: "粤菜", rating: 4.2),
Restaurant(name: "日料屋", cuisine: "日料", rating: 4.8)
]
var body: some View {
NavigationView {
List(restaurants) { restaurant in
NavigationLink(destination: RestaurantDetailView(restaurant: restaurant)) {
RestaurantRow(restaurant: restaurant)
}
}
.navigationTitle("餐厅列表")
}
}
}
struct RestaurantDetailView: View {
let restaurant: Restaurant
var body: some View {
VStack(spacing: 20) {
Image(systemName: "fork.knife.circle.fill")
.resizable()
.frame(width: 100, height: 100)
.foregroundColor(.orange)
Text(restaurant.name)
.font(.largeTitle)
Text(restaurant.cuisine)
.font(.title2)
.foregroundColor(.secondary)
HStack {
ForEach(0..<Int(restaurant.rating), id: \.self) { _ in
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
}
Spacer()
}
.padding()
.navigationTitle("详情")
}
}
**关键概念: **
-
Identifiable协议:让数据可以被列表唯一标识 -
List:自动处理行、分隔线、滑动删除 -
NavigationView+NavigationLink:页面跳转
13.7 数据绑定 - @Binding
当子视图需要修改父视图的状态时:
// 子视图
struct ToggleView: View {
@Binding var isOn: Bool // 绑定,不是自己的状态
var body: some View {
Toggle("开关", isOn: $isOn)
}
}
// 父视图
struct ParentView: View {
@State private var lightOn = false
var body: some View {
ToggleView(isOn: $lightOn) // 传递绑定
}
}
13.8 可观察对象 - @StateObject 和 @ObservedObject
对于复杂数据模型,使用 ObservableObject:
import Combine
class TaskStore: ObservableObject {
@Published var tasks: [Task] = []
@Published var filter: TaskFilter = .all
var filteredTasks: [Task] {
switch filter {
case .all: return tasks
case .active: return tasks.filter { !$0.isCompleted }
case .completed: return tasks.filter { $0.isCompleted }
}
}
func addTask(_ title: String) {
tasks.append(Task(title: title))
}
func toggleTask(_ task: Task) {
if let index = tasks.firstIndex(where: { $0.id == task.id }) {
tasks[index].isCompleted.toggle()
}
}
}
struct TaskListView: View {
@StateObject private var store = TaskStore() // 创建可观察对象
var body: some View {
List {
ForEach(store.filteredTasks) { task in
TaskRow(task: task) {
store.toggleTask(task)
}
}
}
}
}
** @StateObject vs @ObservedObject: **
-
@StateObject:创建并持有对象(用这个视图创建的数据) -
@ObservedObject:引用外部传入的对象
13.9 环境值 - @Environment
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
var body: some View {
VStack {
Text(colorScheme == .dark ? "Dark Mode" : "Light Mode")
Button("关闭") {
dismiss() // 关闭当前页面
}
}
}
}
第14章:SwiftUI 进阶
14.1 动画
SwiftUI 动画非常简单,只需添加 .animation:
struct AnimationView: View {
@State private var isExpanded = false
var body: some View {
VStack {
RoundedRectangle(cornerRadius: isExpanded ? 50 : 10)
.fill(isExpanded ? Color.blue : Color.red)
.frame(
width: isExpanded ? 300 : 100,
height: isExpanded ? 300 : 100
)
.animation(.spring(response: 0.3, dampingFraction: 0.5), value: isExpanded)
Button("切换") {
isExpanded.toggle()
}
}
}
}
**动画类型: **
-
.default- 默认动画 -
.linear- 线性 -
.easeIn/.easeOut/.easeInOut- 缓动 -
.spring()- 弹性动画 -
.interactiveSpring()- 交互式弹性
14.2 手势
struct GestureView: View {
@State private var offset: CGSize = .zero
@State private var scale: CGFloat = 1.0
@State private var rotation: Angle = .zero
var body: some View {
Image(systemName: "star.fill")
.font(.system(size: 100))
.foregroundColor(.yellow)
.offset(offset)
.scaleEffect(scale)
.rotationEffect(rotation)
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
}
.onEnded { _ in
withAnimation {
offset = .zero
}
}
)
.gesture(
MagnificationGesture()
.onChanged { scale = $0 }
)
.gesture(
RotationGesture()
.onChanged { rotation = $0 }
)
}
}
14.3 数据持久化
import SwiftUI
struct TodoItem: Identifiable, Codable {
let id: UUID
var title: String
var isCompleted: Bool
var createdAt: Date
}
class TodoStore: ObservableObject {
@Published var todos: [TodoItem] = []
private let saveKey = "todos"
init() {
load()
}
func add(_ title: String) {
let todo = TodoItem(
id: UUID(),
title: title,
isCompleted: false,
createdAt: Date()
)
todos.append(todo)
save()
}
func toggle(_ todo: TodoItem) {
if let index = todos.firstIndex(where: { $0.id == todo.id }) {
todos[index].isCompleted.toggle()
save()
}
}
func delete(at offsets: IndexSet) {
todos.remove(atOffsets: offsets)
save()
}
// UserDefaults 存储
private func save() {
if let encoded = try? JSONEncoder().encode(todos) {
UserDefaults.standard.set(encoded, forKey: saveKey)
}
}
private func load() {
if let data = UserDefaults.standard.data(forKey: saveKey),
let decoded = try? JSONDecoder().decode([TodoItem].self, from: data) {
todos = decoded
}
}
}