onAppear
会在视图首次出现在屏幕上或被添加到视图层级中时触发。
// 例子1: 直接在后面写代码
List{
...
}
.onAppear {
self.selectedOrder = self.settingStore.displayOrder
}
// 例子2: 用perform传入一个函数,并不是实作,所以函数不需要跟括号
.onAppear(perform: startGame)
<aside>
💡 onAppear 中无法添加异步函数,这时需要改用 Task{ … }
,参照:.task 异步任务
</aside>
onDisappear
会在视图从屏幕上消失或从视图层级中移除时触发。
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!")
}
}
}
TabView
切换到其他标签,前一个标签页的根视图会触发 onDisappear
,新显示的标签页的根视图会触发 onAppear
NavigationLink
推入新页面时,原视图会触发 onDisappear
。当从新视图返回时,原视图会触发 onAppear
<aside> 💡
但这里要非常注意 onAppear
和 onDisappear
添加的位置,在 NavigationStack
中通过 NavigationLink
进入下一级页面,它会清空原先导航中的所有内容,但是如果 onAppear
和 onDisappear
修饰符添加的位置是 NavigationStack
下面,那就不会触发。因为这个导航堆栈一直都在,它属于容器,只是控制其里面的内容变化而已。
NavigationStack
, TabView
这类容器视图,它们的 onAppear
和 onDisappear
通常与它们自身被添加到视图层级或从视图层级移除相关。当它们内部的内容发生改变时(例如 NavigationStack
推入新视图,或 TabView
切换标签),是这些内部的、代表具体页面或标签内容的视图会经历 onAppear
和 onDisappear
。
</aside>
<aside> 💡
仅仅被另一个视图(如 sheet、alert、overlay、键盘)覆盖,通常不足以让原视图触发 onDisappear
,除非这个覆盖机制确实替换了原视图在当前上下文中的主导地位。
</aside>
.fullScreenCover()
出现时,完全覆盖了原视图,但不会导致原视图触发 onDisappear
。同理当它关闭时,原视图也不会触发 onAppear
.sheet()
(无论大小)弹出时,原视图不会触发 onDisappear
。原视图仍然是视图层级的一部分,只是被覆盖了。同理当它被关闭时,原视图也不会触发 onAppear
(因为它一直都在)confirmationDialog
提示框弹出时,该呈现视图通常不会触发 onDisappear
。它仍然是视图层级的一部分.overlay()
覆盖层,是在原视图之上添加一层内容,原视图本身并未从视图层级中移除或消失,因此不会触发 onDisappear
ZStack
中的视图是层叠的。即使一个视图被另一个视图部分或完全遮挡,只要它仍然是 ZStack
的一部分,它就不会触发 onDisappear
。只有当它从 ZStack
的子视图中被移除时才会触发overlay
或在 ZStack
中较高层级实现的话,那么它下面的视图不会触发 onDisappear
onDisappear
onDisappear
。SwiftUI 会调整视图布局以适应键盘(如果配置正确),但视图本身并未“消失”在某属性使用了 @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."
}
}
}
}