I am wanting to build a trapezoidal sunburst effect in SwiftUI. My designs requirements are a static image, but I need something more reusable and dynamic, because I can't replicate the view as an image in SwiftUI without some incredibly hacky workarounds. My solution is to rebuild this view, natively, rather than relying on the .png
asset.
The View to be replicated.
The view I have now.
The code behind what I have now.
struct SunRay: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: CGPoint(x: rect.width * 0.4, y: rect.height))
path.addLine(to: CGPoint(x: rect.width * 0.6, y: rect.height))
path.addLine(to: CGPoint(x: rect.width * 0.8, y: 0))
path.addLine(to: CGPoint(x: rect.width * 0.2, y: 0))
}
}
}
struct RadialSunburstView: View {
var body: some View {
ZStack {
HStack(spacing: -8) {
Group {
SunRay().rotation(Angle(degrees: -40))
SunRay().rotation(Angle(degrees: -30))
SunRay().rotation(Angle(degrees: -20))
SunRay().rotation(Angle(degrees: -10))
SunRay()
SunRay().rotation(Angle(degrees: 10))
SunRay().rotation(Angle(degrees: 20))
SunRay().rotation(Angle(degrees: 30))
SunRay().rotation(Angle(degrees: 40))
}
}
.frame(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height)
.foregroundColor(.white.opacity(0.05))
.clipped()
.background (
LinearGradient.blueGradient
.frame(height: UIScreen.main.bounds.size.height * 0.15)
)
}
.frame(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height * 0.15)
.clipped()
.cornerRadius(20)
}
}
As you can see, my code is closeish? I'm not really sure if I'd call it a win at this point. The largest issue I have right now is that when I attempt to use it in another view, with RadialSunburstView()
I can't actually add any horizontal padding to it. Which I'm sure is a result of my statically defined widths, which really needs to be more dynamic. How do I properly implement something like this, such that I end up with a view that I can reframe, pad, etc. as if it were something like a Rectangle
?
CodePudding user response:
That was fun :) I suggest to draw the whole beam structure as a Shape
. Then you can fill and clip the shape as needed, and also add padding etc.
I added two properties to define nr. of beams and relative center offset.
struct ContentView: View {
@State private var beams: Double = 10
@State private var offset: Double = 0.3
var body: some View {
VStack {
Sunburst(beams: beams, centerOffset: offset)
.fill(.orange)
.background(.blue)
.clipShape(RoundedRectangle(cornerRadius: 12))
.frame(height: 200)
Divider()
.padding()
Text("Nr of beams")
Slider(value: $beams, in: 1.0...100.0)
Text("Offset of center")
Slider(value: $offset, in: 0...1)
}
.padding()
}
}
struct Sunburst: Shape {
let beams: Double // nr of beams
let centerOffset: Double // relative offset of centerpoint downwards, relative to height
func path(in rect: CGRect) -> Path {
let step = 180.0 / Double(beams)
let centerOffsetAbs = rect.height * centerOffset
let center = CGPoint(x: rect.width/2, y: rect.height centerOffsetAbs)
let radius = max(rect.height, rect.width) * (1.5 centerOffset)
return Path { path in
path.move(to: center)
for angle in stride(from: step / 2, to: 180, by: 2 * step) {
path.addLine(to: CGPoint(x: center.x cos(angle * Double.pi / 180) * radius, y: center.y - sin(angle * Double.pi / 180) * radius ))
path.addLine(to: CGPoint(x: center.x cos((angle step) * Double.pi / 180) * radius, y: center.y - sin((angle step) * Double.pi / 180) * radius ))
path.addLine(to: center)
}
}
}
}