SwiftUI之View Tree實戰(zhàn)1

在之前的兩篇文章中,講解了高層次的視圖如何獲取低層次視圖信息的方法,在本篇文章中,我將給大家演示這些技術(shù)在開發(fā)中的實際用處。

本篇文章的主要思想來自https://swiftui-lab.com/communicating-with-the-view-tree-part-3/,我并不會對原作者的文章做一個簡單的翻譯,而是把他的思想進行一個總結(jié),用另一種更簡單,更容易理解的方式表達出來。

我們先看一下最終的效果圖:

Kapture 2020-07-14 at 19.17.30.gif

細心的讀者應(yīng)該發(fā)現(xiàn)了,左邊較小的視圖正是右邊視圖的一個預(yù)覽,仿佛鏡子一般,把右邊視圖的變化映射出來。

其實,這個效果非常有意思,如果你不了解我們之前講解的技術(shù),實現(xiàn)這個效果對你來說實在是太難了,這就是我一直想表達的一個觀點,某些功能或者動畫,在SwiftUI中的實現(xiàn)實在是太簡單了。

要實現(xiàn)上述的功能,整體的步驟為:

  • 設(shè)計需要傳遞的數(shù)據(jù)結(jié)構(gòu),這些信息會從子view傳遞到上層的view中
  • 通過modifier綁定數(shù)據(jù)
  • 根據(jù)數(shù)據(jù)生成視圖

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對應(yīng)的是下圖的view:

企業(yè)微信截圖_0b3329af-a2ea-49c8-926c-22db1c602884.png

son(Color)對應(yīng)下圖的view:

企業(yè)微信截圖_6d895023-5e35-4c78-8b83-10f83d361240.png

miniMapArea對應(yīng)左邊灰色的視圖,我們知道了這些信息后,才能把右邊的視圖映射到左邊。

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的信息放到了一個數(shù)組中[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
                    }
            )
            
    }
}

這段代碼值得關(guān)注的有2點:

  • w和h的計算
  • .anchorPreference:綁定數(shù)據(jù)

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)
    }
}

上邊的這么代碼,只為實現(xiàn)下邊圖片上的view:

企業(yè)微信截圖_27ec77aa-ad97-4f00-aaab-5ceba59cd1a8.png

其中,大部分代碼是非常容易理解的,只有兩個地方用到了一點點算法。

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

企業(yè)微信截圖_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的用法,當(dāng)視圖的關(guān)系只有一層的時候,如下圖所示:

企業(yè)微信截圖_30b325d0-6e9b-432c-8569-f1c9d6e046d1.png

我們通常是不需要transformAnchorPreference的,只需要在子view上通過.anchorPreference綁定數(shù)據(jù)即可,除非要傳遞的信息不只一個,比如通過.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來處理,否則系統(tǒng)就獲取不到更深層次的Preference。

系統(tǒng)在遍歷Preference的時候,采用了類似遞歸的方式,也可以認為是深度優(yōu)先算法,如果某個父類也寫了Preference,則系統(tǒng)不會遍歷子view的Preference,這種情況只有當(dāng)某個父view寫了transformAnchorPreference,系統(tǒng)才會往更深層次去獲取Preference。

關(guān)于上邊這句話的解讀,大家自己去理解吧,因為這也是我的猜測,不一定正確。

總結(jié)

在SwiftUI中,Preference絕對是一柄利器,大家應(yīng)該重視起來這項技術(shù)。

本文源代碼:NestedViwsDemo.swfit

SwiftUI集合:FuckingSwiftUI

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容