SwiftUI之View Tree实战1

在之前的两篇文章中,讲解了高层次的视图如何获取低层次视图信息的方法,在本篇文章中,我将给大家演示这些技术在开发中的实际用处。

本篇文章的主要思想来自https://swiftui-lab.com/communicating-with-the-view-tree-part-3/,我并不会对原作者的文章做一个简单的翻译,而是把他的思想进行一个总结,用另一种更简单,更容易理解的方式表达出来。

我们先看一下最终的效果图:

Kapture 2020-07-14 at 19.17.30.gif

细心的读者应该发现了,左边较小的视图正是右边视图的一个预览,仿佛镜子一般,把右边视图的变化映射出来。

其实,这个效果非常有意思,如果你不了解我们之前讲解的技术,实现这个效果对你来说实在是太难了,这就是我一直想表达的一个观点,某些功能或者动画,在SwiftUI中的实现实在是太简单了。

要实现上述的功能,整体的步骤为:

  • 设计需要传递的数据结构,这些信息会从子view传递到上层的view中
  • 通过modifier绑定数据
  • 根据数据生成视图

MyPreferenceData

struct MyPreferenceData: Identifiable {
    let id = UUID()
    let viewType: ViewType
    let bounds: Anchor<CGRect>
    
    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:

企业微信截图_0b3329af-a2ea-49c8-926c-22db1c602884.png

son(Color)对应下图的view:

企业微信截图_6d895023-5e35-4c78-8b83-10f83d361240.png

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:

企业微信截图_27ec77aa-ad97-4f00-aaab-5ceba59cd1a8.png

其中,大部分代码是非常容易理解的,只有两个地方用到了一点点算法。

第一个是计算let factor = geometry[parentAnchor].width / (geometry[miniMapAreaAnchor].width - 10),表示右边到左边的映射因子,大家看我画的示意图就能明白了:

企业微信截图_002cd1d6-4849-4e9c-be3a-4e5fb68c34d8.png

第二个则是计算彩色块在父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的用法,当视图的关系只有一层的时候,如下图所示:

企业微信截图_30b325d0-6e9b-432c-8569-f1c9d6e046d1.png

我们通常是不需要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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352