View with rounded corners AND border
Asked Answered
H

5

75
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.

Hartzog answered 4/4, 2022 at 23:46 Comment(0)
H
153

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

Holmen answered 5/4, 2022 at 0:5 Comment(6)
Using strokeBorder() is better. It will not give prevent any problems with edge insets.Tyree
strokeBorder() is a crucial modifier! Otherwise it looks like a border made in MS PaintGi
strokeBorder is iOS 17.x and above. At some point that won't matter but it still does at the time of this writing.Darvon
@Darvon 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 modifiersHolmen
@Holmen - 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.Darvon
@Holmen - could you change your answer to use .strokeborder(.blue, lineWidth: 5)? I was really confused by the comments saying iOS 17+ but actually it works fine (as you described it).Otoscope
S
25

sometimes just adding the overlay may produce unsatisfying results (this is the case when the used view is designed with infinite width/height)

for example

so I'd recommend using an inset when trying to add rounded border

.overlay(
    RoundedRectangle(cornerRadius: 20)
        .inset(by: 5) // inset value should be same as lineWidth in .stroke
        .stroke(.blue, lineWidth: 5)
)
Succussion answered 29/11, 2022 at 11:53 Comment(2)
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!!Edward
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!Christmas
T
9
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

Tesch answered 21/2 at 9:58 Comment(1)
Great, Worked for me, thanks @PratikPuckery
U
8

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

Unavailing answered 27/8, 2023 at 17:28 Comment(0)
C
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.

Costplus answered 27/8, 2023 at 21:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.