SwiftUI iOS开发指南:从基础布局到复杂交互实战

SwiftUI的核心概念:View与修饰器

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

SwiftUI iOS开发指南:从基础布局到复杂交互实战

修饰器(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的UIStackViewCALayer

以登录界面为例,用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内部的可变状态(比如开关的开启状态、滑块的位置)。它是一个属性包装器,会将值类型(如BoolString)转换为“可观察对象”——当值变化时,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能修改父ViewisDarkMode状态。

数据驱动:List与ForEach的动态列表构建

iOS应用中最常见的界面是“动态列表”(如联系人、商品列表),SwiftUI用ListForEach实现这一需求——数据变化时,列表自动更新

基础用法:静态列表

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)
}

上述代码中,$fruitsForEach获得Binding<Fruit>,从而允许修改列表项的内容;onDeleteonMove则实现了“滑动删除”和“拖动排序”的核心功能——无需像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,点击时将所有todoItemsisCompleted设为true
  • 分类筛选:添加Picker选择“全部/未完成/已完成”,过滤列表显示。
  • 数据持久化:用UserDefaultsCore Data保存任务(避免重启应用丢失数据)。

原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/258

(0)

相关推荐