'Understanding Preferences in SwiftUI
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=
Solution 1:[1]
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)
Solution 2:[2]
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")
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Asperi |
| Solution 2 |


