I'm working on a simple view in which onAppear
a path is drawn up from the 3rd circle up to the first and then to the second. I am using GeometryReader
and PreferenceKey
to get the actual positions. It looks like I'm correctly getting the center of the circle but it seems to have offset a bit lower than it should be.
Could anyone point out what is wrong?
Here's my code:
struct ScreenPath: View {
@State private var end = CGFloat.zero
var positions: [CGPoint]
var body: some View {
Path { lineBuilder in
let pathSequence = [positions[2], positions[0], positions[1]]
if let start = pathSequence.first {
lineBuilder.move(to: start)
lineBuilder.addLine(to: pathSequence[1])
lineBuilder.addLine(to: pathSequence[2])
}
}
.trim(from: 0, to: end)
.stroke(Color.green, lineWidth: 4)
.animation(.easeIn(duration: 1.5))
.onAppear {
self.end = 1
}
}
}
struct HomeScreen: View {
@State private var circlePositions: [CGPoint] = []
@State private var colors: [Color] = [.white, .white, .white, .white]
@State private var showPath: Bool = false
var body: some View {
GeometryReader { geo in
ZStack {
if showPath {
ScreenPath(positions: circlePositions)
}
VStack(spacing: 30) {
VStack(spacing: 45) {
HStack {
Circle()
.fill(colors[0])
.frame(width: 30, height: 30)
.background(GeometryReader { circleGeo in
Color.clear.preference(key: CirclePositionKey.self, value: [circleGeo.frame(in: .global).center])
})
Spacer().frame(width: 50)
Circle()
.fill(colors[1])
.frame(width: 30, height: 30)
.background(GeometryReader { circleGeo in
Color.clear.preference(key: CirclePositionKey.self, value: [circleGeo.frame(in: .global).center])
})
}
HStack {
Circle()
.fill(colors[2])
.frame(width: 30, height: 30)
.background(GeometryReader { circleGeo in
Color.clear.preference(key: CirclePositionKey.self, value: [circleGeo.frame(in: .global).center])
})
Spacer().frame(width: 50)
Circle()
.fill(colors[3])
.frame(width: 30, height: 30)
.background(GeometryReader { circleGeo in
Color.clear.preference(key: CirclePositionKey.self, value: [circleGeo.frame(in: .global).center])
})
}
}
}
.frame(width: geo.size.width, height: geo.size.height, alignment: .center)
}
.onPreferenceChange(CirclePositionKey.self) { positions in
if positions.count == 4 {
circlePositions = positions
for position in positions {
print("Circle position: \(position)")
}
}
}
.onAppear {
colors[0] = .green
colors[1] = .green
colors[2] = .green
showPath.toggle()
}
}
.border(Color.red)
}
}
extension CGRect {
var center: CGPoint {
return CGPoint(x: self.midX, y: self.midY)
}
}
struct CirclePositionKey: PreferenceKey {
typealias Value = [CGPoint]
static var defaultValue: [CGPoint] = []
static func reduce(value: inout [CGPoint], nextValue: () -> [CGPoint]) {
value.append(contentsOf: nextValue())
}
}
You are taking the frame in the .global
coordinate space. The origin of this coordinate space is the very top left of the screen, ignoring the safe area.
When drawing a Path
, the coordinate space is the local coordinate space of the Path
view, which is safe area-aware. The origin of this coordinate space is inside the safe area.
So a simple way to fix this is to do ignoresSafeArea
on the path:
Path {
...
}
.trim(from: 0, to: end)
.stroke(Color.green, lineWidth: 4)
.ignoresSafeArea()
.animation(.easeIn(duration: 1.5), value: end)
.onAppear {
self.end = 1
}
That said, I'd recommend declaring your own coordinate space instead, like in this question, so that you have more control.
Here is an example using the VStack
's coordinate space and using backgroundPreferenceValue
to put the path below the circles (this way you don't need the ZStack
):
struct ContentView: View {
@State private var colors: [Color] = [.white, .white, .white, .white]
@State private var showPath: Bool = false
var body: some View {
GeometryReader { geo in
VStack(spacing: 30) {
VStack(spacing: 45) {
HStack {
Circle()
.fill(colors[0])
.frame(width: 30, height: 30)
.background(GeometryReader { circleGeo in
Color.clear.preference(key: CirclePositionKey.self, value: [circleGeo.frame(in: .named("DrawingArea")).center])
})
Spacer().frame(width: 50)
Circle()
.fill(colors[1])
.frame(width: 30, height: 30)
.background(GeometryReader { circleGeo in
Color.clear.preference(key: CirclePositionKey.self, value: [circleGeo.frame(in: .named("DrawingArea")).center])
})
}
HStack {
Circle()
.fill(colors[2])
.frame(width: 30, height: 30)
.background(GeometryReader { circleGeo in
Color.clear.preference(key: CirclePositionKey.self, value: [circleGeo.frame(in: .named("DrawingArea")).center])
})
Spacer().frame(width: 50)
Circle()
.fill(colors[3])
.frame(width: 30, height: 30)
.background(GeometryReader { circleGeo in
Color.clear.preference(key: CirclePositionKey.self, value: [circleGeo.frame(in: .named("DrawingArea")).center])
})
}
}
}
.frame(width: geo.size.width, height: geo.size.height, alignment: .center)
.coordinateSpace(.named("DrawingArea"))
.backgroundPreferenceValue(CirclePositionKey.self) { positions in
if showPath {
ScreenPath(positions: positions)
}
}
.onAppear {
colors[0] = .green
colors[1] = .green
colors[2] = .green
showPath.toggle()
}
}
.border(Color.red)
}
}