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
rotationEffectto create 8BadgeSymbols that are rotated about their bottoms to different angles.The key point here is that
ZStackcentres its views using the "logical" frames of the views, which is not affected byrotationEffect. Let's just consider the case of 2BadgeSymbols - one rotated 180 degrees, and one not rotated at all. Let the height of aBadgeSymbolbe h. These twoBadgeSymbols will appear to have height 2 * h when combined in aZStack,but as far as the
ZStackcan see, theseBadgeSymbols have the same frame.As a result, the
ZStackputs the centre of the non-rotatedBadgeSymbolin its centre, and the rotatedBadgeSymbolappears below that. Here I've highlighted the frame of theZStack, and its centre. TheZStackalso has height h.Notice that this is not quite what we want. We want the two
BadgeSymbols 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 theBadgeSymbols. 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 theZStackframe, as before)Clearly, it should be shifted down by h / 4, and that's exactly what the code is doing.
geometry.size.heightis just "h". The y position is 3 * h / 4 because it's adding h / 4 to h / 2 (the position of theBadgeSymbols otherwise).In general, if the scale is 1 / n, then the position should be ((n - 1) / n) * h for it to be centred.