在之前的两篇文章中,讲解了高层次的视图如何获取低层次视图信息的方法,在本篇文章中,我将给大家演示这些技术在开发中的实际用处。
本篇文章的主要思想来自https://swiftui-lab.com/communicating-with-the-view-tree-part-3/,我并不会对原作者的文章做一个简单的翻译,而是把他的思想进行一个总结,用另一种更简单,更容易理解的方式表达出来。
我们先看一下最终的效果图:
细心的读者应该发现了,左边较小的视图正是右边视图的一个预览,仿佛镜子一般,把右边视图的变化映射出来。
其实,这个效果非常有意思,如果你不了解我们之前讲解的技术,实现这个效果对你来说实在是太难了,这就是我一直想表达的一个观点,某些功能或者动画,在SwiftUI中的实现实在是太简单了。
要实现上述的功能,整体的步骤为:
- 设计需要传递的数据结构,这些信息会从子view传递到上层的view中
- 通过modifier绑定数据
- 根据数据生成视图
MyPreferenceData
struct MyPreferenceData: Identifiable {
let id = UUID()
let viewType: ViewType
let bounds: Anchor
func getColor() -> Color {
switch self.viewType {
case .parent:
return Color.orange.opacity(0.5)
case .son(let c):
return c
default:
return Color.gray.opacity(0.3)
}
}
func show() -> Bool {
switch self.viewType {
case .parent:
return true
case .son:
return true
default:
return false
}
}
}
在我们这个例子中,我们需要知道3种类型view的位置信息:
enum ViewType: Equatable {
case parent
case son(Color)
case miniMapArea
}
其中parent
对应的是下图的view:
son(Color)
对应下图的view:
miniMapArea
对应左边灰色的视图,我们知道了这些信息后,才能把右边的视图映射到左边。
MypreferenceKey
struct MypreferenceKey: PreferenceKey {
typealias Value = [MyPreferenceData]
static var defaultValue: Value = []
static func reduce(value: inout [MyPreferenceData], nextValue: () -> [MyPreferenceData]) {
value.append(contentsOf: nextValue())
}
}
通过这段代码,我们声明了一个MypreferenceKey,然后需要把每个view需要携带的信息通过这个key进行绑定,为了方便计算,我们把每个view的信息放到了一个数组中[MyPreferenceData]
。
DragableView
右边视图中的彩色块支持拖拽手势改变自身的frame,我们需要单独将其封装成一个view:
struct DragableView: View {
let color: Color
@State private var currentOffset: CGSize = CGSize.zero
@State private var preOffset: CGSize = CGSize(width: 100, height: 100)
var w: CGFloat {
self.currentOffset.width + self.preOffset.width
}
var h: CGFloat {
self.currentOffset.height + self.preOffset.height
}
var body: some View {
RoundedRectangle(cornerRadius: 5)
.foregroundColor(color)
.frame(width: w, height: h)
.anchorPreference(key: MypreferenceKey.self, value: .bounds) { anchor in
[MyPreferenceData(viewType: .son(color), bounds: anchor)]
}
.gesture(
DragGesture()
.onChanged { (value: DragGesture.Value) in
self.currentOffset = value.translation
}
.onEnded { _ in
self.preOffset = CGSize(width: w,
height: h)
self.currentOffset = CGSize.zero
}
)
}
}
这段代码值得关注的有2点:
- w和h的计算
-
.anchorPreference
:绑定数据
MiniMap
struct MiniMap: View {
let geometry: GeometryProxy
let preferences: [MyPreferenceData]
var body: some View {
guard let parentAnchor = preferences.first(where: { $0.viewType == .parent })?.bounds else {
return AnyView(EmptyView())
}
guard let miniMapAreaAnchor = preferences.first(where: { $0.viewType == .miniMapArea })?.bounds else {
return AnyView(EmptyView())
}
let factor = geometry[parentAnchor].width / (geometry[miniMapAreaAnchor].width - 10)
let miniMapAreaPosition = CGPoint(x: geometry[miniMapAreaAnchor].minX, y: geometry[miniMapAreaAnchor].minY)
let parentPosition = CGPoint(x: geometry[parentAnchor].minX, y: geometry[parentAnchor].minY)
return AnyView(miniMapView(factor, miniMapAreaPosition, parentPosition))
}
func miniMapView(_ factor: CGFloat,
_ miniMapAreaPosition: CGPoint,
_ parentPosition: CGPoint) -> some View {
ZStack(alignment: .topLeading) {
ForEach(preferences.reversed()) { pref in
if pref.show() {
self.rectangleView(pref, factor, miniMapAreaPosition, parentPosition)
}
}
}
.padding(5)
}
func rectangleView(_ pref: MyPreferenceData,
_ factor: CGFloat,
_ miniMapAreaPosition: CGPoint,
_ parentPosition: CGPoint) -> some View {
return Rectangle()
.fill(pref.getColor())
.frame(width: self.geometry[pref.bounds].width / factor,
height: self.geometry[pref.bounds].height / factor)
.offset(x: (self.geometry[pref.bounds].minX - parentPosition.x) / factor + miniMapAreaPosition.x,
y: (self.geometry[pref.bounds].minY - parentPosition.y) / factor + miniMapAreaPosition.y)
}
}
上边的这么代码,只为实现下边图片上的view:
其中,大部分代码是非常容易理解的,只有两个地方用到了一点点算法。
第一个是计算let factor = geometry[parentAnchor].width / (geometry[miniMapAreaAnchor].width - 10)
,表示右边到左边的映射因子,大家看我画的示意图就能明白了:
第二个则是计算彩色块在父view中的相对位置,我们就不做过多解释了。
overlayPreferenceValue
最后,我们把上边的代码组合起来:
struct ContentView: View {
var body: some View {
HStack {
RoundedRectangle(cornerRadius: 5)
.foregroundColor(Color.gray.opacity(0.5))
.frame(width: 250, height: 300)
.anchorPreference(key: MypreferenceKey.self, value: .bounds) { anchor in
[MyPreferenceData(viewType: .miniMapArea, bounds: anchor)]
}
ZStack(alignment: .topLeading) {
VStack {
HStack {
DragableView(color: .green)
DragableView(color: .blue)
DragableView(color: .pink)
}
HStack {
DragableView(color: .black)
DragableView(color: .white)
DragableView(color: .purple)
}
}
}
.frame(width: 550, height: 300)
.background(Color.orange.opacity(0.5))
.transformAnchorPreference(key: MypreferenceKey.self, value: .bounds, transform: {
$0.append(contentsOf: [MyPreferenceData(viewType: .parent, bounds: $1)])
})
}
.overlayPreferenceValue(MypreferenceKey.self) { value in
GeometryReader { proxy in
MiniMap(geometry: proxy, preferences: value)
}
}
}
}
注意: .overlayPreferenceValue
表示会把视图放到最上层,如果想放到最下层,则使用.backgroundPreferenceValue
。
transformAnchorPreference
在这里我想大概讲一个transformAnchorPreference的用法,当视图的关系只有一层的时候,如下图所示:
我们通常是不需要transformAnchorPreference的,只需要在子view上通过.anchorPreference
绑定数据即可,除非要传递的信息不只一个,比如通过.anchorPreference
传递了.bounds
,还想传递.topLeading
,那么这时就需要通过transformAnchorPreference把.topLeading
传递过去。代码类似于这样:
.anchorPreference(key: MypreferenceKey.self, value: .bounds) { anchor in
[MyPreferenceData(viewType: .miniMapArea, bounds: anchor)]
}
.transformAnchorPreference(key: MypreferenceKey.self, value: .topLeading, transform: {
...
}
如果视图的层级很深,则必须使用transformAnchorPreference来处理,否则系统就获取不到更深层次的Preference。
系统在遍历Preference的时候,采用了类似递归的方式,也可以认为是深度优先算法,如果某个父类也写了Preference,则系统不会遍历子view的Preference,这种情况只有当某个父view写了transformAnchorPreference,系统才会往更深层次去获取Preference。
关于上边这句话的解读,大家自己去理解吧,因为这也是我的猜测,不一定正确。
总结
在SwiftUI中,Preference绝对是一柄利器,大家应该重视起来这项技术。
本文源代码:NestedViwsDemo.swfit
SwiftUI集合:FuckingSwiftUI