swiftswiftui

Why strokeBorder does not respect its border in SwiftUI?


I have an issue with my code which the strokeBorder does not stroke inside Border, if I use fill for my Shape it stays and respect to the border, but strokeBorder does not do the same, how to solve this issue?

import SwiftUI

struct ContentView: View {
    var body: some View {
        
        let myCustomInsettableShape = TestShape()
        
        ZStack {

            myCustomInsettableShape
                .fill(Color.white)

            myCustomInsettableShape
                .strokeBorder(Color.black.opacity(0.5), style: StrokeStyle(lineWidth: 50.0, lineCap: .round, lineJoin: .miter))
            
        }
        .background(Color.red)
        .padding(50)
    }
}

struct TestShape: InsettableShape {
    
    var insetAmount: CGFloat = .zero
    
    func path(in rect: CGRect) -> Path {
        
        // Adjust the rect to account for half the stroke width to ensure it stays inside
        let insetRect = rect.insetBy(dx: insetAmount, dy: insetAmount)
        
        // Create the path for the shape
        return Path { path in
            path.move(to: CGPoint(x: insetRect.minX, y: insetRect.midY))
            path.addLine(to: CGPoint(x: insetRect.midX, y: insetRect.minY))
            path.addLine(to: CGPoint(x: insetRect.maxX, y: insetRect.midY))
            path.addLine(to: CGPoint(x: insetRect.midX, y: insetRect.maxY))
            path.closeSubpath()
        }
        
    }
    
    func inset(by amount: CGFloat) -> some InsettableShape {
        var myShape = self
        myShape.insetAmount += amount
        return myShape
    }
    
}

enter image description here


Solution

  • When you use .strokeBorder, it relies on .inset working properly, as explained in the documentation:

    Returns a view that is the result of insetting self by style.lineWidth / 2, stroking the resulting shape with style, and then filling with content.

    I think the way the inset is being applied when TestShape creates a path is probably too primitive. Let's try a test:

    ZStack {
        TestShape()
            .stroke()
    
        TestShape()
            .inset(by: 50)
            .stroke()
    }
    .padding(50)
    

    Screenshot

    I would have expected to see a smaller diamond shape with the edges running parallel to the larger shape.

    So let's fix the path function first. This requires using a little Pythagoras and trigonometry:

    Diagram

    func path(in rect: CGRect) -> Path {
        Path { path in
            let dx: CGFloat
            let dy: CGFloat
            if insetAmount > 0 && rect.width > 0 && rect.height > 0 {
                let diagonal = sqrt((rect.width * rect.width) + (rect.height * rect.height))
                let cosAlpha = rect.height / diagonal
                dx = insetAmount / cosAlpha
                let sinAlpha = rect.width / diagonal
                dy = insetAmount / sinAlpha
            } else {
                dx = 0
                dy = 0
            }
            path.move(to: CGPoint(x: rect.minX + dx, y: rect.midY))
            path.addLine(to: CGPoint(x: rect.midX, y: rect.minY + dy))
            path.addLine(to: CGPoint(x: rect.maxX - dx, y: rect.midY))
            path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY - dy))
            path.closeSubpath()
        }
    }
    

    Trying the test again:

    Screenshot

    That's looking better. Now let's try strokeBorder again with the original code:

    Screenshot

    Problem solved!