101
VStack {
    Text("some text")
    Button("Ok") {}
        .foregroundColor(.cyan)
        .padding()
}
.padding()
.background(.red)
.border(.blue, width: 5)
.cornerRadius(20)

Screenshot

I want the entire view to have the blue border with rounded corners (instead of the red square overlapping the rounded blue border. How? I've tried seemingly all variations of ordering the modifiers.

0

7 Answers 7

206

SwiftUI borders have straight edges no matter what corner radius you apply (.cornerRadius simply clips the view to a rounded mask and doesn't adjust the border's appearance).

If you want a rounded border, you'll need to overlay and .stroke a rounded rectangle:

VStack {
    Text("some text")
    Button("Ok") {}
        .foregroundColor(.cyan)
        .padding()
}
.padding()
.background(.red)
.cornerRadius(20) /// make the background rounded
.overlay( /// apply a rounded border
    RoundedRectangle(cornerRadius: 20)
        .stroke(.blue, lineWidth: 5)
)

Result:

Rounded blue border

Sign up to request clarification or add additional context in comments.

8 Comments

Using strokeBorder() is better. It will not give prevent any problems with edge insets.
strokeBorder() is a crucial modifier! Otherwise it looks like a border made in MS Paint
strokeBorder is iOS 17.x and above. At some point that won't matter but it still does at the time of this writing.
@C6Silver there's a version of strokeBorder that's available in iOS 13. The newer one is just more flexible in terms of how you can stack modifiers
@aheze - thanks for that. Every time I have tried a "strokeBorder" it tells me it requires iOS 17.x. I wish Apple would make the version requirements more clear within Xcode.
|
33

I'd recommend using an overlay with inset when trying to add rounded border:

.overlay {
    let lineWidth: CGFloat = 50
    RoundedRectangle(cornerRadius: 20)
        .inset(by: -lineWidth / 2)
        .stroke(.blue, lineWidth: lineWidth)
}

Example

5 Comments

This answer works for HStack etc. too, great! The only thing that worked for me, most other solutions are for buttons but leave strange line width differences, this here does not. Exactly what your sample image shows was my problem, thx!!
This solution works incredibly well with things like LazyVGrids that have a spacing and you use .frame(maxWidth: .infinity) on the elements. Before the spacing was too small then, now with the inset it works like a charm!
I think you have to set negative inset here if you want outside border.
border automatically goes outside the view, the inset brings it inside so you could just remove the .inset modifier
No, Levan, you are not right. You can verify it using your own example by setting border thickness to some ridiculously big values. The inset should be negative and it should be half of the border thickness, if we don't want the original view to be covered.
26
struct RoundedCorner: Shape {
    var radius: CGFloat = .infinity
    var corners: UIRectCorner = .allCorners
    
    func path(in rect: CGRect) -> Path {
        let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
        return Path(path.cgPath)
    }
}

extension View {
    func roundedCornerWithBorder(lineWidth: CGFloat, borderColor: Color, radius: CGFloat, corners: UIRectCorner) -> some View {
        clipShape(RoundedCorner(radius: radius, corners: corners) )
            .overlay(RoundedCorner(radius: radius, corners: corners)
                .stroke(borderColor, lineWidth: lineWidth))
    }
}

How to use different examples.

Example: 1 - Using All Side Corner Radius with Border.

  var body: some View {
        Text("Some Text")
            .padding()
            .background(.red)
            .roundedCornerWithBorder(lineWidth: 2, borderColor: .blue, radius: 4, corners: [.allCorners])        
    }

enter image description here

Example: 2 - For Top Left & Bottom Left Side Corner Radius with Border.

var body: some View {
    Text("Some Text")
        .padding()
        .background(.red)
        .roundedCornerWithBorder(lineWidth: 2, borderColor: .blue, radius: 20, corners: [.topLeft, .bottomLeft])
}

enter image description here

Example: 3 - For Top Right & Bottom Right Side Corner Radius with Border.

var body: some View {
    Text("Some Text")
        .padding()
        .background(.red)
        .roundedCornerWithBorder(lineWidth: 2, borderColor: .blue, radius: 20, corners: [.topRight, .bottomRight])
}

enter image description here

Example: 4 - For Top Left & Bottom Right Side Corner Radius with Border.

var body: some View {
    Text("Some Text")
        .padding()
        .background(.red)
        .roundedCornerWithBorder(lineWidth: 2, borderColor: .blue, radius: 20, corners: [.topLeft, .bottomRight])
}

enter image description here

Comments

10

Adding over the other answers, here is a neat extension for your SwiftUI app:

fileprivate struct ModifierCornerRadiusWithBorder: ViewModifier {
    var radius: CGFloat
    var borderLineWidth: CGFloat = 1
    var borderColor: Color = .gray
    var antialiased: Bool = true
    
    func body(content: Content) -> some View {
        content
            .cornerRadius(self.radius, antialiased: self.antialiased)
            .overlay(
                RoundedRectangle(cornerRadius: self.radius)
                    .inset(by: self.borderLineWidth)
                    .strokeBorder(self.borderColor, lineWidth: self.borderLineWidth, antialiased: self.antialiased)
            )
    }
}

extension View {
    func cornerRadiusWithBorder(radius: CGFloat, borderLineWidth: CGFloat = 1, borderColor: Color = .gray, antialiased: Bool = true) -> some View {
        modifier(ModifierCornerRadiusWithBorder(radius: radius, borderLineWidth: borderLineWidth, borderColor: borderColor, antialiased: antialiased))
    }
}

You can then just do

VStack {
    Text("some text")
    Button("Ok") {}
        .foregroundColor(.cyan)
        .padding()
}
.padding()
.background(.red)
.cornerRadiusWithBorder(radius: 20, borderLineWidth: 5, borderColor: . blue)

to achieve this

enter image description here

Comments

1

Using ".stroke" might be considered a best practice approach. However, if you'd like to try, the following method could also suffice:

RoundedRectangle(cornerRadius: 20)
    .foregroundColor(.blue)
    .frame(width: 100, height: 100)
    .overlay(
        RoundedRectangle(cornerRadius: 20)
            .foregroundColor(.red)
            .frame(width: 90, height: 90)
            .overlay(
                VStack {
                    Text("some text")
                    Button("Ok") {}
                }
            )
    )

You will get the same result with this.

Comments

0

Instead of using cornerRadius, consider using .clipShape(Capsule())

VStack(alignment: .leading) {
                        
                        Text(address.fullAddress ?? "")
                            .baamStyle()
                        Text(address.postalCode ?? "")
                            .baamStyle()
                    }
                    .padding()
                    .background(SwiftUI.Color(UIColor.Baam.cardViewBackground.color))
                    .clipShape(Capsule())

Comments

0
VStack {
    Text("some text")
        Button("Ok") {}
            .foregroundColor(.cyan)
            .padding()
    }
    .padding()
    .background(.red)
    .cornerRadius(20)
    .overlay(
        RoundedRectangle(cornerRadius: 20)
            .stroke(Color.blue, lineWidth: 5)
    )
}

enter image description here

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.