写更少的 SwiftUI 代码 —— Namespace
你是否写过这样的代码:DetailView(namespace: namespace, isSource: selected == nil)
或者这样的:DetailView(namespace: Namespace().wrappedValue, isSource: false)
这基本上是每一次写 Hero 动画时必不可少的两个参数。
如果还有更复杂的需求,一个 View 可能会有更多的参数需要传递。
其实,很久之前我就在想,如果把 Namespace 以环境变量的方式注入,那么就可以不用再写 namespace
这个参数了,岂不美哉?
Property Wrapper
Property Wrapper 是 SwiftUI 中的一大利器,它无处不在:@State
,@Binding
,@ObservedObject
……
借此机会,我也来好好地学习下如何自己写一个 Property Wrapper。
首先,用 @propertyWrapper
来标注他它是一个包装器。
1 | @propertyWrapper |
由于此时我们想要从环境变量中读取到
Namespace
,因此还需要创建EnvironmentValue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 struct NamespaceKey: EnvironmentKey {
static var defaultValue: Namespace.ID? = nil
}
extension EnvironmentValues {
var namespace: Namespace.ID? {
get { self[NamespaceKey.self] }
set { self[NamespaceKey.self] = newValue }
}
}
extension View {
func namespace(_ namespace: Namespace.ID?) -> some View {
environment(\.namespace, namespace)
}
}
第二步,读取环境变量中的 Namespace.ID
1 | @propertyWrapper |
第三步,提供 WrappedValue
Property Wrapper(属性包装器),顾名思义,把一个属性包装起来。
当我们使用
@PropertyWrapper var variable
时,我们希望使用原本的类型,而不是 Property Wrapper 的类型
wrappedValue
是一个计算属性,用于 获取包装下的值 以及 修改某个值
1 | @propertyWrapper |
到此为止,基本上就大功告成了。
但是,这里只能获取到一个 Optional 的值,显然在 matchedGeometryEffect
中使用并不方便,解决方案是,再创建一个独立的 Namespace
作为候补 Namespace
1 | @propertyWrapper |
此时,尝试在 View 中使用这个 Property Wrapper 会发现报 Runtime Error
Dynamic Property
上面的错误提示表明,我们需要在 View 内部才能使用 @Environment
,@Namespace
这是因为,当 View 刷新时,内容可能会发生变化,此时 Property Wrapper 中的值不能与视图保持同步刷新。
此时,Environment 只会使用 default Value,Namespace 则不会与任何 View 关联。
同时,当我们在 Property Wrapper 中修改 Environment 值时也不会刷新视图(SwiftUI 不知何时刷新/同步数据)
解决方案也很简单,给我们的 Property Wrapper 添加 DynamicProperty
Protocol 即可。
1 | @propertyWrapper |
这样,就可以愉快地在自定义的 Property Wrapper 中使用 SwiftUI 的 Property Wrappers 了,Dynamic Property 背后的 update()
会帮助我们处理数据同步,以及在数据变化时刷新视图
如果这部分仍然不理解,可以参考这个教程:https://www.hackingwithswift.com/plus/intermediate-swiftui/creating-a-custom-property-wrapper-using-dynamicproperty
SwiftUI Built-in Property Wrappers
SwiftUI 的源码我们并不能直接看到,但是可以通过 Swift Interface 来分析。
[XcodePath]/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/
基本所有的 SwiftUI 内置 Property Wrappers 都是用了 DynamicProperty
,这使得 SwiftUI 的渲染引擎知道视图内的值何时发生了变化,然后触发视图刷新,这是一个很重要的机制,确保了 Property Wrapper 中的值与当前视图的状态始终保持同步。
总结
完整代码
使用方法
1 | import SwiftUI |
写更少的 SwiftUI 代码 —— Namespace