how can I animate drawing svg path in my swift app? - ios

In my ios swift app I have SVG file - it's a shape of a map marker, now I want to draw it with animation. I managed to display the drawing thanks to the usage of pocketSVG https://github.com/pocketsvg/PocketSVG/ My code is as follows:
#IBOutlet weak var mainLogoView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let url = Bundle.main.url(forResource: "mainLogo", withExtension: "svg")!
//initialise a view that parses and renders an SVG file in the bundle:
let svgImageView = SVGImageView.init(contentsOf: url)
//scale the resulting image to fit the frame of the view, but
//maintain its aspect ratio:
svgImageView.contentMode = .scaleAspectFit
//layout the view:
svgImageView.translatesAutoresizingMaskIntoConstraints = false
mainLogoView.addSubview(svgImageView)
svgImageView.topAnchor.constraint(equalTo: mainLogoView.topAnchor).isActive = true
svgImageView.leftAnchor.constraint(equalTo: mainLogoView.leftAnchor).isActive = true
svgImageView.rightAnchor.constraint(equalTo: mainLogoView.rightAnchor).isActive = true
svgImageView.bottomAnchor.constraint(equalTo: mainLogoView.bottomAnchor).isActive = true
Timer.scheduledTimer(timeInterval: TimeInterval(2), target: self, selector: "functionHere", userInfo: nil, repeats: false)
}
func functionHere(){
self.performSegue(withIdentifier: "MainSegue", sender: nil)
}
So basically in my ViewController I added on storyboard a view called mainLogoView and then I'm displaying there the SVG file. Now, since it's a shaped marker, very similar to this: http://image.flaticon.com/icons/svg/116/116395.svg I want to draw it - during the time of 2 seconds I want to draw the shape around the circle.
How can I do it?
=====EDIT:
Following Josh's advice I commented out most of the code in my viewDidLoad and did instead:
let url = Bundle.main.url(forResource: "mainLogo", withExtension: "svg")!
for path in SVGBezierPath.pathsFromSVG(at: url)
{
var layer:CAShapeLayer = CAShapeLayer()
layer.path = path.cgPath
layer.lineWidth = 4
layer.strokeColor = orangeColor.cgColor
layer.fillColor = UIColor.white.cgColor
mainLogoView.addSubview(layer)
}
but now the last line throws an error
Cannot convert value of type 'CAShapeLayer' to expected argument type 'UIView'
Also, after editing my code - could you give me a hint how could I use Josh's method here?

Instead of using the imageView method convert your SVG into a cgPath. Assign this path to a CAShapeLayer and add that shape layer to your view. You can animate the CAShapeLayer's endStroke as follows:
func animateSVG(From startProportion: CGFloat, To endProportion: CGFloat, Duration duration: CFTimeInterval = animationDuration) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = startProportion
animation.toValue = endProportion
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
svgLayer.strokeEnd = endProportion
svgLayer.strokeStart = startProportion
svgLayer.add(animation, forKey: "animateRing")
}

I did encounter the same problem, this is my solution. svgWidth, svgHeight are width and height of svg tag
let url = Bundle.main.url(forResource: "mainLogo", withExtension: "svg")!
for path in SVGBezierPath.pathsFromSVG(at: url)
{
var layer:CAShapeLayer = CAShapeLayer()
layer.transform = CATransform3DMakeScale(CGFloat(mainLogoView.frame.width/svgWidth), CGFloat(mainLogoView.frame.height/svgHeight), 1)
layer.path = path.cgPath
layer.lineWidth = 4
layer.strokeColor = orangeColor.cgColor
layer.fillColor = UIColor.white.cgColor
layer.transform = CATransform3DMakeScale(2, 2, 1)
mainLogoView.layer.addSublayer(layer)
}

Related

Swift Change circular border color with animation

