onAppear 修饰器跟 UIKit 里的 viewDidAppear 非常相似。即当视图出现在画面时,自动调用后面的程序
//例子1: 直接在后面写代码
List{
...
}
.onAppear {
self.selectedOrder = self.settingStore.displayOrder
}
//例子2: 用perform传入一个函数,并不是实作,所以函数不需要跟括号
.onAppear(perform: startGame)
<aside>
💡 onAppear 无法用到异步函数上,这时需要改用 Task{ … }
,参照下面:.task 异步任务
</aside>
该修饰符使用方法和 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!")
}
}
}
在某属性使用了 @State 包裹器时,如果是通过 “属性绑定” 的方式与控件进行绑定,那在控件改变时,它实际上是绕过 setter ,直接改变内部存储的实际值,因此它不会触发 didSet 观察属性。既然我们无法使用属性观察器 didset
检测 @State
属性何时发生更改,这种情况下就要改用 onChange
。参见:3. 属性绑定不会触发观察
SwiftUI 允许我们将 onChange()
修饰符附加到任何视图,当程序中的某些状态发生变化时,它将运行我们选择的代码。此行为在 iOS 17 及更高版本中发生变化,旧行为已被弃用。
<aside> 💡 建议:onChange() 可以附加到视图层次结构中的任何位置,但最好将其放在实际发生变化的内容附近。
</aside>
如果需要面向 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 或更高版本,则有几种写法:
有时只想在值更改时运行某个函数,但实际上并不关心新值是什么,那可以不写参数;
.onChange(of: name, updateCode)
// 后面的 updateCode 是一个方法
在特定值发生变化时运行函数,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
可以指定在首次显示视图时是否应运行操作闭包。等于一次性把 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)")
}
}
}
还有一种做法是向 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()
修饰符是 onAppear()
的更强大版本,允许我们在视图显示后立即开始异步工作,onAppear 无法用到异步函数上task()
修饰符更好的是:当视图被销毁时,如果任务尚未完成,任务将自动取消。task()
修饰符附加到层次结构中的任何视图,甚至是由于导航推送而呈现的视图,它只会在显示视图时才真正工作task()
和 onAppear()
都能够运行同步函数,因此选哪个都行;但一般 onAppear()
和 onDisappear()
会一起使用task()
创建的任务,默认以最高可用优先级运行;如果该任务不重要,你可以自定义优先级参数 .task priority: .low)
由于任务是异步执行的,因此这是为视图获取一些初始网络数据的好地方。例如,如果我们想从服务器获取消息列表,将其解码为 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."
}
}
}
}