262

I know you can use .cornerRadius() to round all the corners of a SwiftUI view but is there a way to round only specific corners such as the top?

1
  • 1
    I ended up skipping SwiftUI because no matter what I did, the performance was terrible. In the end, I ended up using the maskedCorners property of the CALayer of my representable UIKit view. Jun 1, 2021 at 13:29

11 Answers 11

822
Demo (Source code is available at the end of the post)

Demo Image

iOS 16+ built-in modifier (Xcode 15 needed)

Clip the view using the new UnevenRoundedRectangle:

.clipShape(
    .rect(
        topLeadingRadius: 0,
        bottomLeadingRadius: 20,
        bottomTrailingRadius: 0,
        topTrailingRadius: 20
    )
)

⚠️ Note: Although it works from iOS 16, .rect needs Xcode 15 to be available.


iOS 13+

You can use it like a normal modifier:

.cornerRadius(20, corners: [.topLeft, .bottomRight])

You need to implement a simple extension on View like this:

extension View {
    func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
        clipShape( RoundedCorner(radius: radius, corners: corners) )
    }
}

And here is the struct behind this:

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)
    }
}

You can also use the shape directly as a clipping mask.


Sample Project:

iOS 13 - Source code on gist Sample iOS 14


iOS 16 - Source code on gist Sample iOS 16

25
  • 77
    This solution is far cleaner than the accepted one. Dec 13, 2019 at 11:28
  • 2
    Checkout this answer for custom border @SorinLica Feb 11, 2020 at 11:33
  • 6
    Do you know how this would be implemented in a SwiftUI view for macOS (not Catalyst)? Looks like NSRect doesn't have an equivalent corner object, and NSBezierPath doesn't have the byRoundingCorners parameter.
    – TheNeil
    Mar 4, 2020 at 23:12
  • 10
    It was working fine until ios14, view from the bottom is disappearing
    – shanezzar
    Oct 6, 2020 at 12:49
  • 8
    It does not work properly anymore in iOS14, I had some layout problems with it.
    – Cinn
    Oct 13, 2020 at 13:56
126

There are two options, you can use a View with a Path, or you can create a custom Shape. In both cases you can use them standalone, or in a .background(RoundedCorders(...))

enter image description here

Option 1: Using Path + GeometryReader

(more info on GeometryReader: https://swiftui-lab.com/geometryreader-to-the-rescue/)

struct ContentView : View {
    var body: some View {
        
        Text("Hello World!")
            .foregroundColor(.white)
            .font(.largeTitle)
            .padding(20)
            .background(RoundedCorners(color: .blue, tl: 0, tr: 30, bl: 30, br: 0))
    }
}
struct RoundedCorners: View {
    var color: Color = .blue
    var tl: CGFloat = 0.0
    var tr: CGFloat = 0.0
    var bl: CGFloat = 0.0
    var br: CGFloat = 0.0
    
    var body: some View {
        GeometryReader { geometry in
            Path { path in
                
                let w = geometry.size.width
                let h = geometry.size.height

                // Make sure we do not exceed the size of the rectangle
                let tr = min(min(self.tr, h/2), w/2)
                let tl = min(min(self.tl, h/2), w/2)
                let bl = min(min(self.bl, h/2), w/2)
                let br = min(min(self.br, h/2), w/2)
                
                path.move(to: CGPoint(x: w / 2.0, y: 0))
                path.addLine(to: CGPoint(x: w - tr, y: 0))
                path.addArc(center: CGPoint(x: w - tr, y: tr), radius: tr, startAngle: Angle(degrees: -90), endAngle: Angle(degrees: 0), clockwise: false)
                path.addLine(to: CGPoint(x: w, y: h - br))
                path.addArc(center: CGPoint(x: w - br, y: h - br), radius: br, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 90), clockwise: false)
                path.addLine(to: CGPoint(x: bl, y: h))
                path.addArc(center: CGPoint(x: bl, y: h - bl), radius: bl, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 180), clockwise: false)
                path.addLine(to: CGPoint(x: 0, y: tl))
                path.addArc(center: CGPoint(x: tl, y: tl), radius: tl, startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false)
                path.closeSubpath()
            }
            .fill(self.color)
        }
    }
}

Option 2: Custom Shape

struct ContentView : View {
    var body: some View {
        
        Text("Hello World!")
            .foregroundColor(.white)
            .font(.largeTitle)
            .padding(20)
            .background(RoundedCorners(tl: 0, tr: 30, bl: 30, br: 0).fill(Color.blue))
    }
}

struct RoundedCorners: Shape {
    var tl: CGFloat = 0.0
    var tr: CGFloat = 0.0
    var bl: CGFloat = 0.0
    var br: CGFloat = 0.0
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        let w = rect.size.width
        let h = rect.size.height
        
        // Make sure we do not exceed the size of the rectangle
        let tr = min(min(self.tr, h/2), w/2)
        let tl = min(min(self.tl, h/2), w/2)
        let bl = min(min(self.bl, h/2), w/2)
        let br = min(min(self.br, h/2), w/2)
        
