TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

SOLVED: Question about putting a rounded border around a text?

Forums > SwiftUI

Hello everyone. So this is my code:

        HStack(spacing: 0) {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            HStack(spacing: 10) {
                Text("Hello, world!")
                    .padding(10)
            }
            .overlay( // apply a rounded border
                RoundedRectangle(cornerRadius: 8, style: .continuous)
                    .strokeBorder(Color(.green), lineWidth: 2)
            )
        }

Now I get an image and a text with a nice green border like this: Image

Now this brings me to my question. I would like the left side of the border I made with the overlay to be:

  1. Now be rounded but be normal square edges. In other words have the cornerRadius: 0
  2. The left line of the border I made with the overlay be invisible. I dont mean white but simply invisible so you can still see the background color.

For the second issue I know some of you will say simply make the line the same color as the background but the thing is the background will change in some situations so I need it to be invisible..

Thank you all!

EDIT: Not sure why the picture did not appear but I hope you guys get it

3      

Hi! Without writing custom Shape and using already provided APIs as well as some creativity you may want to try something like this. Definitely not the sleekest solution I would say but does the job. Well, at least if I got your intentions right...

        HStack(spacing: 0) {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            HStack(spacing: 10) {
                Text("Hello, world!")
                    .padding(10)
            }
            .overlay {
                GeometryReader { geo in
                    let fullPath = (geo.size.height * 2) + (geo.size.width * 2)
                    let endTrim = (geo.size.height / 2 + geo.size.width) / fullPath
                    UnevenRoundedRectangle(topLeadingRadius: 0,
                                           bottomLeadingRadius: 0,
                                           bottomTrailingRadius: 8,
                                           topTrailingRadius: 8,
                                           style: .continuous)
                    .trim(from: 0, to: endTrim)
                    .stroke(Color(.green), lineWidth: 2)
                }
            }
            .overlay {
                GeometryReader { geo in
                    let fullPath = (geo.size.height * 2) + (geo.size.width * 2)
                    let startTrim = (geo.size.height / 2 + geo.size.width + geo.size.height) / fullPath

                    UnevenRoundedRectangle(topLeadingRadius: 0,
                                           bottomLeadingRadius: 0,
                                           bottomTrailingRadius: 8,
                                           topTrailingRadius: 8,
                                           style: .continuous)
                    .trim(from: startTrim, to: 1)
                    .stroke(Color(.green), lineWidth: 2)
                }
            }
        }

4      

The way I would prefer to do that is something like this. But maybe you'll find some info how to make those rounded borders calculations youself.

struct MyLabel: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let width = rect.width
        let height = rect.height
        let posX = rect.origin.x
        let posY = rect.origin.y

        path.move(to: CGPoint(x: posX, y: posY))
        path.addLine(to: CGPoint(x: posX + width, y: posY)) // after this point add curve
        path.addLine(to: CGPoint(x: posX + width, y: posY + height)) // after this point add curve
        path.addLine(to: CGPoint(x: posX, y: posY + height))

        return path
    }
}

and then you can use it simply at it is without much going on on the screen

 .overlay {
                    MyLabel()
                        .stroke(Color.blue, lineWidth: 2)
                }

3      

Here a solution from @rebeloper | s on twitter/X cornerRaduis

struct RectCorner: OptionSet {
    let rawValue: Int

    static let topLeading = RectCorner(rawValue: 1 << 0)
    static let bottomLeading = RectCorner(rawValue: 1 << 3)
    static let bottomTrailing = RectCorner(rawValue: 1 << 2)
    static let topTrailing = RectCorner(rawValue: 1 << 1)

    static let all: RectCorner = [.topLeading, .bottomLeading, bottomTrailing, .topTrailing]

    static let leading: RectCorner = [.topLeading, .bottomLeading]
    static let trailing: RectCorner = [.topTrailing, .bottomTrailing]
    static let top: RectCorner = [.topLeading, .topTrailing]
    static let bottom: RectCorner = [.bottomLeading, .bottomTrailing]
}

struct RoundedCornerShape: InsettableShape {
    var radius: Double = .zero
    var corners: RectCorner = .all
    var style: RoundedCornerStyle = .continuous

    var insertAmount = 0.0

    func path(in rect: CGRect) -> Path {
        var path = Path()

        let topLeading: Double = corners.contains(.topLeading) ? radius - insertAmount : 0
        let bottomLeading: Double = corners.contains(.bottomLeading) ? radius - insertAmount : 0
        let bottomTrailing: Double = corners.contains(.bottomTrailing) ? radius - insertAmount : 0
        let topTrailing: Double = corners.contains(.topTrailing) ? radius - insertAmount : 0

        let cornerRadii = RectangleCornerRadii(topLeading: topLeading, bottomLeading: bottomLeading, bottomTrailing: bottomTrailing, topTrailing: topTrailing)

        path.addRoundedRect(in: rect, cornerRadii: cornerRadii, style: style)

        return path
    }

    func inset(by amount: CGFloat) -> some InsettableShape {
        var roundedCornerShape = self
        roundedCornerShape.insertAmount += amount
        return roundedCornerShape
    }
}

extension View {
    /// Clips this view to its bounding frame, with the specified corner radius, corners and style.
    /// - Parameters:
    ///   - radius: The corner radius to be applied.
    ///   - corners: Corners to apply the `radius`on.
    ///   - style: The shape of the rounded rectangle's corners
    /// - Returns: A view that clips this view to its bounding frame with the specified corner radius, corners and style.
    func cornerRadius(_ radius: CGFloat, corners: RectCorner, style: RoundedCornerStyle = .continuous) -> some View {
        clipShape(
            RoundedCornerShape(radius: radius, corners: corners, style: style)
        )
    }
}

