In the previous two articles, I showed you how high-level views can get information from low-level views. In this article, I’ll show you how these techniques can be used in practice for development.

The main idea of this article comes from swiftui-lab.com/communicati… I will not do a simple translation of the original author’s article, but summarize his thoughts and express them in a simpler and more understandable way.

Let’s take a look at the final result:

Careful readers will notice that the smaller view on the left is a preview of the right view, mirroring the changes in the right view.

In fact, this effect is very interesting, if you don’t know the technology we talked about before, it is very difficult for you to implement this effect, this is the point I have been trying to express, certain functions or animations in SwiftUI are too easy to implement.

To achieve the above functions, the overall steps are as follows:

  • Design the data structure that needs to be passed from the child view to the upper view
  • Bind data through the modifier
  • Generate views from the data

MyPreferenceData

struct MyPreferenceData: Identifiable {
    let id = UUID(a)let viewType: ViewType
    let bounds: Anchor<CGRect>
    
    func getColor(a) -> 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(a) -> Bool {
        switch self.viewType {
        case .parent:
            return true
        case .son:
            return true
        default:
            return false}}}Copy the code

In our example, we need to know the location of three types of Views:

enum ViewType: Equatable {
    case parent
    case son(Color)
    case miniMapArea
}
Copy the code

Where parent corresponds to view:

Son (Color) corresponds to the view below:

MiniMapArea corresponds to the gray view on the left. After we know this information, we can map the right view to the left.

MypreferenceKey

struct MypreferenceKey: PreferenceKey {
    typealias Value = [MyPreferenceData]
    static var defaultValue: Value = []
    static func reduce(value: inout [MyPreferenceData], nextValue: (a)- > [MyPreferenceData]) {
        value.append(contentsOf: nextValue())
    }
}
Copy the code

With this code, we declare a MypreferenceKey, and then bind the information that each view needs to carry with that key. For ease of calculation, we put the information for each view into an array [MyPreferenceData].

DragableView

The color block in the right view supports dragging and dropping gestures to change its frame, which we need to wrap separately into a 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
                    }
            )
            
    }
}
Copy the code

There are two things worth noting about this code:

  • W and H
  • .anchorPreference: Bind data

MiniMap

struct MiniMap: View {
    let geometry: GeometryProxy
    let preferences: [MyPreferenceData]
    
    var body: some View {
        guard let parentAnchor = preferences.first(where: {$0.viewType == .parent })? .boundselse {
            return AnyView(EmptyView()}guard let miniMapAreaAnchor = preferences.first(where: {$0.viewType == .miniMapArea })? .boundselse {
            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)
    }
}
Copy the code

The above code implements the view in the image below:

Most of the code is fairly easy to understand, with only a couple of bits of algorithm.

The first one is to calculate let factor = parentAnchor. Width/(Geometry [miniMapAreaAnchor].width -10), indicating the mapping factor from right to left, you can see the sketch I draw:

The second is to calculate the relative position of the colored blocks in the parent view, which we won’t explain too much.

overlayPreferenceValue

Finally, we put the above code together:

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)
            }
        }
    }
}
Copy the code

Note: overlayPreferenceValue said it would put the view in the top, if you want to be on the bottom, is used. BackgroundPreferenceValue.

transformAnchorPreference

Here I want to tell a transformAnchorPreference general usage, when the relationship view only one layer, as shown in the figure below:

We usually don’t need transformAnchorPreference, only in the child by the view. AnchorPreference data binding, unless the message more than one, Passed. Such as through. AnchorPreference bounds, also want to pass. TopLeading, then you need to pass transformAnchorPreference. TopLeading passed over. The code looks something like this:

.anchorPreference(key: MypreferenceKey.self, value: .bounds) { anchor in
    [MyPreferenceData(viewType: .miniMapArea, bounds: anchor)]
}
.transformAnchorPreference(key: MypreferenceKey.self, value: .topLeading, transform: {
    ...
}
Copy the code

If you view the hierarchy of the deep, you must use transformAnchorPreference, otherwise the system can’t get deeper Preference.

When traversing Preference, the system adopts a similar recursion method, which can also be considered as depth-first algorithm. If a parent class also writes Preference, the system will not traverse the Preference of the child view. This kind of situation only when a parent view wrote transformAnchorPreference, system will go to a deeper level for Preference.

On the above interpretation of this sentence, we go to understand it, because this is also my guess, not necessarily correct.

conclusion

Preference is definitely a powerful tool in SwiftUI and should be taken seriously.

This article source code: NestedviwsDemo.swfit

SwiftUI Collection: FuckingSwiftUI