onAppear 当出现时

onAppear 修饰器跟 UIKit 里的 viewDidAppear 非常相似。即当视图出现在画面时,自动调用后面的程序

//例子1: 直接在后面写代码
List{
	...
}
.onAppear {            
    self.selectedOrder = self.settingStore.displayOrder
}

//例子2: 用perform传入一个函数,并不是实作,所以函数不需要跟括号
.onAppear(perform: startGame)

<aside> 💡 onAppear 无法用到异步函数上,这时需要改用 Task{ … } ,参照下面:.task 异步任务

</aside>


onDisappear 当消失时

该修饰符使用方法和 onAppear 一样。

struct ContentView: View {
    var body: some View {
        NavigationStack {
            VStack {
                NavigationLink {
                    DetailView()
                } label: {
                    Text("Hello World")
                }
            }
            .onAppear {
                print("ContentView appeared!")
            }
            .onDisappear {
                print("ContentView disappeared!")
            }
        }
    }
}

struct DetailView: View {
    var body: some View {
        VStack {
            Text("Second View")
        }
        .onAppear {
            print("DetailView appeared!")
        }
        .onDisappear {
            print("DetailView disappeared!")
        }
    }
}

onChange 当改变时

在某属性使用了 @State 包裹器时,如果是通过 “属性绑定” 的方式与控件进行绑定,那在控件改变时,它实际上是绕过 setter ,直接改变内部存储的实际值,因此它不会触发 didSet 观察属性。既然我们无法使用属性观察器 didset 检测 @State 属性何时发生更改,这种情况下就要改用 onChange 。参见:3. 属性绑定不会触发观察

SwiftUI 允许我们将 onChange() 修饰符附加到任何视图,当程序中的某些状态发生变化时,它将运行我们选择的代码。此行为在 iOS 17 及更高版本中发生变化,旧行为已被弃用。

<aside> 💡 建议:onChange() 可以附加到视图层次结构中的任何位置,但最好将其放在实际发生变化的内容附近。

</aside>

iOS 16 及更早版本

如果需要面向 iOS 16 及更早版本, onChange() 接受一个观察的参数,并将观察到的 newValue 发送到你的闭包中。例如:

struct ContentView: View {
    @State private var name = ""
    var body: some View {
        TextField("Enter your name:", text: $name)
            .textFieldStyle(.roundedBorder)
            .onChange(of: name) { newValue in
                print("Name changed to \\(name)!")
            }
    }
}

iOS 17 及更高版本

如果您的目标是 iOS 17 或更高版本,则有几种写法:

闭包不接受参数:(of: , 函数 )

有时只想在值更改时运行某个函数,但实际上并不关心新值是什么,那可以不写参数;

.onChange(of: name, updateCode)
// 后面的 updateCode 是一个方法

闭包接受两个参数:(of: ) { oldValue, newValue }

在特定值发生变化时运行函数,SwiftUI 会自动将 “旧值 oldValue” 和 “新值 newValue” 传递给您附加的任何函数;

// 现在,代码将在滑块更改时正确打印出值,因为 onChange() 正在观察它。这意味着您可以在 onChange() 函数内执行任何您想要的操作:
// 您可以调用方法、运行算法来确定如何应用更改,或者您可能需要的任何其他操作。
// 请注意大多数其他内容保持不变:我们仍然使用 @State private var 来声明 blurAmount 属性
Slider(value: $blurAmount, in: 0...20)
	.onChange(of: blurAmount) { oldValue, newValue in
			print("New value is \\(newValue)")
	}

首次显示也算改变:initial: true

使用 initial: true 可以指定在首次显示视图时是否应运行操作闭包。等于一次性把 onAppear 的活也干了。

struct ContentView: View {
    @State private var name = ""
    var body: some View {
        TextField("Enter your name", text: $name)
            .onChange(of: name, initial: true) {
                print("Name is now \\(name)")
            }
    }
}

onChange 与 Binding 结合使用

还有一种做法是向 Binding 添加自定义扩展,以便我将观察代码直接附加到绑定而不是视图。它允许我将观察者放置在它正在观察的事物旁边,而不是有很多在我看来, onChange() 修饰符附加在其他地方。

// 创建 Binding 的扩展
// 扩展里定义了 onChange 方法,该方法接受一个方法作为参数,同时返回一个 Binding 对象
extension Binding {
    func onChange(_ handler: @escaping (Value) -> Void) -> Binding<Value> {
        Binding(
            get: { self.wrappedValue },
            set: { newValue in
                self.wrappedValue = newValue
                handler(newValue)
            }
        )
    }
}

// 使用时:
struct ContentView: View {
    @State private var name = ""
		// 这样就可以在需要绑定的地方,使用 Binding 的静态方法 onChange,接受 nameChange 方法参数,并返回新的 Binding
		// 这样可以在绑定的同时,也执行一些额外的命令
    var body: some View {
        TextField("Enter your name:", text: $name.onChange(nameChanged))
            .textFieldStyle(.roundedBorder)
    }
    func nameChanged(to value: String) {
        print("Name changed to \\(name)!")
    }
}

<aside> 💡 虽然可以这样做,但最好使用 Instruments 运行代码检查;因为在视图上使用 onChange() 比将其添加到绑定中,性能更高。

</aside>


.task 异步任务

由于任务是异步执行的,因此这是为视图获取一些初始网络数据的好地方。例如,如果我们想从服务器获取消息列表,将其解码为 Message 结构数组,然后将其显示在列表中,我们可能会编写如下内容:

struct Message: Decodable, Identifiable {
    let id: Int
    let from: String
    let text: String
}

struct ContentView: View {
    @State private var messages = [Message]()
    var body: some View {
        NavigationStack {
            List(messages) { message in
                VStack(alignment: .leading) {
                    Text(message.from)
                        .font(.headline)
                    Text(message.text)
                }
            }
            .navigationTitle("Inbox")
        }
        .task {
            do {
                let url = URL(string: "<https://www.hackingwithswift.com/samples/messages.json>")!
                let (data, _) = try await URLSession.shared.data(from: url)
                messages = try JSONDecoder().decode([Message].self, from: data)
            } catch {
                messages = []
            }
        }
    }
}
// 例如:创建一个简单的网站源代码查看器,用户可以选择要检查的网站:
// task() 修饰符附加到:由导航推送而呈现的子视图中,它只有在子视图显示时,才真正执行

struct ContentView: View {
    let sites = ["Apple.com", "HackingWithSwift.com", "Swift.org"]
    var body: some View {
        NavigationStack {
            List(sites, id: \\.self) { site in
                NavigationLink(site) {
                    SourceViewer(site: site)
                }
            }
            .navigationTitle("View Source")
        }
    }
}

struct SourceViewer: View {
    let site: String
    @State private var sourceCode = "Loading…"

    var body: some View {
        ScrollView {
            Text(sourceCode)
                .font(.system(.body, design: .monospaced))
        }
        .task {
            guard let url = URL(string: "https://\\(site)") else {
                return
            }
            do {
                let (data, _) = try await URLSession.shared.data(from: url)
                sourceCode = String(decoding: data, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines)
            } catch {
                sourceCode = "Failed to fetch site."
            }
        }
    }
}