How to scale and arrange multiple UIBezierPath in one view in SwiftUI? - swiftui

I have multiple UIBezierPath, let's say two squares:
var squareA: UIBezierPath {
let bp = UIBezierPath()
bp.addLine(to: CGPoint(x: 0, y: 0))
bp.addLine(to: CGPoint(x: 100, y: 0))
bp.addLine(to: CGPoint(x: 100, y: 100))
bp.addLine(to: CGPoint(x: 0, y: 100))
return bp
}
var squareB: UIBezierPath {
let bp = UIBezierPath()
bp.move(to: CGPoint(x: 300, y: 200))
bp.addLine(to: CGPoint(x: 400, y: 200))
bp.addLine(to: CGPoint(x: 400, y: 300))
bp.addLine(to: CGPoint(x: 300, y: 300))
return bp
}
Now I would like to display these two squares in one view. This view should be centered and scaled down when I set a specific frame size. Some thing like this:
MyUIBezierPathView(paths: [squareA, squareB])
.frame(width: 200)
How can I solve this problem?
EDIT: I've tried some other solution guidelined by the link that #Aspari posted.
Combine all shapes:
extension UIBezierPath {
static func calculateBounds(paths: [UIBezierPath]) -> CGRect {
let myPaths = UIBezierPath()
for path in paths {
myPaths.append(path)
}
return (myPaths.bounds)
}
}
Now I'm calculating the new dimensions:
struct ShapeView: Shape {
let bezier: UIBezierPath
let pathBounds: CGRect
func path(in rect: CGRect) -> Path {
let scaleX = rect.width / pathBounds.width
let scaleY = rect.height / pathBounds.height
let scale = min(scaleX, scaleY)
return Path(bezier.cgPath)//.applying(CGAffineTransform(scaleX: scale, y: scale))
}
}
And finally in my view I use this view:
struct ShapeDrawer: View {
let pathBounds = UIBezierPath.calculateBounds(paths: [squareA, squareB])
ZStack{
ShapeView(bezier: squareA, pathBounds: pathBounds)
ShapeView(bezier: squareB, pathBounds: pathBounds)
}
.frame(width: 300, height: 300 * pathBounds.height / pathBounds.width)
.background(Color.green)
}
and this works well! But it seems that this solution only works with "straight shapes" like squares, rectangles and so on. If I use some shapes with quadratic curves my shapes are not anymore in the center of the view. Here is an example:
Squares:
Curved Shapes:
Code for the curved shapes:
var han1: UIBezierPath {
let shape = UIBezierPath()
shape.move(to: CGPoint(x: 108, y: 552))
shape.addQuadCurve(to: CGPoint(x: 120, y: 447), controlPoint: CGPoint(x: 118, y: 500))
shape.addQuadCurve(to: CGPoint(x: 153, y: 396), controlPoint: CGPoint(x: 123, y: 413))
shape.addQuadCurve(to: CGPoint(x: 177, y: 407), controlPoint: CGPoint(x: 168, y: 387))
shape.addQuadCurve(to: CGPoint(x: 157, y: 522), controlPoint: CGPoint(x: 195, y: 467))
shape.addQuadCurve(to: CGPoint(x: 126, y: 564), controlPoint: CGPoint(x: 144, y: 549))
shape.addQuadCurve(to: CGPoint(x: 113, y: 565), controlPoint: CGPoint(x: 117, y: 568))
shape.addQuadCurve(to: CGPoint(x: 108, y: 552), controlPoint: CGPoint(x: 107, y: 561))
shape.close()
return shape
}
var han2: UIBezierPath {
let shape = UIBezierPath()
shape.move(to: CGPoint(x: 288, y: 488))
shape.addQuadCurve(to: CGPoint(x: 292, y: 490), controlPoint: CGPoint(x: 289, y: 489))
shape.addQuadCurve(to: CGPoint(x: 410, y: 579), controlPoint: CGPoint(x: 371, y: 554))
shape.addQuadCurve(to: CGPoint(x: 421, y: 593), controlPoint: CGPoint(x: 425, y: 583))
shape.addQuadCurve(to: CGPoint(x: 398, y: 621), controlPoint: CGPoint(x: 415, y: 606))
shape.addQuadCurve(to: CGPoint(x: 362, y: 637), controlPoint: CGPoint(x: 382, y: 636))
shape.addQuadCurve(to: CGPoint(x: 351, y: 620), controlPoint: CGPoint(x: 349, y: 636))
shape.addQuadCurve(to: CGPoint(x: 347, y: 594), controlPoint: CGPoint(x: 354, y: 608))
shape.addQuadCurve(to: CGPoint(x: 289, y: 513), controlPoint: CGPoint(x: 326, y: 563))
shape.addCurve(to: CGPoint(x: 288, y: 488), controlPoint1: CGPoint(x: 271, y: 489), controlPoint2: CGPoint(x: 262, y: 473))
shape.close()
return shape
}
I'm not sure, but it feels like the control points in the quadratic curves are taken into account and are the reason for this offset?
EDIT 2: It's not a problem with the form of shape, but it's a problem with the position of my paths in the view. If there is no shape in my view that starts at CGPoint(x: 0, y: 0) which is the case for the curved shape, there will be this strange offset. How can I remove this?
EDIT 3: I've solved the problem. I had to change my view to:
struct ShapeView: Shape {
let bezier: UIBezierPath
let pathBounds: CGRect
func path(in rect: CGRect) -> Path {
let scaleX = rect.width / pathBounds.width
let scaleY = rect.height / pathBounds.height
let scale = min(scaleX, scaleY)
let path = Path(bezier.cgPath).applying(CGAffineTransform(translationX: -pathBounds.origin.x, y: -pathBounds.origin.y)) // <<<<<< this line
return path.applying(CGAffineTransform(scaleX: scale, y: scale))
}
}

Related

How can I round these corners on my custom shape?

I have made a custom 'slanted' rectangle that I would like to become a rounded 'slanted' rectangle. Here is a picture so far:
struct SlantedRectangle: Shape {
func path(in rect: CGRect) -> Path {
return Path { path in
path.move(to: CGPoint(x: rect.width, y: 30))
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.addLine(to: CGPoint(x: 0, y: rect.height))
path.addLine(to: CGPoint(x: 0, y: 0))
}
}
}
How can I round the top left and top right corners?
For rounded corners, you can use addArc method of Path.
struct SlantedRectangle: Shape {
let slanted: CGFloat
let radius: CGFloat
func path(in rect: CGRect) -> Path {
return Path { path in
path.move(to: CGPoint(x: rect.width - radius, y: slanted))
path.addArc(center: CGPoint(x: rect.width - radius, y: slanted + radius), radius: radius, startAngle: Angle(degrees: -90), endAngle: Angle(degrees: 0), clockwise: false)
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.addLine(to: CGPoint(x: 0, y: rect.height))
path.addLine(to: CGPoint(x: 0, y: radius))
path.addArc(center: CGPoint(x: radius, y: radius), radius: radius, startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false)
path.closeSubpath()
}
}
}
Now create your SlantedRectangle like this.
SlantedRectangle(slanted: 30, radius: 30)
Preview

I can't draw responsive custom button with Path in SwiftUI ( I used Paint Code)

