I'm following this SwiftUI tutorial and encountered an issue with the positioning of badgeSymbols in Badge.swift. Here's the relevant code:
Badge.swift: (where the repositioning is applied)
import SwiftUI
struct Badge: View {
var badgeSymbols: some View {
ForEach(0..<8) { index in
RotatedBadgeSymbol(
angle: .degrees(Double(index) / Double(8)) * 360.0
)
}
.opacity(0.5)
}
var body: some View {
ZStack {
BadgeBackground()
GeometryReader { geometry in
badgeSymbols
.scaleEffect(1.0 / 4.0, anchor: .top)
.position(x: geometry.size.width / 2.0, y: (3.0 / 4.0) * geometry.size.height) // <--------- this is the re-positioning, particularly the "y" component
}
}
.scaledToFit()
}
}
#Preview {
Badge()
}
RotatedBadgeSymbol.swift:
import SwiftUI
struct RotatedBadgeSymbol: View {
let angle: Angle
var body: some View {
BadgeSymbol()
.padding(-60)
.rotationEffect(angle, anchor: .bottom)
}
}
#Preview {
RotatedBadgeSymbol(angle: Angle(degrees: 5))
}
BadgeSymbol.swift (less relevant -- the important thing is that it is returning a view that is a path):
struct BadgeSymbol: View {
static let symbolColor = Color(red: 79.0 / 255, green: 79.0 / 255, blue: 191.0 / 255)
var body: some View {
GeometryReader { geometry in
Path { path in
let width = min(geometry.size.width, geometry.size.height)
let height = width * 0.75
let spacing = width * 0.030
let middle = width * 0.5
let topWidth = width * 0.226
let topHeight = height * 0.488
path.addLines([
CGPoint(x: middle, y: spacing),
CGPoint(x: middle - topWidth, y: topHeight - spacing),
CGPoint(x: middle, y: topHeight / 2 + spacing),
CGPoint(x: middle + topWidth, y: topHeight - spacing),
CGPoint(x: middle, y: spacing)
])
path.move(to: CGPoint(x: middle, y: topHeight / 2 + spacing * 3))
path.addLines([
CGPoint(x: middle - topWidth, y: topHeight + spacing),
CGPoint(x: spacing, y: height - spacing),
CGPoint(x: width - spacing, y: height - spacing),
CGPoint(x: middle + topWidth, y: topHeight + spacing),
CGPoint(x: middle, y: topHeight / 2 + spacing * 3)
])
}
.fill(Self.symbolColor)
}
}
}
I'm puzzled why badgeSymbols needs repositioning in the ZStack, especially given that ZStack should center its children. I initially thought badgeSymbols as a composed object would be automatically centered.
My hypothesis is that the rotationEffect might be applied after the views are added to the ZStack. Is this correct? What's the underlying reason for this behavior?
I've attempted to 'flatten' badgeSymbols using Group {...} and tried adding .drawingGroup() modifier, but neither approach solved the issue.
Can someone explain why this repositioning is necessary and if there's a better way to handle this?
Notice that the sample code uses
rotationEffect
to create 8BadgeSymbol
s that are rotated about their bottoms to different angles.The key point here is that
ZStack
centres its views using the "logical" frames of the views, which is not affected byrotationEffect
. Let's just consider the case of 2BadgeSymbol
s - one rotated 180 degrees, and one not rotated at all. Let the height of aBadgeSymbol
be h. These twoBadgeSymbol
s will appear to have height 2 * h when combined in aZStack
,but as far as the
ZStack
can see, theseBadgeSymbol
s have the same frame.As a result, the
ZStack
puts the centre of the non-rotatedBadgeSymbol
in its centre, and the rotatedBadgeSymbol
appears below that. Here I've highlighted the frame of theZStack
, and its centre. TheZStack
also has height h.Notice that this is not quite what we want. We want the two
BadgeSymbol
s to be moved up a little, so that its "visual" centre is the same as the centre of theZStack
. This is why we need to change the position of theBadgeSymbol
s. We want the badge symbols to have a logical y position of 0, instead of the default centre position (i.e. h / 2), essentially moving it up by h / 2.In the code however, there is also a
scaleEffect
, withanchor: .top
. Just likerotationEffect
, this does not change the frames of anything - it's a purely visual effect. Now it looks like this (the red border represents theZStack
frame, as before)Clearly, it should be shifted down by h / 4, and that's exactly what the code is doing.
geometry.size.height
is just "h". The y position is 3 * h / 4 because it's adding h / 4 to h / 2 (the position of theBadgeSymbol
s otherwise).In general, if the scale is 1 / n, then the position should be ((n - 1) / n) * h for it to be centred.