Creating Custom Rounded Corners in SwiftUI with a View Modifier
SwiftUI provides a powerful and intuitive way to build user interfaces across all Apple platforms. However, some common UI customizations require a bit of extra work. One such customization is applying rounded corners to specific corners of a view with a border (stroke). In this tutorial, we'll explore how to create a custom ViewModifier to simplify this process.
Why Do We Need a Custom Modifier?
By default, SwiftUI's cornerRadius modifier applies the radius to all corners of a view. If you want to round specific corners or add a border to a view with custom rounded corners, you often need to stack multiple modifiers like background, overlay, and clipShape. This can make your code verbose and harder to read.
Our custom roundedCorners modifier simplifies this by encapsulating all the necessary logic into a single, reusable modifier.
The Complete Code
Here's the custom ViewModifier and supporting structures we'll be discussing:
import SwiftUI
public struct RoundedCornerModifier: ViewModifier {
let radius: CGFloat
let corners: UIRectCorner
let strokeColor: Color
let lineWidth: CGFloat
public func body(content: Content) -> some View {
content
.clipShape(RoundedCorner(radius: radius, corners: corners))
.overlay(
RoundedCorner(radius: radius, corners: corners)
.stroke(strokeColor, lineWidth: lineWidth)
)
}
}
extension View {
public func roundedCorners(
radius: CGFloat,
corners: UIRectCorner = .allCorners,
strokeColor: Color = Color.primary,
lineWidth: CGFloat = 1.0
) -> some View {
self.modifier(
RoundedCornerModifier(
radius: radius,
corners: corners,
strokeColor: strokeColor,
lineWidth: lineWidth
)
)
}
}
public struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
public func path(in rect: CGRect) -> Path {
let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius)
)
return Path(path.cgPath)
}
}
Understanding the Components
1. RoundedCorner Shape
The RoundedCorner struct conforms to the Shape protocol and allows us to define a shape with specific corners rounded.
public struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
public func path(in rect: CGRect) -> Path {
let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius)
)
return Path(path.cgPath)
}
}
radius: The radius of the corners.corners: Specifies which corners to round usingUIRectCorner.path(in:): Creates aPathfrom aUIBezierPathwith the specified corners rounded.
2. RoundedCornerModifier ViewModifier
The RoundedCornerModifier applies the RoundedCorner shape to any view it's attached to.
public struct RoundedCornerModifier: ViewModifier {
let radius: CGFloat
let corners: UIRectCorner
let strokeColor: Color
let lineWidth: CGFloat
public func body(content: Content) -> some View {
content
.clipShape(RoundedCorner(radius: radius, corners: corners))
.overlay(
RoundedCorner(radius: radius, corners: corners)
.stroke(strokeColor, lineWidth: lineWidth)
)
}
}
clipShape: Clips the view to the specifiedRoundedCornershape.overlay: Adds a border (stroke) to the view with the sameRoundedCornershape.
3. View Extension
An extension on View provides a convenient way to use the modifier.
extension View {
public func roundedCorners(
radius: CGFloat,
corners: UIRectCorner = .allCorners,
strokeColor: Color = Color.primary,
lineWidth: CGFloat = 1.0
) -> some View {
self.modifier(
RoundedCornerModifier(
radius: radius,
corners: corners,
strokeColor: strokeColor,
lineWidth: lineWidth
)
)
}
}
Default Parameters: The
corners,strokeColor, andlineWidthparameters have default values, making the modifier flexible and easy to use.
How to Use the Custom Modifier
Here's how you can apply the roundedCorners modifier to a view:
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, SwiftUI!")
.padding()
.background(Color.blue)
.roundedCorners(
radius: 20,
corners: [.topLeft, .bottomRight],
strokeColor: Color.white,
lineWidth: 2
)
.padding()
}
}
Explanation
radius: 20: The corner radius to apply.corners: [.topLeft, .bottomRight]: Specifies that only the top-left and bottom-right corners should be rounded.strokeColor: Color.white: The color of the border.lineWidth: 2: The width of the border line.
Benefits of Using the Custom Modifier
Code Readability: Encapsulates multiple modifiers into one, making the code cleaner.
Reusability: Can be reused across different views in your project.
Flexibility: Easily customize which corners to round, the radius, border color, and line width.
Without the Custom Modifier
Without this custom modifier, achieving the same effect requires more verbose code:
Text("Hello, SwiftUI!")
.padding()
.background(Color.blue)
.clipShape(
RoundedCorner(radius: 20, corners: [.topLeft, .bottomRight])
)
.overlay(
RoundedCorner(radius: 20, corners: [.topLeft, .bottomRight])
.stroke(Color.white, lineWidth: 2)
)
.padding()
As you can see, we have to manually apply clipShape and overlay with the RoundedCorner shape each time.
Customizing Further
You can extend this modifier or create additional ones to fit your specific needs, such as adding shadows or gradients.
Adding a Shadow
extension View {
public func roundedCornersWithShadow(
radius: CGFloat,
corners: UIRectCorner = .allCorners,
strokeColor: Color = Color.primary,
lineWidth: CGFloat = 1.0,
shadowColor: Color = .black.opacity(0.2),
shadowRadius: CGFloat = 5,
shadowX: CGFloat = 0,
shadowY: CGFloat = 5
) -> some View {
self.modifier(
RoundedCornerModifier(
radius: radius,
corners: corners,
strokeColor: strokeColor,
lineWidth: lineWidth
)
)
.shadow(
color: shadowColor,
radius: shadowRadius,
x: shadowX,
y: shadowY
)
}
}
Conclusion
Creating custom view modifiers in SwiftUI allows you to encapsulate complex view customizations into reusable components. The roundedCorners modifier we've built simplifies the process of applying rounded corners to specific corners of a view and adding a border.
By using this modifier, you enhance code readability and maintainability in your SwiftUI projects.
Full Example in Context
import SwiftUI
struct CardView: View {
var body: some View {
VStack {
Image(systemName: "star.fill")
.foregroundColor(.white)
.padding()
.background(Color.orange)
.roundedCorners(
radius: 30,
corners: [.topLeft, .topRight],
strokeColor: .white,
lineWidth: 3
)
Text("Custom Rounded Corners")
.font(.headline)
.padding()
.background(Color.orange)
.roundedCorners(
radius: 30,
corners: [.bottomLeft, .bottomRight],
strokeColor: .white,
lineWidth: 3
)
}
.padding()
.background(Color.gray.opacity(0.2))
}
}
Preview
To see the result, you can use the SwiftUI preview:
struct CardView_Previews: PreviewProvider {
static var previews: some View {
CardView()
}
}
By integrating this custom modifier into your SwiftUI toolkit, you streamline the process of creating visually appealing and customized UI elements.