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?
Demo (Source code is available at the end of the post)
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
iOS 16 - Source code on gist
var style: RoundedCornerStyle = .continuous
actually getting used here? –
Rossini NSRect
doesn't have an equivalent corner object, and NSBezierPath
doesn't have the byRoundingCorners
parameter. –
Stiffler .shadow
) won't work. Use @Peter Kreinz answer below if you want to be able to add additional modifiers. –
Marlette ScrollView
. stackoverflow.com/questions/64259513 –
Sculptress clipShape
is not working as desired on ScrollView
s @Sculptress –
Disinclination 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 @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 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(...))
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
}
}
Shape
instead, you don't have to involve GeometryReader
. –
Disinterest path.move(to: CGPoint(x: tl, y: 0))
and that seemed to fix it. –
Handicapper .infinity
–
Malversation 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 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 .infinity
, like frame sizing where you want maximum expansion) –
Malversation 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:
//left Button
.cornerRadius(6, corners: [.topLeft, .bottomLeft])
//right Button
.cornerRadius(6, corners: [.topRight, .bottomRight])
NSRect
doesn't have an equivalent corner object, and NSBezierPath
doesn't have the byRoundingCorners
parameter. –
Stiffler 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:
- Add some padding to the bottom of your view
- Round all corners with
.cornerRadius(_:)
- 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:
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!
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)
}
}
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) )
}
}
iOS 16+
simply use UnevenRoundedRectangle
VStack {
UnevenRoundedRectangle(cornerRadii: .init(bottomTrailing: 50, topTrailing: 50))
.fill(.orange)
.frame(width: 200, height: 100)
}
Result:
One more option to the top cleanest (iOS 15+):
.background(Color.orange, in: RoundedRectangle(cornerRadius: 20))
.background(content: { Color.white.padding(.top, 20) })
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.
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)
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)))
© 2022 - 2024 — McMap. All rights reserved.