概览
在 SwiftUI 中,正是自定义视图让我们的 App 变得与众不同!然而,除了传统的视图接口定义方式以外,我们其实还可以有更“银杏化”的选择。
如上图所示:对于 SubView 子视图所需的参数我们一开始并没有操之过急,而是随后再以独立、灵活的方式将其传入到了 SubView 中,这是怎么做到的呢?
在本篇博文中,您将学到如下内容:
- 概览
- 1. 一个简单的视图需求!
- 2. “传统”的调用方式
- 3. 灵动的方式:按需且独立!
- 4. 再次验证 SwiftUI 视图状态的稳定性
- 总结
闲言少叙,Let‘s go!!!😉
1. 一个简单的视图需求!
我们需要创建一个子视图,它用来显示 Model 可观察对象的内容,同时包括一个界面是否展开的状态,并且可以自定义用户点击的行为:
@Observable
class Model {
var name = "hopy"
var power = 5
}
struct SubView: View {
let model = Model()
@Binding var isExpanding: Bool
var tapHandler: (()->Void)?
//...
}
从上面代码中可以清楚的看到:SubView 子视图包含一个 Model 可观察对象,并且还有 isExpanding 和 tapHandler 属性来分别表示自身展开的状态和用户点击时执行的代码。
我们可以这样实现 SubView 的 body:
var body: some View {
VStack {
Text(model.name)
.font(.largeTitle.weight(.bold))
if isExpanding {
Divider()
HStack {
Text("POW: \(model.power)")
.foregroundStyle(.red)
.font(.headline.weight(.heavy))
Spacer()
Button("Add POW!") {
model.power += 1
}
.buttonStyle(.borderedProminent)
}
}
}.onTapGesture {
tapHandler?()
}
.padding()
.background(Color.black.opacity(0.2), in: RoundedRectangle(cornerRadius: 15.0))
.overlay {
RoundedRectangle(cornerRadius: 15)
.stroke(.black, lineWidth: 5.0)
}
.shadow(radius: 5)
}
2. “传统”的调用方式
现在已经定义好了 SubView 视图,我们可以这样在主视图中创建并使用它:
@State var isExpanding = false
SubView(isExpanding: $isExpanding) {
print("OK!")
}
SubView 的运行界面如下图所示:
如上代码,我们在创建 SubView 子视图时,就需要将它所有必要的传入参数都考虑周全。
当然,这样本身并没有什么不妥。只不过,假若视图包含海量传入参数可能会出现一些不“银杏化”的地方:
- 在视图创建时就需要考虑到它所有的传入参数,即使有些可以暂时“忽略不计”;
- 在视图创建时就需要绞尽脑汁让这一坨冗长的传入参数在代码缩进和排版上看起来不那么“毛骨悚然”;
- 无法清晰的隔离视图自身创建和其状态创建的不同逻辑;
那么,除了视图“传统”的接口设计方式之外,我们是否还有其它的解决方案呢?
答案是肯定的!
3. 灵动的方式:按需且独立!
回忆一下 SwiftUI 中视图的本质:它其实只是状态的函数,它本身很“廉价”,更重要的是它是一个值对象。
这意味着,我们可以随时创建它们的拷贝,并改变拷贝所包含的属性,然后再用修改后的拷贝替换原有的视图。
首先,我们将 SubView 定义修改为如下形式:
struct SubView: View {
let model = Model()
private var isExpanding = false
private var tapHandler: (()->Void)?
}
这样做的好处是:在 SubView 创建时无需传入任何参数,我们完全将 SubView 自身和其状态分开了。
注意在上面代码中我们用 private 关键字修饰了它的各个属性,那么我们必须找到随后改变它们的方法,这该如何是好呢?
因为私有属性只能在类型内部读写,但类型扩展显然属于“内部”这一范畴,所以我们可以在 SubView 的扩展中大展拳脚:
extension SubView {
func isExpanding(_ expanding: Bool) -> Self {
var view = self
view.isExpanding = expanding
return view
}
func tapHandler(_ handler: @escaping ()->()) -> Self {
var view = self
view.tapHandler = handler
return view
}
}
可以看到:在上面 SubView 视图的扩展方法中我们像讨论过的那样显式拷贝了 SubView 对象的实例,然后更改它的属性,最后返回了更改后的视图。
现在,我们可以这样创建 SubView 视图了:
struct ContentView: View {
@State var isExpanding = false
var body: some View {
NavigationStack {
VStack {
SubView()
.isExpanding(isExpanding)
.tapHandler {
withAnimation(.snappy) {
isExpanding.toggle()
}
}
Button("Expanding!") {
withAnimation(.bouncy) {
isExpanding.toggle()
}
}
.padding(.top, 100)
}
.padding()
}
}
}
于是乎,我们可以“赤裸裸的” 让 SubView 先诞生,然后根据需要再以视图扩展的方式为其“注入”必要的参数。这样我们就可以有的放矢的将重点放在视图的某些属性上,创建逻辑会更加清晰明了。
4. 再次验证 SwiftUI 视图状态的稳定性
如果小伙伴们观察的足够仔细就会发现,上述代码每次子视图的展开属性(isExpanding)发生改变时,其 Model 的 power 值就会被重置:
这是因为,每次 isExpanding 属性改变时 SubView 自身的重建也会导致其 Model 对象的重建。
在 Swift 5.9 新 @Observable 对象在 SwiftUI 使用中的陷阱与解决 这篇博文中,我们进行过 SwiftUI 视图 @Observable 对象稳定性的讨论。我们得出的一个重要结论是:如果想要 @Observable 对象保持稳定,必须将它用状态来承载!
在本案例中为了达到这一目的,我们可以有两种方法:
- 在主视图中将 Model 实例传递到 SubView 中;
- 或者在 SubView 中用 @State 修饰 Model 属性;
这里,我们采用第二种方法,将 SubView 中的 model 对象用 @State 属性包装器修饰:
struct SubView: View {
@State var model = Model()
//...
}
最后运行看一下结果:
看到了吗?现在无论 SubView 自身如何变化,我们的 Model 状态都不会“始乱终弃”,它的内容始终保持一致!棒棒哒!💯
总结
在本篇博文中,我们讨论了 SwiftUI “传统”的视图接口定义在具有海量传入参数时的一些不便之处,并且用更加“低耦合”的“环保”方法改善了这一情况。相信现在小伙伴们对于 SwiftUI 中视图的构建会有更写意、更灵活的方式啦!
感谢观赏,再会!😎