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 using UIRectCorner.

  • path(in:): Creates a Path from a UIBezierPath with 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 specified RoundedCorner shape.

  • overlay: Adds a border (stroke) to the view with the same RoundedCorner shape.

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, and lineWidth parameters 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.