Paul
Paul

Reputation: 1431

Given a custom Shape, how do you increase the size to fit a frame in SwiftUI?

I have a series of custom defined Shape and a ZStack that has a set frame, is there a way to increase the size of each custom Shape that can be of various sizes to fit the Zstack frame?

enter image description here

Code that will draw various Shape given points.

struct DrawShape: Shape {

    var points: [CGPoint]
    
    func path(in rect: CGRect) -> Path {
        var path: Path = Path()

        guard let startingPoint = points.first else {
            return path
        }

        path.move(to: startingPoint)

        for pointLocation in points {
            
            path.addLine(to: pointLocation)
            
        }

        return path
    }
}

Upvotes: 3

Views: 1589

Answers (2)

Paul B
Paul B

Reputation: 5125

There is a way to fit any path inside the proposed rectangle keeping aspect ratio. And it's rather short way.

struct FittingShape: Shape {
    var absolutePath: Path
    
    func path(in rect: CGRect) -> Path {
        let boundingRect = absolutePath.boundingRect
        let scale = min(rect.width/boundingRect.width, rect.height/boundingRect.height)
        let scaled = absolutePath.applying(.init(scaleX: scale, y: scale))
        let scaledBoundingRect = scaled.boundingRect
        let offsetX = scaledBoundingRect.midX - rect.midX
        let offsetY = scaledBoundingRect.midY - rect.midY
        return scaled.offsetBy(dx: -offsetX, dy: -offsetY)
    }
}

Test it:

struct ContentView: View {
    var path = Path {
        $0.addLines([
            CGPoint(x: 0, y: 0),
            CGPoint(x: 200, y: 0),
            CGPoint(x: 100, y: 250),
            CGPoint(x: 0, y: 0)
        ])
    }
    
    var body: some View {
        ZStack {
            Color.blue
            FittingShape(absolutePath: path) // The shape is centered
            //DrawShape(points: points) // This gives sligtly different result: the shape is sticked to the top
        }
        .frame(width: 300, height: 500)
    }
}

Upvotes: 1

aheze
aheze

Reputation: 30554

Here's what I came up with:

  1. Draw the path, as usual
  2. Get the boundingRect of the path
  3. Determine the scale factor needed for the path
    • rect is the available area of the shape
    • if boundingRect is fatter than rect, use their widths
    • if boundingRect is skinnier than rect, use their heights
  4. Draw a new path, this time with the scaled points and offset (if needed)
    • if the origin of the path isn't (0, 0), each new point needs to have the origin subtracted
  5. Return the new path
struct DrawShape: Shape {
    var points: [CGPoint]
    
    func path(in rect: CGRect) -> Path {
        
        /// draw the path
        var path = Path()
        guard let startingPoint = points.first else { return path }
        path.move(to: startingPoint)
        for pointLocation in points {
            path.addLine(to: pointLocation)
        }
        
        /// aspect fit scale
        let scale: CGFloat
        if path.boundingRect.height / path.boundingRect.width < rect.height / rect.width {
            scale = rect.width / path.boundingRect.width /// boundingRect is fatter
        } else {
            scale = rect.height / path.boundingRect.height /// boundingRect is skinnier
        }

        /// draw the scaled path
        var scaledPath = Path()
        scaledPath.move(to: convertPoint(startingPoint, offset: path.boundingRect.origin, scale: scale))
        for pointLocation in points {
            scaledPath.addLine(to: convertPoint(pointLocation, offset: path.boundingRect.origin, scale: scale))
        }
        
        /// return the scaled path
        return scaledPath
    }
    
    /// point = original point
    /// offset = in case the origin of `boundingRect` isn't (0,0), make sure to offset each point
    /// scale = how much to scale the point by
    func convertPoint(_ point: CGPoint, offset: CGPoint, scale: CGFloat) -> CGPoint {
        return CGPoint(x: (point.x - offset.x) * scale, y: (point.y - offset.y) * scale)
    }
}

struct ContentView: View {
    var body: some View {
        ZStack {
            Color.blue
            
            DrawShape(points: [
                CGPoint(x: 0, y: 0),
                CGPoint(x: 200, y: 0),
                CGPoint(x: 100, y: 250),
                CGPoint(x: 0, y: 0)
            ])
        }
        .frame(width: 300, height: 500)
    }
}
Before After
Small triangle at top-left of blue background Triangle scaled to fit width of background

Upvotes: 2

Related Questions