iOS 15 Swift 5.5 Trying to understand preferences, but struggling with GeometryReader
I crafted this code reading a tutorial, but it doesn't work correctly. The green box should appear over the red dot, the first one. But it is over the centre one. When I tap on the dot, it moves to the third...it feels like SwiftUI drew the whole thing and then changed its mind about the coordinates. Sure I could change the offsets, but that is a hack. Surely SwiftUI should be updating the preferences if the thing moves.
import SwiftUI
struct ContentView: View {
@State private var activeIdx: Int = 0
@State private var rects: [CGRect] = Array<CGRect>(repeating: CGRect(), count: 12)
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 15).stroke(lineWidth: 3.0).foregroundColor(Color.green)
.frame(width: rects[activeIdx].size.width, height: rects[activeIdx].size.height)
.offset(x: rects[activeIdx].minX, y: rects[activeIdx].minY)
HStack {
SubtleView(activeIdx: $activeIdx, idx: 0)
SubtleView(activeIdx: $activeIdx, idx: 1)
SubtleView(activeIdx: $activeIdx, idx: 2)
}
.animation(.easeInOut(duration: 1.0), value: rects)
}.onPreferenceChange(MyTextPreferenceKey.self) { preferences in
for p in preferences {
self.rects[p.viewIdx] = p.rect
print("p \(p)")
}
}.coordinateSpace(name: "myZstack")
}
}
struct SubtleView: View {
@Binding var activeIdx:Int
let idx: Int
var body: some View {
Circle()
.fill(Color.red)
.frame(width: 64, height: 64)
.background(MyPreferenceViewSetter(idx: idx))
.onTapGesture {
activeIdx = idx
}
}
}
struct MyTextPreferenceData: Equatable {
let viewIdx: Int
let rect: CGRect
}
struct MyTextPreferenceKey: PreferenceKey {
typealias Value = [MyTextPreferenceData]
static var defaultValue: [MyTextPreferenceData] = []
static func reduce(value: inout [MyTextPreferenceData], nextValue: () -> [MyTextPreferenceData]) {
value.append(contentsOf: nextValue())
}
}
struct MyPreferenceViewSetter: View {
let idx: Int
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.clear)
.preference(key: MyTextPreferenceKey.self,
value: [MyTextPreferenceData(viewIdx: self.idx, rect: geometry.frame(in: .named("myZstack")))])
}
}
}
Tried moving the onPreferences up a level, down a level..attaching it to each view... but still doesn't update...what did I miss here=
You read absolute coordinates in preference so need to use position
instead of offset
RoundedRectangle(cornerRadius: 15).stroke(lineWidth: 3.0).foregroundColor(Color.green)
.frame(width: rects[activeIdx].size.width, height: rects[activeIdx].size.height)
.position(x: rects[activeIdx].midX, y: rects[activeIdx].midY) // << here !!
Tested with Xcode 13.2 / iOS 15.2
And here is complete fixed body:
var body: some View {
ZStack {
HStack {
SubtleView(activeIdx: $activeIdx, idx: 0)
SubtleView(activeIdx: $activeIdx, idx: 1)
SubtleView(activeIdx: $activeIdx, idx: 2)
}
RoundedRectangle(cornerRadius: 15).stroke(lineWidth: 3.0).foregroundColor(Color.green)
.frame(width: rects[activeIdx].size.width, height: rects[activeIdx].size.height)
.position(x: rects[activeIdx].midX, y: rects[activeIdx].midY)
}.onPreferenceChange(MyTextPreferenceKey.self) { preferences in
for p in preferences {
self.rects[p.viewIdx] = p.rect
print("p \(p)")
}
}.coordinateSpace(name: "myZstack")
.animation(.easeInOut(duration: 0.5), value: activeIdx)
The problem is that you are using offset(x:y:)
instead of position(x:y:)
.
Your ZStack
's coordinates will start from the top-left which will (locally) be (0, 0)
. However, by default the green square will be centered between all 3 red circles. This causes a problem because the coordinates start at (0, 0)
but the square starts centered (ie one square off).
Initially you have the first circle starting at (0, 0)
but you are already have a centered square, so the offset is 0
in both x
and y
, therefore already centered and it appears like you are on a different index.
Fix this by replacing with position(x:y:)
and by measuring the mid points:
RoundedRectangle(cornerRadius: 15).stroke(lineWidth: 3.0).foregroundColor(Color.green)
.frame(width: rects[activeIdx].size.width, height: rects[activeIdx].size.height)
.position(x: rects[activeIdx].midX, y: rects[activeIdx].midY)
I think the answer is simply that a ZStack
is naturally center aligned, so your initial position, before the offset is over the second circle, but your activeIdx
is 0 which is the first circle. If you view it on the Canvas in the static preview. Simply changing the ZStack
alignment to .leading
lines everything up.
var body: some View {
ZStack(alignment: .leading) { // Set your alignment here
RoundedRectangle(cornerRadius: 15).stroke(lineWidth: 3.0).foregroundColor(Color.green)
.frame(width: rects[activeIdx].size.width, height: rects[activeIdx].size.height)
.offset(x: rects[activeIdx].minX, y: rects[activeIdx].minY)
HStack {
SubtleView(activeIdx: $activeIdx, idx: 0)
SubtleView(activeIdx: $activeIdx, idx: 1)
SubtleView(activeIdx: $activeIdx, idx: 2)
}
// Also change the animation value to activeIdx
.animation(.easeInOut(duration: 1.0), value: activeIdx)
}.onPreferenceChange(MyTextPreferenceKey.self) { preferences in
for p in preferences {
self.rects[p.viewIdx] = p.rect
print("p \(p)")
}
}.coordinateSpace(name: "myZstack")
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.