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