import SwiftUI
struct ElapsedTime: View {
@State var fruits = ["Apple", "Banana", "Orange", "Orange", "Apple"]
var body: some View {
List(fruits, id: \.self) { fruit in
Text(fruit)
}
Button("Remove"){
fruits.remove(at: 3)
}
Divider()
Button("Insert"){
fruits.insert("Watermelon", at: 2)
}
}
}
#Preview {
ElapsedTime()
}ScrollView {
VStack {
...
}
}| ScrollView | List |
|---|---|
| 自己负责布局 | 系统负责布局 |
| 不会自动复用 | 会自动复用 Cell |
| 没有滑动删除 | 支持删除、移动 |
| 没有 Section | 支持 Section |
| 更自由 | 更适合数据列表 |
List SwiftUI 只会创建 屏幕可见区域 附近的 Row。
List {
ForEach(0..<100) { i in
Text("\(i)")
}
}支持混合内容
List {
Text("Header")
ForEach(["a", "b", "c", "d", "e"], id: \.self) { ch in
Text("\(ch)")
}
Text("Footer")
}并不是 0-99 一次全部执行,只有真正出现在屏幕上的Row才会调用
.onAppear
import SwiftUI
struct Row: View {
let id: Int
var body: some View {
Text("\(id)")
.onAppear {
print("Appear", id)
}
}
}
struct ElapsedTime: View {
var body: some View {
List(0..<100) {
Row(id: $0)
}
}
}
#Preview {
ElapsedTime()
}这是所有 View 都有的 Modifier
import SwiftUI
struct Row: View {
let id: Int
var body: some View {
Text("\(id)")
.onAppear {
print("Appear", id)
}
.onDisappear {
print("Disappear", id)
}
}
}
struct ElapsedTime: View {
var body: some View {
List(0..<100) {
Row(id: $0)
}
Text("Hello")
.onAppear {
print("出现")
}
}
}
#Preview {
ElapsedTime()
}SwiftUI 是 View 出现在 View Tree 中就可能触发,不是
页面显示一次。每次离开屏幕再进入 每次都会执行,所以
.onAppear 不是只执行一次。
List(viewModel.items) { item in
Text(item.name)
.onAppear {
if item == viewModel.items.last {
viewModel.loadMore()
}
}
}流程,SwiftUI中实现分页加载最常见的方式之一
用户滑动 -> 最后一个 Cell 出现 ->
onAppear -> loadMore -> 追加数据
-> List 更新View 出现
->
启动异步 Task
->
SwiftUI 管理声明周期当 View 出现时,自动启动一个异步任务(async Task),并在 View 消失时自动管理这个任务。
可以理解成,但是实际长比这种写法更智能
.onAppear {
Task {
await loadData()
}
}为什么有 .task,假设你需要请求网络,在 Swift Concurrency 出现之前 通常会写
.onAppear {
loadData()
}如果现在 loadData 是异步函数
func loadData() async {
...
}
// 你不能直接写
.onAppear {
await loadData() // ❌ 编译错误
}
// .onAppear 的闭包不是 async
// 只能这样
.onAppear {
Task {
await loadData()
}
}
// 于是 SwiftUI 提供了
.task {
await loadData()
}import SwiftUI
struct ElapsedTime: View {
var body: some View {
List {
Text("Apple")
Text("Banana")
Text("Orange")
}.listStyle(.plain)
}
}
#Preview {
ElapsedTime()
}ListStyle 可以决定
List {
...
}
.listStyle(.automatic).automatic 系统自己决定.plain 是最普通的列表,没圆角 没 grouped
Header简单.grouped 经典IOS设置页面.inset 比grouped更现代.insetGrouped 苹果现在最喜欢的风格.sidebar 用于侧边栏import SwiftUI
struct ElapsedTime: View {
var body: some View {
List {
Text("Apple")
Text("Banana")
Text("Orange")
}.listStyle(.plain)
List {
Section("Fruit") {
Text("Apple")
Text("Banana")
}
Section("Drink") {
Text("Tea")
}
}
.listStyle(.grouped)
}
}
#Preview {
ElapsedTime()
}.listRowSeparator(_:) 是 SwiftUI 中控制 List
每一行分割线(Separator)是否显示以及显示位置 的 Modifier
.listRowSeparator(.visible)
// 或
.listRowSeparator(.hidden)例子
List {
Text("Apple")
.listRowSeparator(.hidden)
Text("Banana")
.listRowSeparator(.hidden)
Text("Orange")
.listRowSeparator(.hidden)
}List Item 背景设置
import SwiftUI
struct ElapsedTime: View {
var body: some View {
List {
Text("Apple")
.listRowSeparator(.hidden)
.listRowBackground(Color.red)
Text("Banana")
.listRowSeparator(.hidden)
Text("Orange")
.listRowSeparator(.hidden)
}
}
}
#Preview {
ElapsedTime()
}.listRowBackground(RoundedRectangle(cornerRadius: 10).foregroundStyle(.yellow).padding(5))Section 是 SwiftUI 中 用于将内容分组(Grouping) 的容器,最常见于 List 和 Form 中。
一个 List 可以包含多个 Section,每个 Section 又包含若干个 View。
import SwiftUI
struct ElapsedTime: View {
var body: some View {
List {
Section {
Text("Apple")
Text("Banana")
}
Section("动物") {
Text("Cat")
Text("Dog")
}
Section(
header: Text("国家"),
footer: Text("即将支持更多国家")
) {
Text("USA")
Text("China")
}
// Header 可以是任何 View
Section {
Text("Apple")
Text("Banana")
} header: {
HStack {
Image(systemName: "leaf.fill")
Text("水果")
}
}
}
}
}
#Preview {
ElapsedTime()
}Section 主要有三个用途:
虽然 Section 本身不会折叠,DisclosureGroup 像可折叠的 Section
List {
DisclosureGroup("水果") {
Text("Apple")
Text("Banana")
}
}State + Section 自己维护 类似的
import SwiftUI
struct ElapsedTime: View {
@State private var expanded = true
var body: some View {
List {
Section {
if expanded {
Text("Apple")
Text("Banana")
}
} header: {
Button("水果") {
expanded.toggle()
}
}
}
}
}
#Preview {
ElapsedTime()
}NavigationStack 是 SwiftUI 新一代导航器,而 NavigationLink 是触发导航的控件。
import SwiftUI
struct DetailView: View {
let content: String
var body: some View {
Text("\(self.content)")
}
}
struct ElapsedTime: View {
var body: some View {
NavigationStack {
NavigationLink {
Text("进入详情1")
} label: {
DetailView(content: "页面详情1")
}
Divider()
NavigationStack {
NavigationLink("进入详情2") {
DetailView(content: "页面详情2")
}
}
}
}
}
#Preview {
ElapsedTime()
}最经典
NavigationLink("下一页") {
DetailView()
}等价于
NavigationLink(destination: DetailView()) {
Text("下一页")
}label 可以放任何 View
NavigationLink {
DetailView()
} label: {
HStack {
Image(systemName: "star.fill")
Text("详情")
}
}NavigationStack {
List {
NavigationLink("第一页") {
Page1()
}
NavigationLink("第二页") {
Page2()
}
NavigationLink("第三页") {
Page3()
}
}
}点击哪个,就 push 哪个。
NavigationStack 不只是 push View。可以 push 数据。
struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Button("进入详情") {
path.append("Apple")
}
.navigationDestination(for: String.self) { value in
DetailView(name: value)
}
}
}
}这里不是 push 一个 View,而是 push 一个 String,SwiftUI 根据
.navigationDestination(for: String.self) 自动生成页面。
这就是 数据驱动导航 Data-driven Navigation。
配合 NavigationPath,NavigationLink 也可以不直接指定目标页面。
import SwiftUI
struct DetailView: View {
let content: String
var body: some View {
Text("\(self.content)")
}
}
struct ElapsedTime: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
NavigationLink("点击查看详情", value: "详情页面内容")
.navigationDestination(for: String.self) { value in
DetailView(content: value)
}
}
}
}
#Preview {
ElapsedTime()
}除了点击,还可以代码控制
import SwiftUI
struct DetailView: View {
let content: String
@Binding var path:NavigationPath
var body: some View {
Text("\(self.content)")
Button("进一步详情"){
path.append("\(content)o")
}
Button("返回"){
path.removeLast()
}
Button("返回首页"){
// clear Stack
path.removeLast(path.count)
}
}
}
struct ElapsedTime: View {
@State private var path:NavigationPath = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
NavigationLink("点击查看详情", value: "o")
.navigationDestination(for: String.self) { value in
DetailView(content: value, path: $path)
}
}
}
}
#Preview {
ElapsedTime()
}| 组件 | 作用 | 类比 UIKit |
|---|---|---|
NavigationStack |
管理整个导航栈,保存页面历史 | UINavigationController |
NavigationLink |
用户点击后触发导航 | pushViewController 的入口 |
NavigationPath |
保存导航路径(数据而非视图) | 导航控制器维护的控制器栈(但以数据建模) |
.navigationDestination(for:) |
根据数据类型生成目标页面 | 集中式路由/目标控制器映射 |
NavigationLink(value:) |
通过值驱动导航 | 将导航意图交给 NavigationStack 解析 |
List 样例
import SwiftUI
struct Todo: Identifiable {
let id = UUID()
var title: String
var done: Bool
}
struct ElapsedTime: View {
@State private var todos = [
Todo(title: "Apple", done: false),
Todo(title: "Orange", done: true)
]
var body: some View {
List($todos) { $todo in
Toggle("\(todo.title) \(String(todo.done))", isOn: $todo.done)
}
}
}
#Preview {
ElapsedTime()
}ForEach 同样支持
List {
ForEach($todos) { $todo in
Toggle(todo.title, isOn: $todo.done)
}
}在 #Preview 中创建State变量
import SwiftUI
struct Todo: Identifiable {
let id = UUID()
var title: String
var done: Bool
}
struct ElapsedTime: View {
@Binding var todos:[Todo]
var body: some View {
List($todos) { $todo in
Toggle("\(todo.title) \(String(todo.done))", isOn: $todo.done)
}
}
}
#Preview {
@Previewable @State var todos = [
Todo(title: "Apple", done: false),
Todo(title: "Orange", done: true)
]
ElapsedTime(todos: $todos)
}SwiftUI 自动完成
import SwiftUI
struct Item: Identifiable {
let id = UUID()
var name: String
}
struct ElapsedTime: View {
@State private var items = [
Item(name: "Apple"),
Item(name: "Banana"),
Item(name: "Orange")
]
var body: some View {
NavigationStack {
List(
$items,
editActions: [.delete, .move]
) { $item in
TextField("Name", text: $item.name)
}
.toolbar {
EditButton()
}
}
}
}
#Preview {
ElapsedTime()
}.toolbar 是 SwiftUI
用来给当前界面添加工具栏(Toolbar)内容的修饰器。它统一了导航栏按钮、底部工具栏、键盘工具栏等各种工具栏。
基本语法
.toolbar {
ToolbarItem {
Button("编辑") {
print("编辑")
}
}
}通常配合 NavigationStack
import SwiftUI
struct Item: Identifiable {
let id = UUID()
var name: String
}
struct ElapsedTime: View {
@State private var items = [
Item(name: "Apple"),
Item(name: "Banana"),
Item(name: "Orange")
]
var body: some View {
NavigationStack {
List(
$items,
editActions: [.delete, .move]
) { $item in
TextField("Name", text: $item.name)
}
.navigationTitle("List Demo")
.toolbar {
ToolbarItem {
EditButton()
}
ToolbarItem {
Button("测试按钮") {
print("测试按钮被点击")
}
}
}
}
}
}
#Preview {
ElapsedTime()
}import SwiftUI
struct ElapsedTime: View {
@State private var count = 0
var body: some View {
NavigationStack {
VStack(spacing: 20) {
Text("Count: \(count)")
.font(.largeTitle)
}
.navigationTitle("Toolbar Demo")
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("重置") {
count = 0
}
}
ToolbarItem(placement: .topBarTrailing) {
Button {
count += 1
} label: {
Image(systemName: "plus")
}
}
ToolbarItem(placement: .bottomBar) {
Button("减少") {
count -= 1
}
}
ToolbarItem(placement: .status) {
Text("当前值:\(count)")
}
}
}
}
}
#Preview {
ElapsedTime()
}多个按钮可以放一起
.toolbar {
ToolbarItemGroup(placement: .topBarTrailing) {
Button {
} label: {
Image(systemName: "square.and.arrow.up")
}
Button {
} label: {
Image(systemName: "plus")
}
}
}class 的 Model 用 @Observable
@Observable class CodeBreaker {
...
}作为 State 传递时,就不用像 struct 一样 $ Binding
了。因为 class 实例是引用类型。
import SwiftUI
struct ElapsedTime: View {
@State private var fruits = [
"Apple",
"Banana",
"Orange",
"Pear"
]
var body: some View {
NavigationStack {
List {
ForEach(fruits, id: \.self) { fruit in
Text(fruit)
}
.onDelete(perform: delete)
.onMove(perform: move)
}
.toolbar {
EditButton()
}
}
}
func delete(at offsets: IndexSet) {
fruits.remove(atOffsets: offsets)
}
func move(from source: IndexSet, to destination: Int) {
fruits.move(fromOffsets: source, toOffset: destination)
}
}
#Preview {
ElapsedTime()
}