154

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?

pestophagous
  • 3,919
  • 3
  • 34
  • 41
Richard Witherspoon
  • 2,743
  • 3
  • 13
  • 28
  • 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. – Tamás Sengel Jun 01 '21 at 13:29

8 Answers8

530

Using as a custom modifier

You can use it like a normal modifier:

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

Demo

Demo Image

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:

Sample

Mojtaba Hosseini
  • 71,072
  • 19
  • 226
  • 225
  • Hi @Mojtaba, I like this solution. Is the `var style: RoundedCornerStyle = .continuous` actually getting used here? – Mark Moeykens Nov 23 '19 at 17:57
  • 60
    This solution is far cleaner than the accepted one. – Reda Lemeden Dec 13 '19 at 11:28
  • 1
    How to add border to the same view ? – Sorin Lica Feb 11 '20 at 10:21
  • 2
    Checkout [this answer](https://stackoverflow.com/a/58632759/5623035) for custom border @SorinLica – Mojtaba Hosseini Feb 11 '20 at 11:33
  • 5
    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 04 '20 at 23:12
  • This is a good solution but adding additional modifiers (e.g. `.shadow`) won't work. Use @Peter Kreinz answer below if you want to be able to add additional modifiers. – Darkisa Jun 20 '20 at 22:17
  • 7
    It was working fine until ios14, view from the bottom is disappearing – shanezzar Oct 06 '20 at 12:49
  • 6
    It does not work properly anymore in iOS14, I had some layout problems with it. – Cinn Oct 13 '20 at 13:56
  • @Cinn what layout problem exactly? – Mojtaba Hosseini Oct 13 '20 at 17:04
  • @MojtabaHosseini I have a List to which only the 2 top corners are rounded, building for iOS14 now hides the bottom of this List. Applying cornerRadius to a container solves it but then the List rows overflow in the rounded corners. – Cinn Oct 13 '20 at 18:13
  • Share me a link to a reproducible code. Maybe I can find a way. @Cinn – Mojtaba Hosseini Oct 13 '20 at 18:26
  • UIBezierPath not available on macOS. – Fin Oct 22 '20 at 15:46
  • Hi, @shanezzar, I met the same problem, please see my answer here: https://stackoverflow.com/a/64571117/4733603 – Kyle Xie Oct 30 '20 at 02:02
  • it stopped working on simulator 14.3 (entire view is not visible, if rounder corners are applied), however it stil works good on device with 14.3.... – Palejandro Dec 20 '20 at 17:12
  • I can't edit my previous comment , so here is another information: standard swift's function .cornerRadius() is not working properly too on simulator... view with rounded corners is also not visible, but it's content is visible.... strange behaviour. – Palejandro Dec 20 '20 at 17:30
  • 2
    This answer does not work if you are trying to round the corners of a `ScrollView`. https://stackoverflow.com/questions/64259513 – joshuakcockrell Apr 29 '21 at 04:21
  • 1
    It's a bug on iOS 14 that `clipShape` is not working as desired on `ScrollView`s @joshuakcockrell – Mojtaba Hosseini May 01 '21 at 10:21
  • 1
    This is just beautiful - love it and it works like a charm! Used it with Xcode 13.0 beta 5 (13A5212g) – Manu Rink Oct 07 '21 at 06:58
  • 1
    Somebody give this man an award – Josh Oct 21 '21 at 17:28
  • it works quite well except for animations. If I animate a view from one shape to another and the view has normal cornerRadius on all corners, the corner radius also animates. With this method though there is no animation of the cornerRadius. It just jumps to the new value. – alionthego Nov 13 '21 at 07:29
113

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
    }
}
Matty Cross
  • 32
  • 1
  • 5
kontiki
  • 32,877
  • 11
  • 99
  • 118
  • If you define a custom `Shape` instead, you don't have to involve `GeometryReader`. – rob mayoff Aug 11 '19 at 05: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 '20 at 01: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 '21 at 23:28
  • 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. – Eneko Alonso Jul 24 '21 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`. – Peter Schorn Jan 27 '22 at 22:44
  • @PeterSchorn SwiftUI handles rounding to infinity by doing the maximum amount of rounding for the current size, resulting in a capsule-like shape. It's nice to just say infinity and not have to calculate anything (note that SwiftUI has similar behaviors across the board for `.infinity`, like frame sizing where you want maximum expansion) – Trev14 Feb 17 '22 at 16:39
63

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])
Peter Kreinz
  • 7,308
  • 1
  • 58
  • 47
  • 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 04 '20 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. – Richard Groves Sep 23 '20 at 13:26
  • Hi, @RichardGroves, I met the exact same problem as you. See my answer here: https://stackoverflow.com/a/64571117/4733603 – Kyle Xie Oct 30 '20 at 01: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. – Richard Groves Nov 04 '20 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 06 '20 at 01:50
  • Why did you nest a struct within a struct? – Anthony Gedeon Feb 23 '21 at 23:16
  • @AnthonyGedeon: so the style belongs to the modifier – Peter Kreinz Feb 24 '21 at 10:52
11

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)
    }
}
orj
  • 12,716
  • 14
  • 62
  • 73
6

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) )
    }
}
ChrisR
  • 3,983
  • 1
  • 3
  • 17
1

Step 1: Crate a shape which can clip the view. We are going to use UIBeizerPath to implement rounding specific corner. Then copy the cgPath to Path.

//step 1 -- Create a shape view which can give shape

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

Step 2: Embed the shape to ViewModifier

//step 2 - embed shape in viewModifier to help use with ease

    struct CornerRadiusStyle: ViewModifier {
        var radius: CGFloat
        var corners: UIRectCorner
    
        func body(content: Content) -> some View {
            content
                .clipShape(CornerRadiusShape(radius: radius, corners: corners))
        }
    }

Step 3: Add Polymorphic function with signature as cornerRadius

//step 3 - crate a polymorphic view with same name as swiftUI's cornerRadius

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

Step 4: Use like following:

//use any way you want
struct ContentView: View {
    var body: some View {
        VStack {
            Rectangle()
                .frame(width: 100, height: 100, alignment: .center)
                .cornerRadius(radius: 20.0, corners: [.topLeft])
            
            Rectangle()
                .frame(width: 100, height: 100, alignment: .center)
                .cornerRadius(radius: 20.0, corners: [.topLeft, .bottomLeft])
            
            Rectangle()
                .frame(width: 100, height: 100, alignment: .center)
                .cornerRadius(radius: 20.0, corners: [.allCorners])
            
        }
    }
}

Done! :)

Fatemeh
  • 98
  • 11
0

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.

0

I have discovered a simple approach for rounding one-sided corners. It uses a 'positive-negative padding dance' to accomplish just what I was looking for.

So it basically 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 ob bottom or side corners. Hope this helps somebody!

Nikandr Marhal
  • 552
  • 1
  • 6
  • 9