简体   繁体   中英

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=

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.

结果的 GIF

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM