Animating Paths in SwiftUI Shapes - swiftui

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

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.

How to scale and arrange multiple UIBezierPath in one view in 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))
}
}

SwiftUI, multiple shapes with unified shadow?

Is there a way in SwiftUI to union two shapes so that they cast a unified shadow. I have tried various combinations and modifiers but don't seem to be able to achieve the look I am after. Any insight would be much appreciated.
struct CardView: View {
var body: some View {
Group {
RoundedRectangle(cornerRadius: 20)
.fill(Color.orange)
.frame(width: 200, height: 250)
.zIndex(0)
Circle()
.trim(from: 0.5, to: 1)
.fill(Color.orange)
.frame(width: 100, height: 100)
.offset(y: -125)
.zIndex(1)
}.shadow(color: Color.black, radius: 10, x: 0, y: 0)
}
}
This is what I get, and what I am after ...
NOTE: zIndex was just something I tried, i.e. both shapes having the same zIndex etc. its also a quick way to reorder things without having to move the shapes within the container.
Here is possible solution. Tested with Xcode 11.4 / iOS 13.4
struct CardView: View {
var body: some View {
VStack(spacing: 0) {
Circle()
.trim(from: 0.5, to: 1)
.fill(Color.orange)
.frame(width: 100, height: 100)
.offset(x: 0, y: 50)
RoundedRectangle(cornerRadius: 20)
.fill(Color.orange)
.frame(width: 200, height: 250)
}
.compositingGroup()
.shadow(color: Color.primary, radius: 10, x: 0, y: 0)
}
}
I think the correct answer is #Asperi's .compositingGroup as documented here (https://developer.apple.com/documentation/swiftui/group/3284789-compositinggroup) but I'd like to leave a different working version because there are some use cases where you may not be able or want to use .compositingGroup.
So let's look at .background, you can copy and paste the code to see in the simulator:
import SwiftUI
struct AnyShapedView: View {
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 20)
.fill(Color.orange)
.frame(width: 200, height: 250)
.zIndex(0)
Circle()
.trim(from: 0.5, to: 1)
.fill(Color.orange)
.frame(width: 100, height: 100)
.offset(y: -125)
.zIndex(1)
}
}
}
struct ContentView: View {
var body: some View {
AnyShapedView()
.background(
AnyShapedView()
.shadow(color: Color.black, radius: 10, x: 0, y: 0)
)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The output:
The version above is not the best practice, but nevertheless, try to understand how shapes and cropping work, as in some use cases it might come useful.
Draw the shape:
struct CustomShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let width = rect.size.width
let height = rect.size.height
path.move(to: CGPoint(x: 0.89995*width, y: 0.16667*height))
path.addLine(to: CGPoint(x: 0.75*width, y: 0.16667*height))
path.addCurve(to: CGPoint(x: 0.5*width, y: 0), control1: CGPoint(x: 0.75*width, y: 0.07463*height), control2: CGPoint(x: 0.63805*width, y: 0))
path.addCurve(to: CGPoint(x: 0.25*width, y: 0.16667*height), control1: CGPoint(x: 0.36195*width, y: 0), control2: CGPoint(x: 0.25*width, y: 0.07463*height))
path.addLine(to: CGPoint(x: 0.10005*width, y: 0.16667*height))
path.addCurve(to: CGPoint(x: 0, y: 0.23337*height), control1: CGPoint(x: 0.0448*width, y: 0.16667*height), control2: CGPoint(x: 0, y: 0.19653*height))
path.addLine(to: CGPoint(x: 0, y: 0.93333*height))
path.addCurve(to: CGPoint(x: 0.10005*width, y: 1.00003*height), control1: CGPoint(x: 0, y: 0.97017*height), control2: CGPoint(x: 0.0448*width, y: 1.00003*height))
path.addLine(to: CGPoint(x: 0.89995*width, y: 1.00003*height))
path.addCurve(to: CGPoint(x: width, y: 0.93333*height), control1: CGPoint(x: 0.9552*width, y: 1.00003*height), control2: CGPoint(x: width, y: 0.97017*height))
path.addLine(to: CGPoint(x: width, y: 0.23337*height))
path.addCurve(to: CGPoint(x: 0.89995*width, y: 0.16667*height), control1: CGPoint(x: width, y: 0.19653*height), control2: CGPoint(x: 0.9552*width, y: 0.16667*height))
path.closeSubpath()
return path
}
}
And custom color, shadow and size in your parent view:
CustomShape()
.fill(.orange)
.shadow(color: Color.primary, radius: 10, x: 0, y: 0)
.frame(width: 300, height: 400)