很多时候我们需要在 View 初始化的时候设置一些状态,只要设置一次。和UIKit 中的 viewDidLoad 相似。然而因为 SwiftUI 的设计理念不同,SwiftUI View 中的生命周期只提供了 onAppear,没有类似 onFirstAppear 的方法。
当前提供的 onAppear 并不能满足我们在一些场景的需求,因为 onAppear 会在页面每次重新出现的时候都执行,不适合只要在页面初始化第一次的时候赋值的场景。
比如下面的例子:
swift
struct ContentView: View {
@State private var greeting: String = "Initial"
var body: some View {
NavigationStack {
VStack {
Text(greeting)
.onTapGesture {
greeting = "点击"
}
NavigationLink(destination: Text("Pushed Text")) {
Text("Push a View")
}
.padding()
}
.onAppear {
greeting = "初始化"
}
}
}
}
有一个标签在页面刚出现的时候为"初始化",如果用户点击了之后,标签的文案改为"点击"。在目前的生命周期事件里,只能把设置初始化值的代码写在 onAppear 里。
但是这样会引发什么问题呢?如果我们点击 NavigationLink 跳转到下一个页面,然后返回到当前页面,标签的值被重置为"初始化"了。其实我们是希望它保持"点击"的状态了。但是如果使用 onAppear 就会不可避免产生这样的问题。
那能不能在 View 的初始化方法中赋值呢?比如这样:
swift
struct FirstAppearView: View {
@State private var greeting: String = "Initial"
init(greeting: String) {
self.greeting = greeting
}
}
如果 FirstAppearView 整个应用生命周期只被使用一次,以上的代码是成立的。如果这个页面可能被多次使用,这个方式就不行了。因为 SwfitUI 底层优化了性能,等于会自动缓存一些 View。使用 FirstAppearView 如果是 reuse 来的,那么 init 方法中的赋值就不会被执行了。总结来说,init 方法关联的是应用级别的初始化。但是我们想要的 onFirstAppear 其实是页面级别的初始化。
所以我们必须自定义一个方法,然后绑定到一个自身的状态值来标记。
我们可以通过下面的代码来自定义 ViewModifier 来达到这个目的:
swift
public extension View {
func onFirstAppear(_ action: @escaping () -> ()) -> some View {
modifier(FirstAppear(action: action))
}
}
private struct FirstAppear: ViewModifier {
let action: () -> ()
// Use this to only fire your block one time
@State private var hasAppeared = false
func body(content: Content) -> some View {
// And then, track it here
content.onAppear {
guard !hasAppeared else { return }
hasAppeared = true
action()
}
}
}
使用的方法和 onAppear 是一样的,使用我们的自定义的 onFirstAppear 替换 onAppear 就可以达到我们的目的了。
swift
struct ContentView: View {
@State private var greeting: String = "Initial"
var body: some View {
NavigationStack {
VStack {
Text(greeting)
.onTapGesture {
greeting = "点击"
}
NavigationLink(destination: Text("Pushed Text")) {
Text("Push a View")
}
.padding()
}
.onFirstAppear {
greeting = "初始化"
}
}
}
}