iosswiftui

How to add a background color to a path, similar to the one shown in another image?


I'm trying to create a circular text path in SwiftUI using a custom CircularTextPath view. The text is being drawn on an arc, and I want to add a background color behind the path, as shown in another image I have (for example, adding a solid background color to the entire text arc).

CircularTextPath:

import SwiftUI

struct CircularTextPath: View {
    let text: String
    let arcAngle: Angle
    
    var body: some View {
        if arcAngle.radians != 0.0 {
            textAsPath
                .fixedSize()
                .hidden()
                .overlay {
                    GeometryReader { fullText in
                        let textWidth = fullText.size.width
                        let startAngle = -(arcAngle.radians / 2)
                        let radius = arcAngle.radians > 0 ?  textWidth * 1.5 / arcAngle.radians : textWidth * 2.0 / arcAngle.radians
                        
                        HStack(spacing: 0) {
                            ForEach(Array(text.enumerated()), id: \.offset) { index, character in
                                Text(String(character))
                                    .hidden()
                                    .overlay {
                                        GeometryReader { charSpace in
                                            let midX = charSpace.frame(in: .named("FullText")).midX
                                            let fraction = midX / textWidth
                                            let angle = startAngle + (fraction * arcAngle.radians)
                                            let xOffset = (textWidth / 2) - midX
                                            TextPath(character: String(character))
                                                .scaleEffect(x: 1, y: 1) // Flip horizontally to correct mirroring
                                                .offset(y: -radius)
                                                .rotationEffect(.radians(angle))
                                                .offset(x: xOffset, y: radius)
                                        }
                                    }
                            }
                        }
                        .fixedSize()
                        .frame(width: textWidth)
                    }
                }
                .coordinateSpace(name: "FullText")
        } else {
            textAsPath
        }
    }
    
    private var textAsPath: some View {
        HStack(spacing: 0) {
            ForEach(Array(text.enumerated()), id: \.offset) { index, character in
                TextPath(character: String(character))
                    .scaleEffect(x: 1, y: 1) // Flip horizontally to correct mirroring
            }
        }
    }
}

struct TextPath: View {
    let character: String
    
    var body: some View {
        let path = characterToPath(character)
        return path.fill(Color.primary)
    }
    
    private func characterToPath(_ character: String) -> Path {
        let font = UIFont.systemFont(ofSize: 20)
        let attributedString = NSAttributedString(string: character, attributes: [.font: font])
        let line = CTLineCreateWithAttributedString(attributedString)
        let path = CGMutablePath()
        
        for run in (CTLineGetGlyphRuns(line) as! [CTRun]) {
            for index in 0..<CTRunGetGlyphCount(run) {
                var glyph = CGGlyph()
                var position = CGPoint()
                CTRunGetGlyphs(run, CFRangeMake(index, 1), &glyph)
                CTRunGetPositions(run, CFRangeMake(index, 1), &position)
                if let letterPath = CTFontCreatePathForGlyph(font, glyph, nil) {
                    let transform = CGAffineTransform(translationX: position.x, y: position.y).scaledBy(x: 1, y: -1) // Flip horizontally
                    path.addPath(letterPath, transform: transform)
                }
            }
        }
        return Path(path)
    }
}




What I want to achieve: I want to add a background color behind the path, specifically behind the entire circular arc of text. How can I modify this code to add a solid background color to the path, similar to the effect shown in the other image I have?

What I've tried so far:

what output I got

enter image description here

output I want

enter image description here

what I want to do

enter image description here

when two or three lines

enter image description here


