admin管理员组文章数量:1310074
I'm working on a SwiftUI project where I allow users to create shapes using paths, and everything works perfectly so far. I can draw shapes like circles, squares, rectangles, stars, and ovals using Path and manipulate their points. However, I am trying to overlay a text string along the path of these shapes so that the text follows the shape's border (e.g., the text should follow the perimeter of a square when the user selects the square shape).
I am using a custom ShapeAlongPathView to achieve this, where I calculate the position and angle of each character in the text to make it align with the path of the shape. The issue is that, while this works for some shapes (like circles and rectangles), it doesn’t work as expected for all shapes, especially complex ones like stars. The text either doesn’t align properly or appears distorted.
What I’ve tried:
- I’m using Path to create the shapes and their respective borders.
- I’ve implemented a ShapeAlongPathView that uses the calculatePositionAndAngle method to place text characters along the path.
- I’ve used a drag gesture to allow the user to move the shapes and the text positions accordingly.
- The text string is intended to follow the shape’s border, but it doesn't work for some shapes like stars or ovals.
Here is the code I am working with:
import SwiftUI
enum ShapeType: String, CaseIterable {
case circle, square, rectangle, star, oval
}
struct ShapeModel: Identifiable {
let id = UUID()
var type: ShapeType
var points: [VectorPoint]
var position: CGPoint
}
struct ShapeView: View {
@Environment(\.presentationMode) var presentationMode
@State private var shapes: [ShapeModel] = []
@State private var selectedShape: ShapeType = .circle
@State var isDhased: Bool = false
@State var isFilled: Bool = false
@State var borderColor: Color = .blue
@State var borderWidth: CGFloat = 1
var body: some View {
GeometryReader { geometry in
ZStack {
Image("imgCup")
.resizable()
.scaledToFit()
VStack {
GeometryReader { proxy in
ZStack {
ForEach($shapes) { $shape in
drawShape(shape: $shape)
.position(shape.position)
.gesture(DragGesture().onChanged { value in
shape.position = value.location
})
.overlay(
Path { path in
for (index, point) in shape.points.enumerated() {
path.addLine(to: point.position)
}
}
.overlay(
ShapeAlongPathView(text: "longStringlongStringlongStringlongStringlongStringlongStringlongString", path: shape.points.map { $0.position }, letterSpacing: $borderWidth, fontSize: $borderWidth)
)
)
}
}
.frame(width: proxy.size.width, height: proxy.size.height)
}
}
// UI Elements
VStack {
HStack {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(systemName: "arrow.left")
.resizable()
.scaledToFit()
.frame(width: 12, height: 12)
.tint(.white)
})
.frame(width: geometry.size.width * 0.1, height: geometry.size.height * 0.04)
.background(.ultraThinMaterial)
.cornerRadius(10)
Button("Add") {
addShape(type: selectedShape)
}
.frame(width: geometry.size.width * 0.2, height: geometry.size.height * 0.05)
.foregroundStyle(.white)
.background(.ultraThinMaterial)
.cornerRadius(10)
.shadow(radius: 1)
Button("-") {
isDhased.toggle()
}
.frame(width: geometry.size.width * 0.1, height: geometry.size.height * 0.05)
.foregroundStyle(.white)
.background(.ultraThinMaterial)
.cornerRadius(10)
.shadow(radius: 1)
Button("*") {
isFilled.toggle()
}
.frame(width: geometry.size.width * 0.1, height: geometry.size.height * 0.05)
.foregroundStyle(.white)
.background(.ultraThinMaterial)
.cornerRadius(10)
.shadow(radius: 1)
ColorPicker("", selection: Binding(get: {
Color(borderColor)
}, set: { newValue in
borderColor = newValue
}))
Spacer()
Button(action: {
shapes.removeAll()
}, label: {
Text("Clear")
.frame(width: geometry.size.width * 0.2, height: geometry.size.height * 0.05)
.foregroundStyle(.white)
.background(.ultraThinMaterial)
.cornerRadius(10)
.shadow(radius: 1)
})
}
.frame(width: geometry.size.width - 40, height: 80, alignment: .center)
Picker("Select Shape", selection: $selectedShape) {
ForEach(ShapeType.allCases, id: \ .self) { shape in
Text(shape.rawValue.capitalized)
}
}
.pickerStyle(SegmentedPickerStyle())
.padding()
}
.position(x: geometry.size.width / 2, y: 80)
Slider(value: $borderWidth, in: 10...30)
.position(x: geometry.size.width / 2, y: geometry.size.height - 40)
}
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .top)
.background(Color.gray)
}
}
private func addShape(type: ShapeType) {
let newShape = ShapeModel(type: type, points: createInitialPoints(for: type), position: CGPoint(x: 100, y: 200))
shapes.append(newShape)
}
private func createInitialPoints(for type: ShapeType) -> [VectorPoint] {
let width: CGFloat = 100
let height: CGFloat = 100
switch type {
case .circle:
return [
VectorPoint(position: CGPoint(x: 150, y: 150)),
VectorPoint(position: CGPoint(x: 200, y: 150)) // Resizable control point
]
case .square:
return [
VectorPoint(position: CGPoint(x: 100, y: 100)),
VectorPoint(position: CGPoint(x: 200, y: 100)),
VectorPoint(position: CGPoint(x: 200, y: 200)),
VectorPoint(position: CGPoint(x: 100, y: 200))
]
case .rectangle:
return [
VectorPoint(position: CGPoint(x: 100, y: 100)),
VectorPoint(position: CGPoint(x: 250, y: 100)),
VectorPoint(position: CGPoint(x: 250, y: 200)),
VectorPoint(position: CGPoint(x: 100, y: 200))
]
case .star:
return generateStarPoints(center: CGPoint(x: 150, y: 150), radius: 50)
case .oval:
return [
VectorPoint(position: CGPoint(x: 150, y: 150)),
VectorPoint(position: CGPoint(x: 200, y: 150)), // Horizontal scaling
VectorPoint(position: CGPoint(x: 150, y: 180)) // Vertical scaling
]
}
}
private func generateStarPoints(center: CGPoint, radius: CGFloat) -> [VectorPoint] {
let angles = stride(from: 0, through: 360, by: 72).map { angle in
angle * 3.14 / 180
}
return angles.map { angle in
VectorPoint(position: CGPoint(x: center.x + radius * cos(angle), y: center.y + radius * sin(angle)))
}
}
@ViewBuilder
private func drawShape(shape: Binding<ShapeModel>) -> some View {
ZStack {
Path { path in
if shape.wrappedValue.type == .circle {
let center = shape.wrappedValue.points[0].position
let edge = shape.wrappedValue.points[1].position
let radius = abs(center.x - edge.x)
path.addEllipse(in: CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2))
} else if shape.wrappedValue.type == .oval {
let center = shape.wrappedValue.points[0].position
let hEdge = shape.wrappedValue.points[1].position
let vEdge = shape.wrappedValue.points[2].position
let width = abs(center.x - hEdge.x) * 2
let height = abs(center.y - vEdge.y) * 2
path.addEllipse(in: CGRect(x: center.x - width / 2, y: center.y - height / 2, width: width, height: height))
} else if shape.wrappedValue.type == .square || shape.wrappedValue.type == .rectangle {
path.move(to: shape.wrappedValue.points[0].position)
for point in shape.wrappedValue.points.dropFirst() {
path.addLine(to: point.position)
}
path.closeSubpath()
} else if shape.wrappedValue.type == .star {
path.move(to: shape.wrappedValue.points[0].position)
for point in shape.wrappedValue.points.dropFirst() {
path.addLine(to: point.position)
}
path.closeSubpath()
}
}
.fill(isFilled ? borderColor : Color.blue.opacity(0.0))
.stroke(borderColor, style: StrokeStyle(
lineWidth: 2,
lineCap: .round, // Optional: for rounded ends
lineJoin: .round, // Optional: for rounded corners
dash: [isDhased ? 1 : borderWidth, isDhased ? 0 : borderWidth] // Dash pattern: 10 points on, 5 points off
))
ForEach(shape.points.indices, id: \ .self) { index in
Circle()
.fill(Color.red)
.frame(width: 15, height: 15)
.position(shape.points[index].position.wrappedValue)
.gesture(DragGesture()
.onChanged { value in
shape.points[index].position.wrappedValue = value.location
}
)
}
}
}
}
struct ShapeView_Previews: PreviewProvider {
static var previews: some View {
ShapeView()
}
}
struct ShapeAlongPathView: View {
let text: String
let path: [CGPoint]
@Binding var letterSpacing: CGFloat
@Binding var fontSize: CGFloat
var body: some View {
ZStack {
ForEach(0..<calculateNumberOfCharacters(), id: \.self) { index in
if let positionAndAngle = calculatePositionAndAngle(at: index) {
let characterIndex = text.index(text.startIndex, offsetBy: index % text.count)
let character = text[characterIndex]
Text(String(character))
.font(.system(size: fontSize, weight: .bold))
.foregroundColor(.white)
.rotationEffect(.radians(positionAndAngle.angle))
.position(positionAndAngle.position)
}
}
}
}
private func calculateNumberOfCharacters() -> Int {
let pathLength = calculatePathLength()
return Int(pathLength / letterSpacing)
}
private func calculatePathLength() -> CGFloat {
var length: CGFloat = 0
for i in 1..<path.count {
let start = path[i - 1]
let end = path[i]
length += hypot(end.x - start.x, end.y - start.y)
}
return length
}
// MARK: - Calculate Position and Angle
private func calculatePositionAndAngle(at index: Int) -> (position: CGPoint, angle: CGFloat)? {
guard path.count > 1 else { return nil }
let segmentLength = CGFloat(index) * letterSpacing
var accumulatedLength: CGFloat = 0
for i in 1..<path.count {
let start = path[i - 1]
let end = path[i]
let segmentDist = hypot(end.x - start.x, end.y - start.y)
if accumulatedLength + segmentDist >= segmentLength {
let ratio = (segmentLength - accumulatedLength) / segmentDist
let x = start.x + ratio * (end.x - start.x)
let y = start.y + ratio * (end.y - start.y)
let dx = end.x - start.x
let dy = end.y - start.y
let angle = atan2(dy, dx)
return (position: CGPoint(x: x, y: y), angle: angle)
}
accumulatedLength += segmentDist
}
return nil
}
}
The issue: The text does not always follow the shape's path correctly. For example:
- For squares and rectangles, the text wraps correctly around the edges.
- For stars and ovals, the text gets distorted or doesn't follow the path smoothly.
I would appreciate any help on:
- Suggestions on how I can ensure the text aligns properly with any arbitrary shape's border.
- Any best practices or tips to improve the accuracy of this text placement along complex paths.
- Suggestions for handling dynamic shapes where the user can drag the shape around and the text should adjust accordingly.
this is the result what I got right now
maybe change needs in ShapeAlongPathView class
版权声明:本文标题:ios - Overlaying Text Along a Shape Path in SwiftUI — Issues with Text Following the Shape Border - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1741830372a2399895.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论