I was using PaintCode app for line to code.
But I can't use this code.. Because this code is not responsive.. I don't want button sizes to be static.
I don't want to give a static number. How can I make responsive button with Custom Path ? You can download SVG file below
struct BarButton: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX - 14, y: .zero))
path.addCurve(to: CGPoint(x: 121.63, y: 11.28), control1: CGPoint(x: 116.2, y: -0.69), control2: CGPoint(x: 121.6, y: 4.65))
path.addCurve(to: CGPoint(x: 121.07, y: 14.95), control1: CGPoint(x: 121.63, y: 12.53), control2: CGPoint(x: 121.45, y: 13.76))
path.addLine(to: CGPoint(x: 116.14, y: 30.58))
path.addCurve(to: CGPoint(x: 104.63, y: 38.97), control1: CGPoint(x: 114.56, y: 35.6), control2: CGPoint(x: 109.89, y: 39))
path.addLine(to: CGPoint(x: 11.02, y: 38.44))
path.addCurve(to: CGPoint(x: -0.91, y: 26.44), control1: CGPoint(x: 4.42, y: 38.4), control2: CGPoint(x: -0.91, y: 33.04))
path.addLine(to: CGPoint(x: -0.91, y: 11.81))
path.addCurve(to: CGPoint(x: 11.04, y: -0.19), control1: CGPoint(x: -0.91, y: 5.21), control2: CGPoint(x: 4.43, y: -0.16))
return path
}
}
struct BarButton_Previews: PreviewProvider {
static var previews: some View {
BarButton()
.frame(width: 125, height: 40)
// .background(Color.orange)
}
}
WeTransfer File Link for SVG:
https://wetransfer.com/downloads/3cc5bab61ce0c676c95c3a8fbd3bd4cf20220506201936/9d9ce6

How to check whether one CGPoint is in a Shape?

Let's say I have a Shape:
struct ShapeA: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: CGPoint(x: 100, y: 100))
path.addQuadCurve(to: CGPoint(x: 110, y: 210), control: CGPoint(x: 130, y: 200))
path.addQuadCurve(to: CGPoint(x: 180, y: 240), control: CGPoint(x: 140, y: 240))
path.addQuadCurve(to: CGPoint(x: 200, y: 250), control: CGPoint(x: 150, y: 250))
path.closeSubpath()
}
}
}
and a single CGPoint: CGPoint(x: 100, y: 100). How can I check whether this CGPoint is in the ShapeA (or any other Shape)
path.contains(point)
(Create path by calling .path(in rect:) on a ShapeA instance.)
For self-intersecting paths, you should look at the eoFill option for dealing with even-odd fill rules.

UIBezierPath / Shape has strange positioning and sizing behaviour

