I created a custom shape in the form of a ring segment. I eventually want to use this shape as a button in my app, and therefore I need to be able to position text right in the middle of it. Below picture shows the ring segment (blue) and the text (red) I wish to center vertically inside the blue segment.
The code below creates the custom blue shape segment:
struct CurvedButtonData {
enum ButtonLocation {
case top
case right
case bottom
case left
}
/// The button's width as an angle in degrees.
let width: Double = 90.0
/// The button's start location, as an angle in degrees.
fileprivate(set) var start = 0.0
/// The button's end location, as an angle in degrees.
fileprivate(set) var end = 0.0
init(position: ButtonLocation) {
switch position {
case .top:
start = -135
case .right:
start = -45
case .bottom:
start = 45
case .left:
start = 135
}
end = start + width
}
}
struct CurvedButton: Shape, InsettableShape {
let data: CurvedButtonData
var insetAmount = 0.0
func path(in rect: CGRect) -> Path {
let points = CurvedButtonGeometry(curvedButtonData: data, rect: rect)
var path = Path()
path.addArc(center: points.center, radius: points.innerRadius, startAngle: .degrees(data.start), endAngle: .degrees(data.end), clockwise: false)
path.addArc(center: points.center, radius: points.outerRadius - insetAmount, startAngle: .degrees(data.end), endAngle: .degrees(data.start), clockwise: true)
path.closeSubpath()
return path
}
func inset(by amount: CGFloat) -> CurvedButton {
var button = self
button.insetAmount += amount
return button
}
}
struct CurvedButtonGeometry {
let data: CurvedButtonData
let center: CGPoint
let innerRadius: CGFloat
let outerRadius: CGFloat
init(curvedButtonData: CurvedButtonData, rect: CGRect) {
center = CGPoint(x: rect.midX, y: rect.midY)
let radius = min(rect.width, rect.height) / 4
innerRadius = radius
outerRadius = radius * 2
data = curvedButtonData
}
/// Returns the view location of the point in the wedge at unit-
/// space location `unitPoint`, where the X axis of `p` moves around the
/// wedge arc and the Y axis moves out from the inner to outer
/// radius.
subscript(unitPoint: UnitPoint) -> CGPoint {
let radius = lerp(innerRadius, outerRadius, by: unitPoint.y)
let angle = lerp(data.start, data.end, by: Double(unitPoint.x))
return CGPoint(x: center.x + CGFloat(cos(angle)) * radius,
y: center.y + CGFloat(sin(angle)) * radius)
}
/// Linearly interpolate from `from` to `to` by the fraction `amount`.
private func lerp<T: BinaryFloatingPoint>(_ fromValue: T, _ toValue: T, by amount: T) -> T {
return fromValue + (toValue - fromValue) * amount
}
}
And this is my pityful attempt to create a button with my custom shape and center the "Click me" text inside the blue area:
import SwiftUI
struct CircularCVBControl: View {
var body: some View {
Button {
} label: {
ZStack(alignment: .top) {
CurvedButton(data: .init(position: .top))
.fill(.blue)
.overlay(alignment: .top) {
Text("Click me")
.foregroundColor(.red)
}
}
}
.buttonStyle(.plain)
}
}
struct CircularCVBControl_Previews: PreviewProvider {
static var previews: some View {
CircularCVBControl()
}
}
How can I now neatly (and without using a hacky padding on the Text) center the text vertically in my custom shape? I am looking for some sort of custom alignment guide/logic that does the job. Also, the shape can be rotated in other directions too, and the text should rotate with it.
2
Answers
The problem is that your curve does not render in the middle of the frame. To solve this, I believe you need to offset your center.
Like this:
This is a solution only for the ‘.top’ position. I will leave the other positions to you 😉
Here is the full code:
I see that you want to combine multiple Curvedbuttons together as one ‘circle’. In that case you indeed have to offset the Text instead of the Shape.
You can do it with GeometryReader. I’ve implemented it for the .top position as shown below. You can add the other positions into the getOffset() method. I am not entirely happy with this code as some of the coordinates are not computed in more than one place. You can/should clean this code a lot but I wanted to demonstrate the core principle without changing everything. Hope this helps.