        path.move(to: CGPoint(x: w / 2.0, y: 0))
        path.addLine(to: CGPoint(x: w - tr, y: 0))
        path.addArc(center: CGPoint(x: w - tr, y: tr), radius: tr,
                    startAngle: Angle(degrees: -90), endAngle: Angle(degrees: 0), clockwise: false)
        
        path.addLine(to: CGPoint(x: w, y: h - br))
        path.addArc(center: CGPoint(x: w - br, y: h - br), radius: br,
                    startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 90), clockwise: false)
        
        path.addLine(to: CGPoint(x: bl, y: h))
        path.addArc(center: CGPoint(x: bl, y: h - bl), radius: bl,
                    startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 180), clockwise: false)
        
        path.addLine(to: CGPoint(x: 0, y: tl))
        path.addArc(center: CGPoint(x: tl, y: tl), radius: tl,
                    startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false)
        path.closeSubpath()

        return path
    }
}
6
  • If you define a custom Shape instead, you don't have to involve GeometryReader.
    – rob mayoff
    Aug 11, 2019 at 5:24
  • Just a small correction on option 2: I think the path starts at the wrong x value since it looks to cut off the top line in its left half. I changed the path start point to path.move(to: CGPoint(x: tl, y: 0)) and that seemed to fix it.
    – Alex H
    Nov 24, 2020 at 1:21
  • This isn't as clean as answers below, but it's the only one that works as of iOS 14 when I want to round 3 corners. The other method ends up rounding all 4 when I want them rounded to .infinity
    – Trev14
    Mar 15, 2021 at 23:28
  • 1
    While using UIBezierPath works well on iOS, it does not work on macOS or other places were UIKit is not available. Manually drawing the path in pure SwiftUI works great on all Apple platforms. Jul 24, 2021 at 21:42
  • A custom Shape is clearly the best way to accomplish this because it uses Native SwiftUI. @Trev14 It makes no sense to round a corner to .infinity. Jan 27, 2022 at 22:44
86

View Modifiers made it easy:

struct CornerRadiusStyle: ViewModifier {
    var radius: CGFloat
    var corners: UIRectCorner
    
    struct CornerRadiusShape: 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)
        }
    }

    func body(content: Content) -> some View {
        content
            .clipShape(CornerRadiusShape(radius: radius, corners: corners))
    }
}

extension View {
    func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
        ModifiedContent(content: self, modifier: CornerRadiusStyle(radius: radius, corners: corners))
    }
}

Example:

enter image description here

//left Button
.cornerRadius(6, corners: [.topLeft, .bottomLeft])

//right Button
.cornerRadius(6, corners: [.topRight, .bottomRight])
8
  • 1
    Do you know how this would be implemented in a SwiftUI view for macOS (not Catalyst)? Looks like NSRect doesn't have an equivalent corner object, and NSBezierPath doesn't have the byRoundingCorners parameter.
    – TheNeil
    Mar 4, 2020 at 23:09
  • Any else using this, or the above version on iOS 14? I find it clips any scrollview to the edges - same code runs fine on iOS 13 devices/simulators. Sep 23, 2020 at 13:26
  • Hi, @RichardGroves, I met the exact same problem as you. See my answer here: stackoverflow.com/a/64571117/4733603
    – Kyle Xie
    Oct 30, 2020 at 1:59
  • @KyleXie Thanks but I need it for cases where just 2 corners are rounded and there is no standard shape to do that, which is why I'd got to the custom path shape in the first place. Nov 4, 2020 at 17:24
  • @RichardGroves, ah, I see. I currently use full rounded corners and use something else covered the bottom corners. I know it's really hacking, but I have no other way to make it work.
    – Kyle Xie
    Nov 6, 2020 at 1:50
65

If you need only your top corners rounded - and in 99 of 100 cases I'm sure that's exactly what you're looking for - there a much simpler solution for the problem. Works like this:

  1. Add some padding to the bottom of your view
  2. Round all corners with .cornerRadius(_:)
  3. Remove the padding by applying negative padding of the same value
struct OnlyTopRoundedCornersDemo: View {
    let radius = 12 // radius we need
    var body: some View {
        Rectangle()
            .frame(height: 50)
            .foregroundColor(.black)
        .padding(.bottom, radius)
        .cornerRadius(radius)
        .padding(.bottom, -radius)
    }
}

The resulting view looks like this:

enter image description here

As you can see, its frame is perfectly aligned with its content (blue border). Same approach could be used to round pairs of bottom or side corners. Hope this helps!