I'm trying to convert some SVG data (some shapes for chinese characters) to an UIBezierPath. Each character consists of multiple UIBezierPath. This is an example for one UIBezierPath:
var mypath: UIBezierPath {
let bp = UIBezierPath()
bp.move(to: CGPoint(x: 147, y: 32))
bp.addQuadCurve(to: CGPoint(x: 203, y: 102), controlPoint: CGPoint(x: 181, y: 74))
bp.addQuadCurve(to: CGPoint(x: 271, y: 189), controlPoint: CGPoint(x: 242, y: 166))
bp.addQuadCurve(to: CGPoint(x: 274, y: 217), controlPoint: CGPoint(x: 287, y: 204))
bp.addQuadCurve(to: CGPoint(x: 229, y: 235), controlPoint: CGPoint(x: 258, y: 229))
bp.addQuadCurve(to: CGPoint(x: 193, y: 235), controlPoint: CGPoint(x: 204, y: 241))
bp.addQuadCurve(to: CGPoint(x: 190, y: 219), controlPoint: CGPoint(x: 183, y: 231))
bp.addQuadCurve(to: CGPoint(x: 143, y: 71), controlPoint: CGPoint(x: 199, y: 195))
bp.addQuadCurve(to: CGPoint(x: 125, y: 33), controlPoint: CGPoint(x: 134, y: 55))
bp.addCurve(to: CGPoint(x: 147, y: 32), controlPoint1: CGPoint(x: 113, y: 5), controlPoint2: CGPoint(x: 128, y: 9))
bp.close()
return bp
}
Since my final result consists of multiple UIBezierPaths I'm trying to scale them:
extension UIBezierPath {
static func calculateBounds(paths: [UIBezierPath]) -> CGRect {
let myPaths = UIBezierPath()
for path in paths {
myPaths.append(path)
}
return (myPaths.bounds)
}
}
struct ShapeView: Shape {
let bezier: UIBezierPath
let pathBounds: CGRect
func path(in rect: CGRect) -> Path {
let pointScale = (rect.width >= rect.height) ?
max(pathBounds.height, pathBounds.width) :
min(pathBounds.height, pathBounds.width)
let pointTransform = CGAffineTransform(scaleX: 1/pointScale, y: 1/pointScale)
let path = Path(bezier.cgPath).applying(pointTransform)
let multiplier = min(rect.width, rect.height)
let transform = CGAffineTransform(scaleX: multiplier, y: multiplier)
return path.applying(transform)
}
}
View:
struct Drawer: View {
let pathBounds = UIBezierPath.calculateBounds(paths: [mypath]) // to simplify I just add only one shape. Usually there are multiple
var body: some View {
ZStack{
ShapeView(bezier: mypath, pathBounds: pathBounds)
.background(Color.red)
}
}
}
This is the result that is displayed:
As you can see: The shape is not centered and it's bigger than the screen. What am I doing wrong and how can I achieve that the shapes will be centered and fit perfectly in the entire screen?
I'm not sure about what's that path transforming logic is about (if even you need it the defect is there), but just applying shape like
struct ShapeView: Shape {
let bezier: UIBezierPath
let pathBounds: CGRect
func path(in rect: CGRect) -> Path {
Path(bezier.cgPath) // << simple conversion
}
}
gives this (probably what you did expect):

Animating Paths in SwiftUI Shapes