I created a circular button using swift, what I am trying to do is changing the border color with animation
I used the following code :
let color = CABasicAnimation(keyPath: "borderColor")
color.fromValue = UIColor.black.cgColor
color.toValue = UIColor.red.cgColor
color.duration = 1
color.repeatCount = 1
sender.layer.add(color, forKey: "color and width")
The border color is changing but it is not given the desire effect, it change the border color all at once. What I would like to have as effect is like your drawing over the old color, to keep it simple ==> like a progress bar where u see the old color like fade away and be replaced by the new color.
Is there is any way to do that?
Thanks for helping.
Updating code
var storkeLayer = CAShapeLayer()
#IBOutlet weak var Mybut: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
storkeLayer.fillColor = UIColor.clear.cgColor
storkeLayer.strokeColor = UIColor.black.cgColor
storkeLayer.lineWidth = 2
// Create a rounded rect path using button's bounds.
storkeLayer.path = CGPath.init(roundedRect: Mybut.bounds, cornerWidth: Mybut.frame.width / 2 , cornerHeight: Mybut.frame.height / 2, transform: nil)
// Add layer to the button
Mybut.layer.addSublayer(storkeLayer)
}
#IBAction func bytton(_ sender: UIButton) {
storkeLayer.fillColor = UIColor.clear.cgColor
storkeLayer.strokeColor = UIColor.red.cgColor
storkeLayer.lineWidth = 2
// Create a rounded rect path using button's bounds.
storkeLayer.path = CGPath.init(roundedRect: Mybut.bounds, cornerWidth: Mybut.frame.width / 2 , cornerHeight: Mybut.frame.height / 2, transform: nil)
// Add layer to the button
sender.layer.addSublayer(storkeLayer)
// Create animation layer and add it to the stroke layer.
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = CGFloat(0.0)
animation.toValue = CGFloat(1.0)
animation.duration = 1
animation.fillMode = kCAFillModeForwards
// animation.fillMode = kCAFillModeBackwards
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
storkeLayer.add(animation, forKey: "circleAnimation")
}
You can stroke the button's path using the following code snippet.
#IBAction func buttonTapped(_ sender: UIButton) {
let storkeLayer = CAShapeLayer()
storkeLayer.fillColor = UIColor.clear.cgColor
storkeLayer.strokeColor = UIColor.red.cgColor
storkeLayer.lineWidth = 2
// Create a rounded rect path using button's bounds.
storkeLayer.path = CGPath.init(roundedRect: sender.bounds, cornerWidth: 5, cornerHeight: 5, transform: nil) // same path like the empty one ...
// Add layer to the button
sender.layer.addSublayer(storkeLayer)
// Create animation layer and add it to the stroke layer.
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = CGFloat(0.0)
animation.toValue = CGFloat(1.0)
animation.duration = 1
animation.fillMode = kCAFillModeForwards
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
storkeLayer.add(animation, forKey: "circleAnimation")
}
This snippet creates something like this:

How to parse SVG path component to UIBezierPath using SVGKit in iOS?