Solution

  • Converting a path that contains the outline of letters to an arc may be difficult. However, maybe there is an easier way.

    If you know the arc angle and the radius then you can simply trim the path of a circle. Your example includes these values, so you should be able to stroke the trimmed circle without having to convert the text to a path.

    Here is an example to show it working. I am using CurvedText for this, as I was suggesting you use in my answer to your earlier question.

    let arcDegrees: Double = 320
    let radius: CGFloat = 90
    let fontSize: CGFloat = 50
    let strokeWidth: CGFloat = 70
    
    var body: some View {
        ZStack {
            let endTrimFraction = (360.0 - arcDegrees) / (2 * 360.0)
            Circle()
                .trim(from: endTrimFraction, to: 1 - endTrimFraction)
                .stroke(.black, lineWidth: strokeWidth)
                .rotationEffect(.degrees(-90))
                .padding(fontSize / 2)
    
            // See https://stackoverflow.com/a/77280669/20386264
            CurvedText(string: "I am he who remains", radius: -radius)
                .offset(y: radius)
                .font(.system(size: fontSize, weight: .black))
                .foregroundStyle(.white)
        }
        .frame(width: (2 * radius) + fontSize, height: (2 * radius) + fontSize)
    }
    

    Screenshot


    EDIT If I understand correctly, what you are aiming for is a way to vary the arc angle of some text dynamically, exactly as was being done in your earlier question, but also to show a colored background behind the text. The background should fit the text as the angle is changed.

    So, based on the approach shown above, here is an adaption of the answer I previously provided to your other question. The main changes:

    I found that it looks best if the background is slightly wider than the text. The easiest way to achieve this is to add leading and trailing spaces to the text being shown.

    struct CircularTextView: View {
        @State private var arcDegrees: Double = 0
        @State private var textRadius = CGFloat.zero
        let text: String = " I am he who remains " // 👈 with leading + trailing spaces
        let fontSize: CGFloat = 30
        let fontWeight: Font.Weight = .black
        let textColor: Color = .white
        let strokeColor: Color = .blue
        let strokeWidth: CGFloat = 50
    
        var body: some View {
            VStack {
                CircularText(text: text, arcAngle: .degrees(arcDegrees), textRadius: $textRadius)
                    .font(.system(size: fontSize, weight: fontWeight))
                    .foregroundStyle(textColor)
                    .background {
                        if arcDegrees == 0 {
                            strokeColor
                                .frame(height: strokeWidth)
                        } else {
                            let endTrimFraction = max(0, (360.0 - abs(arcDegrees)) / (2 * 360.0))
                            let diameter = 2 * abs(textRadius)
                            Circle()
                                .trim(from: endTrimFraction, to: 1 - endTrimFraction)
                                .stroke(strokeColor, lineWidth: strokeWidth)
                                .rotationEffect(.degrees(arcDegrees > 0 ? 90 : -90))
                                .frame(width: diameter, height: diameter)
                                .offset(y: textRadius)
                        }
                    }
                    .frame(height: 300)
                    .border(Color.gray.opacity(0.3))
    
                Slider(value: $arcDegrees, in: -360...360, step: 1)
                    .padding(.horizontal)
                Text("Arc Degrees: \(Int(arcDegrees))")
                    .font(.subheadline)
            }
        }
    }
    
    struct CircularText: View {
        let text: String
        let arcAngle: Angle
        @Binding var textRadius: CGFloat // 👈 added
    
        var body: some View {
            if arcAngle.radians == 0.0 {
                textAsChars
            } else {
                textAsChars
                    .fixedSize()
                    .hidden()
                    .overlay {
                        GeometryReader { fullText in
                            let textWidth = fullText.size.width
                            let startAngle = -(arcAngle.radians / 2)
                            let radius = textWidth / arcAngle.radians
                            HStack(spacing: 0) {
                                ForEach(Array(text.enumerated()), id: \.offset) { index, character in
                                    Text(String(character))
                                        .hidden()
                                        .overlay {
                                            GeometryReader { charSpace in
                                                let midX = charSpace.frame(in: .named("FullText")).midX
                                                let fraction = midX / textWidth
                                                let angle = startAngle + (fraction * arcAngle.radians)
                                                let xOffset = (textWidth / 2) - midX
                                                Text(String(character))
                                                    .offset(y: -radius)
                                                    .rotationEffect(.radians(angle))
                                                    .offset(x: xOffset, y: radius)
                                            }
                                        }
                                }
                            }
                            .fixedSize()
                            .frame(width: textWidth)
                            .onChange(of: radius, initial: true) { _, newVal in // 👈 added
                                textRadius = newVal
                            }
                        }
                    }
                    .coordinateSpace(name: "FullText")
            }
        }
    
        private var textAsChars: some View {
            HStack(spacing: 0) {
                ForEach(Array(text.enumerated()), id: \.offset) { index, character in
                    Text(String(character))
                }
            }
        }
    }
    

    Animation