4
  • 3
    So Clean, it works perfectly!
    – frndev
    Jun 9, 2022 at 10:12
  • Best answer by far, since no new files necessary! Thanks!
    – smat88dd
    Sep 29, 2022 at 14:47
  • 1
    I was looking to remove the optimization opportunity "A CAShapeLayer is used with a path that's a rect, a rounded-rect, or an ellipse. Instead, use an appropriately transformed plain CALayer with cornerRadius set", and this solution worked for me. I'd just point that radius should be defined as CGFloat, otherwise Xcode throws an error.
    – Alex Luque
    Jun 1, 2023 at 15:58
  • Thanks! Very cool approach for round pairs!
    – kollein
    Sep 30, 2023 at 12:01
13

Another option (maybe better) is actually to step back to UIKIt for this. Eg:

struct ButtonBackgroundShape: Shape {

    var cornerRadius: CGFloat
    var style: RoundedCornerStyle

    func path(in rect: CGRect) -> Path {
        let path = UIBezierPath(roundedRect: rect, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
        return Path(path.cgPath)
    }
}
0
9

Here an adaption for macOS:

// defines OptionSet, which corners to be rounded – same as UIRectCorner
struct RectCorner: OptionSet {
    
    let rawValue: Int
        
    static let topLeft = RectCorner(rawValue: 1 << 0)
    static let topRight = RectCorner(rawValue: 1 << 1)
    static let bottomRight = RectCorner(rawValue: 1 << 2)
    static let bottomLeft = RectCorner(rawValue: 1 << 3)
    
    static let allCorners: RectCorner = [.topLeft, topRight, .bottomLeft, .bottomRight]
}


// draws shape with specified rounded corners applying corner radius
struct RoundedCornersShape: Shape {
    
    var radius: CGFloat = .zero
    var corners: RectCorner = .allCorners

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

        let p1 = CGPoint(x: rect.minX, y: corners.contains(.topLeft) ? rect.minY + radius  : rect.minY )
        let p2 = CGPoint(x: corners.contains(.topLeft) ? rect.minX + radius : rect.minX, y: rect.minY )

        let p3 = CGPoint(x: corners.contains(.topRight) ? rect.maxX - radius : rect.maxX, y: rect.minY )
        let p4 = CGPoint(x: rect.maxX, y: corners.contains(.topRight) ? rect.minY + radius  : rect.minY )

        let p5 = CGPoint(x: rect.maxX, y: corners.contains(.bottomRight) ? rect.maxY - radius : rect.maxY )
        let p6 = CGPoint(x: corners.contains(.bottomRight) ? rect.maxX - radius : rect.maxX, y: rect.maxY )

        let p7 = CGPoint(x: corners.contains(.bottomLeft) ? rect.minX + radius : rect.minX, y: rect.maxY )
        let p8 = CGPoint(x: rect.minX, y: corners.contains(.bottomLeft) ? rect.maxY - radius : rect.maxY )

        
        path.move(to: p1)
        path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY),
                    tangent2End: p2,
                    radius: radius)
        path.addLine(to: p3)
        path.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.minY),
                    tangent2End: p4,
                    radius: radius)
        path.addLine(to: p5)
        path.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.maxY),
                    tangent2End: p6,
                    radius: radius)
        path.addLine(to: p7)
        path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.maxY),
                    tangent2End: p8,
                    radius: radius)
        path.closeSubpath()

        return path
    }
}

// View extension, to be used like modifier:
// SomeView().roundedCorners(radius: 20, corners: [.topLeft, .bottomRight])
extension View {
    func roundedCorners(radius: CGFloat, corners: RectCorner) -> some View {
        clipShape( RoundedCornersShape(radius: radius, corners: corners) )
    }
}
9

iOS 16+

simply use UnevenRoundedRectangle

 VStack {
    UnevenRoundedRectangle(cornerRadii: .init(bottomTrailing: 50, topTrailing: 50))
                    .fill(.orange)
                    .frame(width: 200, height: 100)
 }

Result:

enter image description here

1
5

One more option to the top cleanest (iOS 15+):

.background(Color.orange, in: RoundedRectangle(cornerRadius: 20))
.background(content: { Color.white.padding(.top, 20) })

enter image description here

1

I'd like to add to Kontiki's answer;

If you're using option 2 and want to add a stroke to the shape, be sure to add the following right before returning the path:

path.addLine(to: CGPoint(x: w/2.0, y: 0))

Otherwise, the stroke will be broken from the top left corner to the middle of the top side.

1

if you want to achieve this  bubble

Use the code in this link where the struct of type shape is RoundedCorners:Shape{ //} https://stackoverflow.com/a/56763282/10637692

use below lines of code in this link before path.closeSubpath()

path.move(to: CGPoint(x: 280, y: 20))
path.addLine(to: CGPoint(x: w, y: 0))
path.addArc(center: CGPoint(x: w, y: 70), radius: br,   //x =  move triangle to right left
startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 180), clockwise: false)
0

enter image description here

Current version of reaching this would be;

VStack {
    Text("Top Leading & Trailing Corners")
}.frame(maxWidth: .infinity, maxHeight: 200)
 .background(.white)
 .clipShape(
 .rect(cornerRadii: RectangleCornerRadii(topLeading: 24, topTrailing: 24)))

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.