bjorn.lau
bjorn.lau

Reputation: 1188

Round specific border corner with SwiftUI

I am trying to add a border to a view and round the topLeading and topTrailing corner only. It seems extremely difficult to achieve? It's easy enough to just round the corners with this extension:

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

extension View {
    func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
        clipShape( RoundedCorner(radius: radius, corners: corners) )
    }
}

But this does not work when you apply a stroke. Any ideas how to achieve this? Rounded stroke example

Upvotes: 2

Views: 2105

Answers (2)

Paul B
Paul B

Reputation: 5115

If for you need to implement specific radius for specific corners here is a solution in pure SwiftUI.

// Shape itself
struct SpecificCornerShape: Shape {
    var topLeft: CGFloat = 0
    var bottomLeft: CGFloat = 0
    var topRight: CGFloat = 0
    var bottomRight: CGFloat = 0
    
    func path(in rect: CGRect) -> Path {
        let minX = rect.minX
        let minY = rect.minY
        let maxX = rect.maxX
        let maxY = rect.maxY
        
        var path = Path()
        path.move(to: CGPoint(x: minX + topLeft, y: minY))
        path.addLine(to: CGPoint(x: maxX - topRight, y: minY))
        path.addArc(
            center: CGPoint(x: maxX - topRight, y: minY + topRight),
            radius: topRight,
            startAngle: Angle(radians: 3 * .pi / 2),
            endAngle: Angle.zero,
            clockwise: false)
        path.addLine(to: CGPoint(x: maxX, y: maxY - bottomRight))
        path.addArc(
            center: CGPoint(x: maxX - bottomRight, y: maxY - bottomRight),
            radius: bottomRight,
            startAngle: Angle.zero,
            endAngle: Angle(radians: .pi / 2),
            clockwise: false)
        path.addLine(to: CGPoint(x: minX + bottomLeft, y: maxY))
        path.addArc(
            center: CGPoint(x: minX + bottomLeft, y: maxY - bottomLeft),
            radius: bottomLeft,
            startAngle: Angle(radians: .pi / 2),
            endAngle: Angle(radians: .pi),
            clockwise: false)
        path.addLine(to: CGPoint(x: minX, y: minY + topLeft))
        path.addArc(
            center: CGPoint(x: minX + topLeft, y: minY + topLeft),
            radius: topLeft,
            startAngle: Angle(radians: .pi),
            endAngle: Angle(radians: 3 * .pi / 2),
            clockwise: false)
        path.closeSubpath()
        return path
    }
}

// Helpful modifier for clipping and stroking with any shape and style (color, gradient, material)

struct ClipAndStroke<S: Shape, SH: ShapeStyle>: ViewModifier {
    let shape: S
    let shapeStyle: SH
    let lineWidth: CGFloat
    func body(content: Content) -> some View {
        content
            .clipShape(shape)
            .overlay(
                shape
                    .stroke(shapeStyle, lineWidth: lineWidth)
            )
    }
}


// Convenience function to apply our modifier 
extension View {
    func clipAndStroke<S: Shape, SH: ShapeStyle>(_ shape: S, shapeStyle: SH, lineWidth: CGFloat = 1) -> some View {
        modifier(ClipAndStroke(shape: shape, shapeStyle: shapeStyle, lineWidth: lineWidth))
    }
}

Usage sample:

struct ContentView: View {
    var body: some View {
        Color.yellow
            .frame(width: 300, height: 150)
            .clipAndStroke(SpecificCornerShape(bottomLeft: 10, bottomRight: 30), shapeStyle: .red, lineWidth: 15)
    }
}

Upvotes: 2

Shawn
Shawn

Reputation: 781

The common way to add a border to a view in SwiftUI is via the .overlay() modifier. Using the RoundedCorner shape you've already made, we can modify this answer to create a new modifier that'll both round the shape and add a border.

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

extension View {
    public func borderRadius<S>(_ content: S, width: CGFloat = 1, cornerRadius: CGFloat, corners: UIRectCorner) -> some View where S : ShapeStyle {
        let roundedRect = RoundedCorner(radius: cornerRadius, corners: [.topLeft, .topRight])
        return clipShape(roundedRect)
            .overlay(roundedRect.stroke(content, lineWidth: width))
    }
}

Usage:

Color.yellow
.borderRadius(Color.red, width: 15, cornerRadius: 25, corners: [.topLeft, .topRight])
.padding()
.frame(width: 300, height: 150)

Upvotes: 3

Related Questions