translation
Inspecting the View Tree (Anchor Preferences) – Part 2 – The SwiftUI Lab
More content, welcome to pay attention to the public number
Swift Garden
In the first part of the article, we introduced the use of preference. This is useful for scenarios where information is passed up. By defining the association type of a PreferenceKey, we can put anything in there.
In the second part, it’s time to introduce Anchor Preferences. At the time of writing this article, I couldn’t find any documentation, blogs, or articles on how to use these obscure tools. So please join me as we explore this uncharted territory.
Anchor preferences are intuitively hard to understand at first, but once we’ve mastered them, they’re hard to forget. To keep things simple, let’s use the example from Part 1. It’s great if you’re already familiar with the challenge itself, so you can focus on all these new features. Unlike the previous solution, we no longer use the coordinate system and will replace.onpreferencechange () with something else.
Here we go again: we want to animate the border from month to month:
Anchor Preference
Now let’s warmly welcome: Anchor. This is an opaque type that holds type T, which can be CGRect or CGPoint. We accessed the view boundary using Anchor and then used Anchor to access functions such as Top, Toplead, topTrailing, Center, trailing, bottom, bottomLeading, View properties such as bottomTrailing and leading.
Because it is an opaque type, we cannot use it alone. Remember GeometryReader to the Rescue introduced the subscript getter for GeometryProxy? When we use the value of Anchor as the index of geometry Proxy, you can get CGRect or CGPoint values. At the same time, they have been converted to the coordinate space of the GeometryReader view.
Let’s start by modifying the data handled by the PreferenceKey. In this example, we want to replace CGRect with Anchor:
struct MyTextPreferenceData {
let viewIdx: Int
let bounds: Anchor<CGRect>}Copy the code
Our PreferenceKey remains the same:
struct MyTextPreferenceKey: PreferenceKey {
typealias Value = [MyTextPreferenceData]
static var defaultValue: [MyTextPreferenceData] = []
static func reduce(value: inout [MyTextPreferenceData], nextValue: (a)- > [MyTextPreferenceData]) {
value.append(contentsOf: nextValue())
}
}Copy the code
MonthView is now more concise. Instead of using.preference(), we call modifier.anchorpreference (). Unlike the other methods, here we specify a value (.bounds in our case) that means our Transform closure gets an Anchor that represents the bounds of the view being modified. We create our MyTextPreferenceData value with ($0), similar to the normal property. Thus, we no longer need to use the GeometryReader in the.background() modifier to get the boundaries of the text view.
To help you understand, let’s look at the code:
struct MonthView: View {@Binding var activeMonth: Int
let label: String
let idx: Int
var body: some View {
Text(label)
.padding(10)
.anchorPreference(key: MyTextPreferenceKey.self, value: .bounds, transform: { [MyTextPreferenceData(viewIdx: self.idx, bounds: $0)] })
.onTapGesture { self.activeMonth = self.idx }
}
}Copy the code
Finally, let’s update the ContentView. There are some changes here. First of all, we are no longer using the onPreferenceChange (), but call. BackgroundPreferenceValue (). This is a similar modifier to.background(), but with one big improvement: you can access an array of preferences for the entire view tree. In this way, we get the boundaries of all month views and can use them to calculate where we need to draw the boundaries.
There is another place where you need an GeometryReader to understand the values of an Anchor. Note that we no longer need to care about coordinate space; the GeometryReader handles it.
struct ContentView : View {@State private var activeIdx: Int = 0
var body: some View {
VStack {
Spacer(a)HStack {
MonthView(activeMonth: $activeIdx, label: "January", idx: 0)
MonthView(activeMonth: $activeIdx, label: "February", idx: 1)
MonthView(activeMonth: $activeIdx, label: "March", idx: 2)
MonthView(activeMonth: $activeIdx, label: "April", idx: 3)}Spacer(a)HStack {
MonthView(activeMonth: $activeIdx, label: "May", idx: 4)
MonthView(activeMonth: $activeIdx, label: "June", idx: 5)
MonthView(activeMonth: $activeIdx, label: "July", idx: 6)
MonthView(activeMonth: $activeIdx, label: "August", idx: 7)}Spacer(a)HStack {
MonthView(activeMonth: $activeIdx, label: "September", idx: 8)
MonthView(activeMonth: $activeIdx, label: "October", idx: 9)
MonthView(activeMonth: $activeIdx, label: "November", idx: 10)
MonthView(activeMonth: $activeIdx, label: "December", idx: 11)}Spacer()
}.backgroundPreferenceValue(MyTextPreferenceKey.self) { preferences in
GeometryReader { geometry in
self.createBorder(geometry, preferences)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}
}
func createBorder(_ geometry: GeometryProxy, _ preferences: [MyTextPreferenceData]) -> some View {
let p = preferences.first(where: {$0.viewIdx == self.activeIdx })
letbounds = p ! =nil ? geometry[p!.bounds] : .zero
return RoundedRectangle(cornerRadius: 15)
.stroke(lineWidth: 3.0)
.foregroundColor(Color.green)
.frame(width: bounds.size.width, height: bounds.size.height)
.fixedSize()
.offset(x: bounds.minX, y: bounds.minY)
.animation(.easeInOut(duration: 1.0))}}Copy the code
BackgroundPreferenceValue () corresponding overlayPreferenceValue (). It does the same thing, except instead of drawing behind it, it draws in front of the view.
Multiple anchor preferences with a PreferenceKey
We already know that there is more than one Anchor value. Bounds, topLeading, Center, Bottom, etc. Maybe sometimes we need to get more than one of these values. But, as we’ll learn, it’s not as simple as calling.anchorpreference () for all of these values. To illustrate this point, let’s solve the problem again.
But this time, instead of using an Anchor to fetch the boundaries of the month view, we use two separate Anchor values. One was used for topLeading while the other was used for bottomTrailing. As a reminder, Anchor is a better solution for this particular problem. But we’ll use the third method here just to learn how to get multiple anchor preferences on the same view.
We first modify MyTextPreferenceData to accommodate the two endpoints of the rectangle. This time we need to make them selectable because they can’t be set at the same time.
struct MyTextPreferenceData {
let viewIdx: Int
var topLeading: Anchor<CGPoint>? = nil
var bottomTrailing: Anchor<CGPoint>? = nil
}Copy the code
The PreferenceKey remains the same:
struct MyTextPreferenceKey: PreferenceKey {
typealias Value = [MyTextPreferenceData]
static var defaultValue: [MyTextPreferenceData] = []
static func reduce(value: inout [MyTextPreferenceData], nextValue: (a)- > [MyTextPreferenceData]) {
value.append(contentsOf: nextValue())
}
}Copy the code
Our month view limit requires setting two anchor preferences. However, if we call.anchorpreference () on the same view more than once, only the last call will take effect. . Therefore, we can only be called once anchorPreference (), and then use the transformAnchorPreference () fill the missing data:
struct MonthView: View {@Binding var activeMonth: Int
let label: String
let idx: Int
var body: some View {
Text(label)
.padding(10)
.anchorPreference(key: MyTextPreferenceKey.self, value: .topLeading, transform: { [MyTextPreferenceData(viewIdx: self.idx, topLeading: $0)] })
.transformAnchorPreference(key: MyTextPreferenceKey.self, value: .bottomTrailing, transform: { ( value: inout [MyTextPreferenceData], anchor: Anchor<CGPoint>) in
value[0].bottomTrailing = anchor
})
.onTapGesture { self.activeMonth = self.idx }
}
}Copy the code
Finally, we update.createBorder() accordingly to be calculated based on two points instead of a rectangle:
struct ContentView : View {@State private var activeIdx: Int = 0
var body: some View {
VStack {
Spacer(a)HStack {
MonthView(activeMonth: $activeIdx, label: "January", idx: 0)
MonthView(activeMonth: $activeIdx, label: "February", idx: 1)
MonthView(activeMonth: $activeIdx, label: "March", idx: 2)
MonthView(activeMonth: $activeIdx, label: "April", idx: 3)}Spacer(a)HStack {
MonthView(activeMonth: $activeIdx, label: "May", idx: 4)
MonthView(activeMonth: $activeIdx, label: "June", idx: 5)
MonthView(activeMonth: $activeIdx, label: "July", idx: 6)
MonthView(activeMonth: $activeIdx, label: "August", idx: 7)}Spacer(a)HStack {
MonthView(activeMonth: $activeIdx, label: "September", idx: 8)
MonthView(activeMonth: $activeIdx, label: "October", idx: 9)
MonthView(activeMonth: $activeIdx, label: "November", idx: 10)
MonthView(activeMonth: $activeIdx, label: "December", idx: 11)}Spacer()
}.backgroundPreferenceValue(MyTextPreferenceKey.self) { preferences in
GeometryReader { geometry in
self.createBorder(geometry, preferences)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}
}
func createBorder(_ geometry: GeometryProxy, _ preferences: [MyTextPreferenceData]) -> some View {
let p = preferences.first(where: {$0.viewIdx == self.activeIdx })
letaTopLeading = p? .topLeadingletaBottomTrailing = p? .bottomTrailinglettopLeading = aTopLeading ! =nil? geometry[aTopLeading!] : .zeroletbottomTrailing = aBottomTrailing ! =nil? geometry[aBottomTrailing!] : .zeroreturn RoundedRectangle(cornerRadius: 15)
.stroke(lineWidth: 3.0)
.foregroundColor(Color.green)
.frame(width: bottomTrailing.x - topLeading.x, height: bottomTrailing.y - topLeading.y)
.fixedSize()
.offset(x: topLeading.x, y: topLeading.y)
.animation(.easeInOut(duration: 1.0))}}Copy the code
Nested views
So far, we’ve been dealing with the brother (or cousin) view. However, things get more challenging when it comes to setting preferences for nested views. At this time. TransformAnchorPreference () become more important. For example, if you have two views, parent and child, setting.anchorpreference () on both will not work. The closure of the subview will not execute. To solve this problem, need to use in child views anchorPreference () on the parent view using transformAnchorPreference (). As to why we did this, we will elaborate.
The next step
For the last step in this series, we’re going to use a different example. We’re going to create a mini map. The mini-map is built by reading the view tree of a form. We’ll also see how modifying the form’s view tree has a direct impact on the mini-map, which only responds to preferences changes in the form’s view tree.
Sneak peek:
My official account here Swift and computer programming related articles, as well as excellent translation of foreign articles, welcome to pay attention to ~