I really like the Timer symbol in the SF Symbol collection, and I would like to animate it in SwiftUI. There are a couple of different ways to do this, the easy one is to make an image of the face, and another image of the hand, slap them in a ZStack, and then animate the Image("hand") view.
I could also draw the face and the hand as individual shapes and stack and animate them the same way, like this:
struct TimerFace: Shape {
func path(in rect: CGRect) -> Path {
var facePath = Path()
facePath.move(to: CGPoint(x: 50, y: 100))
facePath.addCurve(to: CGPoint(x: 100, y: 50), control1: CGPoint(x: 77.61, y: 100), control2: CGPoint(x: 100, y: 77.61))
facePath.addCurve(to: CGPoint(x: 50.05, y: 0), control1: CGPoint(x: 100, y: 22.44), control2: CGPoint(x: 77.66, y: 0))
facePath.addCurve(to: CGPoint(x: 44.74, y: 5.12), control1: CGPoint(x: 46.75, y: 0), control2: CGPoint(x: 44.74, y: 1.96))
facePath.addLine(to: CGPoint(x: 44.74, y: 22.01))
facePath.addCurve(to: CGPoint(x: 49.38, y: 26.79), control1: CGPoint(x: 44.74, y: 24.74), control2: CGPoint(x: 46.65, y: 26.79))
facePath.addCurve(to: CGPoint(x: 54.02, y: 22.01), control1: CGPoint(x: 52.11, y: 26.79), control2: CGPoint(x: 54.02, y: 24.74))
facePath.addLine(to: CGPoint(x: 54.02, y: 11.24))
facePath.addCurve(to: CGPoint(x: 88.76, y: 50), control1: CGPoint(x: 73.68, y: 13.3), control2: CGPoint(x: 88.76, y: 29.81))
facePath.addCurve(to: CGPoint(x: 50, y: 88.8), control1: CGPoint(x: 88.76, y: 71.44), control2: CGPoint(x: 71.58, y: 88.8))
facePath.addCurve(to: CGPoint(x: 11.24, y: 50), control1: CGPoint(x: 28.47, y: 88.8), control2: CGPoint(x: 11.2, y: 71.44))
facePath.addCurve(to: CGPoint(x: 19.62, y: 26.08), control1: CGPoint(x: 11.29, y: 41.05), control2: CGPoint(x: 14.4, y: 32.63))
facePath.addCurve(to: CGPoint(x: 19.67, y: 17.8), control1: CGPoint(x: 21.67, y: 23.06), control2: CGPoint(x: 22.11, y: 20.19))
facePath.addCurve(to: CGPoint(x: 11, y: 19.04), control1: CGPoint(x: 17.32, y: 15.5), control2: CGPoint(x: 13.44, y: 15.74))
facePath.addCurve(to: CGPoint(x: 0, y: 50), control1: CGPoint(x: 4.16, y: 27.56), control2: CGPoint(x: 0, y: 38.37))
facePath.addCurve(to: CGPoint(x: 50, y: 100), control1: CGPoint(x: 0, y: 77.61), control2: CGPoint(x: 22.39, y: 100))
return facePath
}
}
struct TimerHand: Shape {
func path(in rect: CGRect) -> Path {
var handPath = Path()
handPath.move(to: CGPoint(x: 7.93, y: 40))
handPath.addCurve(to: CGPoint(x: 15.8, y: 29.75), control1: CGPoint(x: 13.5, y: 39.9), control2: CGPoint(x: 16.88, y: 35.25))
handPath.addLine(to: CGPoint(x: 10.76, y: 2.77))
handPath.addCurve(to: CGPoint(x: 5.22, y: 2.77), control1: CGPoint(x: 10.03, y: -0.92), control2: CGPoint(x: 5.95, y: -0.92))
handPath.addLine(to: CGPoint(x: 0.21, y: 29.72))
handPath.addCurve(to: CGPoint(x: 7.93, y: 40), control1: CGPoint(x: -0.9, y: 35.25), control2: CGPoint(x: 2.44, y: 39.93))
return handPath
}
}
struct TimerView: View {
#State private var rotation = 0.0
var body: some View {
VStack {
ZStack {
TimerFace().frame(CGSize(width: 100,height: 100), alignment: .center)
TimerHand().frame(CGSize(width: 16,height: 64), alignment: .center)
.rotationEffect(.degrees(rotation), anchor: .center)
}
Slider(value: $rotation, in: 0...360, step: 1.0)
}
}
}
It would look like this
I do think this is a bit like "cheating". Any suggestions on how to do this the "proper" way, instead of animating the hand frame, animating the paths themselves?
You can put the rotation inside the frame:
struct TimerHand: Shape {
var degree : Double
func path(in rect: CGRect) -> Path {
var handPath = Path()
handPath.move(to: CGPoint(x: 7.93, y: 40))
handPath.addCurve(to: CGPoint(x: 15.8, y: 29.75), control1: CGPoint(x: 13.5, y: 39.9), control2: CGPoint(x: 16.88, y: 35.25))
handPath.addLine(to: CGPoint(x: 10.76, y: 2.77))
handPath.addCurve(to: CGPoint(x: 5.22, y: 2.77), control1: CGPoint(x: 10.03, y: -0.92), control2: CGPoint(x: 5.95, y: -0.92))
handPath.addLine(to: CGPoint(x: 0.21, y: 29.72))
handPath.addCurve(to: CGPoint(x: 7.93, y: 40), control1: CGPoint(x: -0.9, y: 35.25), control2: CGPoint(x: 2.44, y: 39.93))
return handPath.rotation(.degrees(degree), anchor: .center).path(in: rect)
}
}
struct TimerView: View {
#State private var rotation = 0.0
var body: some View {
VStack {
ZStack {
TimerFace().frame(width: 100,height: 100, alignment: .center)
TimerHand(degree: rotation).frame(width: 16,height: 64, alignment: .center)
}
Slider(value: $rotation, in: 0...360, step: 1.0)
}
}
}