How to round specific corners of a View?
Asked Answered
A

11

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?

Ancell answered 25/6, 2019 at 18:50 Comment(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.Versed
D
827
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

Disinclination answered 29/10, 2019 at 11:1 Comment(25)
Hi @Mojtaba, I like this solution. Is the var style: RoundedCornerStyle = .continuous actually getting used here?Rossini
This solution is far cleaner than the accepted one.Trictrac
Checkout this answer for custom border @SorinLicaDisinclination
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.Stiffler
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.Marlette
It was working fine until ios14, view from the bottom is disappearingDaft
It does not work properly anymore in iOS14, I had some layout problems with it.Amabelle
@Amabelle what layout problem exactly?Disinclination
@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.Amabelle
Share me a link to a reproducible code. Maybe I can find a way. @AmabelleDisinclination
UIBezierPath not available on macOS.Sirmons
Hi, @shanezzar, I met the same problem, please see my answer here: https://mcmap.net/q/23529/-swiftui-issue-on-ios-14-with-scrollview-hstack-content-being-cut-offAquamarine
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....Improvvisatore
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.Improvvisatore
This answer does not work if you are trying to round the corners of a ScrollView. stackoverflow.com/questions/64259513Sculptress
It's a bug on iOS 14 that clipShape is not working as desired on ScrollViews @SculptressDisinclination
This is just beautiful - love it and it works like a charm! Used it with Xcode 13.0 beta 5 (13A5212g)Paulenepauletta
Somebody give this man an awardSimilarity
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.Fortress
Is there an alternative to UIRectCorner that can respect the layout direction (i.e. topLeading instead of topLeft) so that leading means left in LTR and right in RTL layout direction?Extensity
This answer would be substantially improved if it included the source code directly in the post as text, rather than images or links. Text in images cannot be copied to use it, and images cannot be read by users with screen readers. Links may become invalid if the linked site changes.Drawstring
@RyanM Thanks for your attention. The source code is included. The images are extra sample project for better understanding and playground and I think adding the code in the same style of the actual code block will reduce the focus of the readerDisinclination
Thanks for the thorough answer!Any idea why the iOS 16 solution requires Xcode 15? If it's in SwiftUI for iOS 16, I reckon it ought to work with Xcode 14, too.Seeley
@Seeley it's a @backDeployed, You need newer module (included in Xcode 15) to make it work but it can work on older runtimes since it's just a wrapper around the old solutions ;)Disinclination
I am proud of you @MojtabaHosseiniGalla
C
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
    }
}
Clerc answered 25/6, 2019 at 23:31 Comment(6)
If you define a custom Shape instead, you don't have to involve GeometryReader.Disinterest
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.Handicapper
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 .infinityMalversation
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.Diplo
A custom Shape is clearly the best way to accomplish this because it uses Native SwiftUI. @Malversation It makes no sense to round a corner to .infinity.Abercromby
@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)Malversation
W
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])
Whitecollar answered 3/3, 2020 at 17:21 Comment(8)
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.Stiffler
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.Bennet
Hi, @RichardGroves, I met the exact same problem as you. See my answer here: https://mcmap.net/q/23529/-swiftui-issue-on-ios-14-with-scrollview-hstack-content-being-cut-offAquamarine
@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.Bennet
@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.Aquamarine
Why did you nest a struct within a struct?Snips
@AnthonyGedeon: so the style belongs to the modifierWhitecollar
This solution works, but won't work when you need the element to have just a stroke instead of a .fill(). Since it clips the shape, it will clip the stroke.Overwhelming
F
66

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!

Farnsworth answered 30/5, 2022 at 14:8 Comment(4)
So Clean, it works perfectly!Vapor
Best answer by far, since no new files necessary! Thanks!Unknow
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.Slaphappy
Thanks! Very cool approach for round pairs!Neuroblast
P
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)
    }
}
Pulchia answered 27/8, 2019 at 0:48 Comment(0)
E
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) )
    }
}
Etheridge answered 24/2, 2022 at 8:22 Comment(0)
R
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

Rhines answered 2/11, 2023 at 12:13 Comment(1)
How is this different from the accepted answer (section iOS 16+)?Disinclination
J
6

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

Jackquelinejackrabbit answered 4/2, 2023 at 19:56 Comment(0)
A
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.

Arezzini answered 1/7, 2021 at 21:6 Comment(0)
P
1

if you want to achieve this  bubble

Use the code in this link where the struct of type shape is RoundedCorners:Shape{ //} https://mcmap.net/q/23505/-how-to-round-specific-corners-of-a-view

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)
Papillose answered 2/2, 2023 at 7:42 Comment(0)
B
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)))
Bluing answered 8/3 at 20:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.