I am animating a SVG image in iOS using Swift. I have been able to render the SVG easily using SVGKit (https://github.com/SVGKit/SVGKit) but to animate it I need to convert the SVG path element to UIBezierPath. I can do so using other libraries but it'd be nice if I could do all of it using SVGKit alone. I haven't find any straight forward way to get the path element as well.
I had the same issues with Swift and using SVGKit. Even after following this simple tutorial and converting it to swift, I could render the SVG but not animate the line drawing. What worked for me was switching to PocketSVG
They have a function to iterate over each Layer and this is how I used it to animate a SVG file:
let url = NSBundle.mainBundle().URLForResource("tiger", withExtension: "svg")!
let paths = SVGBezierPath.pathsFromSVGAtURL(url)
for path in paths {
// Create a layer for each path
let layer = CAShapeLayer()
layer.path = path.CGPath
// Default Settings
var strokeWidth = CGFloat(4.0)
var strokeColor = UIColor.blackColor().CGColor
var fillColor = UIColor.whiteColor().CGColor
// Inspect the SVG Path Attributes
print("path.svgAttributes = \(path.svgAttributes)")
if let strokeValue = path.svgAttributes["stroke-width"] {
if let strokeN = NSNumberFormatter().numberFromString(strokeValue as! String) {
strokeWidth = CGFloat(strokeN)
}
}
if let strokeValue = path.svgAttributes["stroke"] {
strokeColor = strokeValue as! CGColor
}
if let fillColorVal = path.svgAttributes["fill"] {
fillColor = fillColorVal as! CGColor
}
// Set its display properties
layer.lineWidth = strokeWidth
layer.strokeColor = strokeColor
layer.fillColor = fillColor
// Add it to the layer hierarchy
self.view.layer.addSublayer(layer)
// Simple Animation
let animation = CABasicAnimation(keyPath:"strokeEnd")
animation.duration = 4.0
animation.fromValue = 0.0
animation.toValue = 1.0
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
layer.addAnimation(animation, forKey: "strokeEndAnimation")
The path is available by calling .path on Apple's classes. I suggest you read the Apple CoreAnimation documentation, especially CAShapeLayer (which is used heavily by SVGKit).
Apple also provides code for converting to UIBezierPath - again, search the Apple docs for details on how to do this. e.g.:
(UIBezierPath *)bezierPathWithCGPath:(CGPathRef)CGPath;

Animating the drawing of letters with CGPaths and CAShapeLayers

I'm currently using this code to animate the drawing of a character:
var path = UIBezierPath()
var unichars = [UniChar]("J".utf16)
var glyphs = [CGGlyph](count: unichars.count, repeatedValue: 0)
let gotGlyphs = CTFontGetGlyphsForCharacters(font, &unichars, &glyphs, unichars.count)
if gotGlyphs {
let cgpath = CTFontCreatePathForGlyph(font, glyphs[0], nil)
path = UIBezierPath(CGPath: cgpath!)
}
creates a bezierpath from a character ("J" in this case). Get path to trace out a character in an iOS UIFont
Then I create a CAShapeLayer() and add an animation to it.
let layer = CAShapeLayer()
layer.position = //CGPoint
layer.bounds = //CGRect()
view.layer.addSublayer(layer)
layer.path = path.CGPath
layer.lineWidth = 5.0
layer.strokeColor = UIColor.blackColor().CGColor
layer.fillColor = UIColor.clearColor().CGColor
layer.geometryFlipped = true
layer.strokeStart = 0.0
layer.strokeEnd = 1.0
let anim = CABasicAnimation(keyPath: "strokeEnd")
anim.duration = 8.0
anim.fromValue = 0.0
anim.toValue = 1.0
layer.addAnimation(anim, forKey: nil)
The result is my chosen character being animated correctly. However when I add another path to path with .appendPath() the appended path gets added right onto the original path as you could expect. What should I do if I want to draw a letter where all characters have appropriate spacing etc.?
Thank you for your time.
You can do this using translation on path (using CGAffineTransformMakeTranslation), since path doesn't have a "position", it is just set of points. But to make a translation every iteration of a character, we need a current width of a path - we can use CGPathGetBoundingBox() for this, which gets the box that the path will cover. So having everything we need, we can make an example. Make a path for whole word would be something along the lines:
let word = "Test"
let path = UIBezierPath()
let spacing: CGFloat = 50
var i: CGFloat = 0
for letter in word.characters {
let newPath = getPathForLetter(letter)
let actualPathRect = CGPathGetBoundingBox(path.CGPath)
let transform = CGAffineTransformMakeTranslation((CGRectGetWidth(actualPathRect) + min(i, 1)*spacing), 0)
newPath.applyTransform(transform)
path.appendPath(newPath)
i++
}
Where function getPathForLetter() is just:
func getPathForLetter(letter: Character) -> UIBezierPath {
var path = UIBezierPath()
let font = CTFontCreateWithName("HelveticaNeue", 64, nil)
var unichars = [UniChar]("\(letter)".utf16)
var glyphs = [CGGlyph](count: unichars.count, repeatedValue: 0)
let gotGlyphs = CTFontGetGlyphsForCharacters(font, &unichars, &glyphs, unichars.count)
if gotGlyphs {
let cgpath = CTFontCreatePathForGlyph(font, glyphs[0], nil)
path = UIBezierPath(CGPath: cgpath!)
}
return path
}

Animating on some part of UIBezier path

I have two UIBezierPath...
First path shows the total path from to and fro destination and the second path is a copy of the first path but that copy should be a percentage of the first path which I am unable to do.
Basically I would like the plane to stop at the part where green UIBezier path ends and not go until the past green color.
I am attaching a video in hte link that will show the animation I am trying to get. http://cl.ly/302I3O2f0S3Y
Also a similar question asked is Move CALayer via UIBezierPath
Here is the relevant code
override func viewDidLoad() {
super.viewDidLoad()
let endPoint = CGPointMake(fullFlightLine.frame.origin.x + fullFlightLine.frame.size.width, fullFlightLine.frame.origin.y + 100)
self.layer = CALayer()
self.layer.contents = UIImage(named: "Plane")?.CGImage
self.layer.frame = CGRectMake(fullFlightLine.frame.origin.x - 10, fullFlightLine.frame.origin.y + 10, 120, 120)
self.path = UIBezierPath()
self.path.moveToPoint(CGPointMake(fullFlightLine.frame.origin.x, fullFlightLine.frame.origin.y + fullFlightLine.frame.size.height/4))
self.path.addQuadCurveToPoint(endPoint, controlPoint:CGPointMake(self.view.bounds.size.width/2, -200))
self.animationPath = self.path.copy() as! UIBezierPath
let w = (viewModel?.completePercentage) as CGFloat!
// let animationRectangle = UIBezierPath(rect: CGRectMake(fullFlightLine.frame.origin.x-20, fullFlightLine.frame.origin.y-270, fullFlightLine.frame.size.width*w, fullFlightLine.frame.size.height-20))
// let currentContext = UIGraphicsGetCurrentContext()
// CGContextSaveGState(currentContext)
// self.animationPath.appendPath(animationRectangle)
// self.animationPath.addClip()
// CGContextRestoreGState(currentContext)
self.shapeLayer = CAShapeLayer()
self.shapeLayer.path = path.CGPath
self.shapeLayer.strokeColor = UIColor.redColor().CGColor
self.shapeLayer.fillColor = UIColor.clearColor().CGColor
self.shapeLayer.lineWidth = 10
self.animationLayer = CAShapeLayer()
self.animationLayer.path = animationPath.CGPath
self.animationLayer.strokeColor = UIColor.greenColor().CGColor
self.animationLayer.strokeEnd = w
self.animationLayer.fillColor = UIColor.clearColor().CGColor
self.animationLayer.lineWidth = 3
fullFlightLine.layer.addSublayer(shapeLayer)
fullFlightLine.layer.addSublayer(animationLayer)
fullFlightLine.layer.addSublayer(self.layer)
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
updateAnimationForPath(self.animationLayer)
}
func updateAnimationForPath(pathLayer : CAShapeLayer) {
let animation : CAKeyframeAnimation = CAKeyframeAnimation(keyPath: "position")
animation.path = pathLayer.path
animation.calculationMode = kCAAnimationPaced
animation.delegate = self
animation.duration = 3.0
self.layer.addAnimation(animation, forKey: "bezierPathAnimation")
}
}
extension Int {
var degreesToRadians : CGFloat {
return CGFloat(self) * CGFloat(M_PI) / 180.0
}
}
Your animation for traveling a path is exactly right. The only problem now is that you are setting the key path animation's path to the whole bezier path:
animation.path = pathLayer.path
If you want the animation to cover only part of that path, you have two choices:
Supply a shorter version of the bezier path, instead of pathLayer.path.
Alternatively, wrap the whole animation in a CAAnimationGroup with a shorter duration. For example, if your three-second animation is wrapped inside a two-second animation group, it will stop two-thirds of the way through.

Animate UIImageView Diagonally

I'd like to be able to animate my UIImageView background in a diagonal fashion. Meaning the background would animate and scroll from the top left of the screen towards the bottom right of the screen. Ideally, this would be a continuous animation.
I have the directions figured out, however, what I have seems to take a copy of the background & animate it over a static background. This causes for a weird effect.
See gif:
Here is the code:
var imageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
self.setup();
}
func setup() {
self.imageView.frame = self.view.bounds
self.imageView.image = UIImage(named: "myimage.jpg")
self.view.addSubview(self.imageView)
self.scroll()
}
func scroll() {
var theImage = UIImage(named: "myimage.jpg")!
var pattern = UIColor(patternImage: theImage)
var layer = CALayer()
layer.backgroundColor = pattern.CGColor
layer.transform = CATransform3DMakeScale(1,-1,1)
layer.anchorPoint = CGPointMake(0, 1)
var viewSize = self.imageView.bounds.size
layer.frame = CGRectMake(0,0, theImage.size.width + viewSize.width, viewSize.height)
self.imageView.layer.addSublayer(layer)
var startPoint = CGPointZero
var endPoint = CGPointMake(theImage.size.width, theImage.size.height + 15)
var animation = CABasicAnimation(keyPath: "position")
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.fromValue = NSValue(CGPoint:startPoint)
animation.toValue = NSValue(CGPoint:endPoint)
animation.repeatCount = Float.infinity
animation.duration = 1.0
layer.addAnimation(animation, forKey: "position")
}
Is anyone able to figure out how to make the whole background scroll? Do i just need a larger image, and scroll that? If that is the case, what does the contentMode of the imageView need to be set at?
Thanks
---- Update
I've reworked the code a bit, and have a working version of what I wanted. I'm curious for some feedback, as I'm still not 100% comfortable with animations in general (but i'm learning! :) )
-- I removed the whole background image in general, as DuncanC suggested it wasn't serving a good purpose. Now, i'm just making an image layer, trying to make if sufficiently big, and looping the animation. Thoughts?
override func viewDidLoad() {
super.viewDidLoad()
self.scroll()
}
func scroll() {
var theImage = UIImage(named: "myimage.jpg")!
var pattern = UIColor(patternImage: theImage)
var layer = CALayer()
layer.backgroundColor = pattern.CGColor
layer.transform = CATransform3DMakeScale(1,-1,1)
layer.anchorPoint = CGPointMake(0, 1)
// i'm making the view large here so the animation can keep running smoothly without
// showing what is behind our image
// is there a better way to do this?
var viewSize = CGSizeMake(self.view.bounds.size.height * 10, self.view.bounds.size.width * 10)
layer.frame = CGRectMake(0,0, theImage.size.width + viewSize.width, viewSize.height)
self.view.layer.addSublayer(layer)
var startPoint = CGPointMake(-theImage.size.width, -theImage.size.height)
var endPoint = CGPointMake(0, 0)
var animation = CABasicAnimation(keyPath: "position")
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.fromValue = NSValue(CGPoint:startPoint)
animation.toValue = NSValue(CGPoint:endPoint)
animation.repeatCount = Float.infinity
animation.duration = 3.0
layer.addAnimation(animation, forKey: "position")
}
**Result:*
Your code doesn't make a lot of sense. You have an image view that contains the image myimage.jpg, and then you create a CALayer that contains that image as a pattern color. You add the layer to your image view and animate it's position.
Just use a UIView animation and animate the center of your image view directly. If you're using AutoLayout then you'll need to add horizontal and vertical position constraints, wire them up as IBOutlets, and then in your animation block change the constraints and call layoutIfNeeded() on the image view's superview.

Resources