Then you can do this

struct ContentView: View {
    var body: some View {
        VStack {
            Rectangle()
                .fill(.blue)
                .frame(width: 300, height: 200)
                .cornerRadius(20, corners: .trailing)
        }
        .padding()
    }
}

Or as in your example

struct ContentView: View {
    var body: some View {
        HStack(spacing: 0) {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)

            Text("Hello World!")
        }
        .padding()
        .overlay(
            RoundedCornerShape(radius: 20, corners: .trailing)
                .strokeBorder(.green, lineWidth: 2)
        )
    }
}

3      

Thank you both very much. Nice solutions but in the end I went with @ygeras first solution. But I could as an extra explenation as to why we had to use 2 overlays? Couldnt we somehow do 1 overlay to have the same result?

3      

The point is that when you use .trim function, as you probably noticed, it works with % length of the line. And it starts counting it length for rectangular shape from right side of the rectangular height in the middle of the height. So if you use just one, unfortunately it won't be able to trim part of it then leave it drawn then trim it again. You can just try to remove the second overlay and you will see that only part of your border will be drawn and not the way you want. So I used it twice :) and we have the parts we want to see on the screen.

4      

Thanks you! Smart solution

3      

Sorry, forgot to mention. It is not necessary to use two overlays, what I meant you can just put two georeaders in one overlay :) it will do the same job. If you plan to reuse the border in other views, maybe it will make sense to create something like this.

struct MyCustomBorder: ViewModifier {
    var cornerRadius: CGFloat
    var lineWidth: CGFloat
    var lineColor: Color

    func body(content: Content) -> some View {
        content
            .overlay {
                GeometryReader { geo in
                    let fullPath = (geo.size.height * 2) + (geo.size.width * 2)
                    let startTrim = (geo.size.height / 2 + geo.size.width + geo.size.height) / fullPath

                    // Upper part of the border
                    UnevenRoundedRectangle(topLeadingRadius: 0,
                                           bottomLeadingRadius: 0,
                                           bottomTrailingRadius: cornerRadius,
                                           topTrailingRadius: cornerRadius,
                                           style: .continuous)
                    .trim(from: startTrim, to: 1)
                    .stroke(lineColor, lineWidth: lineWidth)

                    // lower part of the border
                    let endTrim = (geo.size.height / 2 + geo.size.width) / fullPath
                    UnevenRoundedRectangle(topLeadingRadius: 0,
                                           bottomLeadingRadius: 0,
                                           bottomTrailingRadius: cornerRadius,
                                           topTrailingRadius: cornerRadius,
                                           style: .continuous)
                    .trim(from: 0, to: endTrim)
                    .stroke(lineColor, lineWidth: lineWidth)
                }
            }
    }
}

extension View {
    func customBorder(cornerRaidus: CGFloat = 8, 
                      lineWidth: CGFloat = 2,
                      lineColor: Color) -> some View {
        modifier(MyCustomBorder(cornerRadius: cornerRaidus, 
                                lineWidth: lineWidth,
                                lineColor: lineColor))
    }
}

and then in every view you are using it just add modifier and that's it.

struct ContentView: View {
    var body: some View {
        HStack(spacing: 0) {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            HStack(spacing: 10) {
                Text("Hello, world!")
                    .padding(10)
            }
            // so now what you need to use is only this modifier
            .customBorder(lineColor: .blue)
        }
    }
}

3      

But no end to perfection. So I think this is the one I would be satisfied with

struct ContentView: View {
    var body: some View {
        HStack(spacing: 0) {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            HStack(spacing: 10) {
                Text("Hello, world!")
                    .padding(8)
            }
            .makeRoundedCornerBorder(lineColor: .green)

        }
    }
}

struct RoundedCorners: Shape {    
    var cornerRadius: CGFloat = .infinity
    var corners: UIRectCorner = .allCorners

    func path(in rect: CGRect) -> Path {
        let path = UIBezierPath(roundedRect: rect,
                                byRoundingCorners: corners,
                                cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
        return Path(path.cgPath)
    }
}

struct RoundedCornersBorder: ViewModifier {
    var cornerRadius: CGFloat
    var lineWidth: CGFloat
    var lineColor: Color

    func body(content: Content) -> some View {
        content
            .overlay(
                GeometryReader { geo in
                    let fullPath = (geo.size.height * 2) + (geo.size.width * 2)
                    let endTrim = (geo.size.width * 2 + geo.size.height) / fullPath

                    RoundedCorners(cornerRadius: 8, corners: [.topRight, .bottomRight])
                        .trim(from: 0, to: endTrim)
                        .stroke(lineColor, lineWidth: lineWidth)
                }
            )
    }
}

extension View {
    func makeRoundedCornerBorder(cornerRadius: CGFloat = 8,
                                 lineWidth: CGFloat = 2,
                                 lineColor: Color) -> some View {
        modifier(RoundedCornersBorder(cornerRadius: cornerRadius, 
                                      lineWidth: lineWidth,
                                      lineColor: lineColor))
    }
}

3      

Hacking with Swift is sponsored by Blaze.

SPONSORED Still waiting on your CI build? Speed it up ~3x with Blaze - change one line, pay less, keep your existing GitHub workflows. First 25 HWS readers to use code HACKING at checkout get 50% off the first year. Try it now for free!

Reserve your spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.