Animate a UIView on a sine wave path - ios

How would you make a UIView move along a sine wave path? Ideally, I'd be able to stop the animation when the UIView goes off the screen and release it. I see that UIView->animateWithDuration can only animate on one property at a time, but this seems like two exclusive things are happening simultaneously: it's moving to the right and it's moving up/down.

You can actually do this with a simple CAKeyframeAnimation.
override func viewDidLoad() {
super.viewDidLoad()
let myView = UIView(frame: CGRect(x: 10, y: 100, width: 50, height: 50))
myView.backgroundColor = UIColor.cyan
view.addSubview(myView)
let animation = CAKeyframeAnimation()
animation.keyPath = "position"
animation.duration = 60 // 60 seconds
animation.isAdditive = true // Make the animation position values that we later generate relative values.
// From x = 0 to x = 299, generate the y values using sine.
animation.values = (0..<300).map({ (x: Int) -> NSValue in
let xPos = CGFloat(x)
let yPos = sin(xPos)
let point = CGPoint(x: xPos, y: yPos * 10) // The 10 is to give it some amplitude.
return NSValue(cgPoint: point)
})
myView.layer.add(animation, forKey: "basic")
}

Related

merge two CAShaplayer with transparent in Swift5?

We have created two CAShapeLayer and we have animated them by CABasicAnimation changing position like fromValue to toValue so far code works as excepted.
Two layers meeting at some points while animating, its moving one over another seems overlapping. We need to achieve like below image but we have got different output.
Desired output:
Output at Loading view::
Result we got after animating layers:
Code Below:
class MovingAnimationVC: UIViewController {
let layerSize = CGSize(width: 100, height: 100)
private var layerWidth : CGFloat = 100
private var layerHeight : CGFloat = 100
var circleA : CAShapeLayer! = nil
var circleB : CAShapeLayer! = nil
lazy var aPosition = CGPoint(x: -layerWidth/2 + 30, y: 100)
lazy var bPosition = CGPoint(x: (self.view.frame.width - layerWidth / 2) - 30 , y: 400)
override func viewDidLoad() {
super.viewDidLoad()
if circleA == nil{
circleA = CAShapeLayer()
let pa = UIBezierPath(ovalIn: CGRect(origin: aPosition, size: layerSize))
circleA.path = pa.cgPath
circleA.fillColor = UIColor.orange.cgColor
view.layer.addSublayer(circleA)
}
if circleB == nil{
circleB = CAShapeLayer()
let pa = UIBezierPath(ovalIn: CGRect(origin: bPosition, size: layerSize))
circleB.path = pa.cgPath
circleB.fillColor = UIColor.orange.cgColor
view.layer.addSublayer(circleB)
}
}
override func viewDidAppear(_ animated: Bool) {
animateCircleA()
animateCircleB()
}
fileprivate func animateCircleA(){
lazy var startPointA = CGPoint(x: -layerWidth/2 + 30, y: 0)
lazy var endPointA = CGPoint(x: (self.view.frame.width - layerWidth / 2), y: 300)
let animate = CABasicAnimation(keyPath: "position")
animate.timingFunction = CAMediaTimingFunction(name: .linear)
animate.fromValue = NSValue(cgPoint: startPointA)
animate.toValue = NSValue(cgPoint: endPointA)
animate.repeatCount = 2
animate.duration = 5.0
circleA.add(animate, forKey: "position")
}
fileprivate func animateCircleB(){
lazy var startPointB = CGPoint(x: 0, y: 0)
lazy var endPointB = CGPoint(x:-(self.view.frame.width - layerWidth / 2) - 30, y: -(self.view.frame.width - layerWidth / 2) + 30)
let animate = CABasicAnimation(keyPath: "position")
animate.timingFunction = CAMediaTimingFunction(name: .linear)
animate.fromValue = NSValue(cgPoint: startPointB)
animate.toValue = NSValue(cgPoint: endPointB)
animate.repeatCount = 2
animate.duration = 5.0
circleB.add(animate, forKey: "position")
}
}
Kindly suggest me right way to get started.
How to make both layer transparent while crossing each other?
The easiest way is to just set the alphas of the layers, but if you don't want the layers to be transparent, then that means you want a different blend mode for the layers.
According to this question, to change the blend mode of a layer, you can set CALayer.compositingFilter. From your desired output, it seems like the screen blend mode is appropriate:
circleA.compositingFilter = "screenBlendMode"
circleA.compositingFilter = "screenBlendMode"
Note that you should add your layers to a view with a transparent background, otherwise the background color of the view is also blended with the circles' colours. This view can be added as a subview of whatever view you were originally going to put the circles in, filling its entire bounds.
someView.backgroundColor = .clear
someView.layer.addSublayer(circleA)
someView.layer.addSublayer(circleB)

How to position Layers in swift programmatically?

I am trying to position a circular progress bar but I am failing constantly. I guess I am not understanding the concept of frames and views and bounds. I found this post on stack overflow that shows me exactly how to construct a circular progress bar.
However, in the post, a circleView was created as UIView using CGRect(x: 100, y: 100, width: 100, height: 100)
In my case, manually setting the x and y coordinates is obv a big no. So the circleView has to be in the centre of it's parent view.
Here is my view hierarchy:
view -> view2ForHomeController -> middleView -> circleView
So everything is positioned using auto layout. Here is the problem:The circleView is adding properly and it positions itself at the x and y value I specify but how can I specify the x and y values of the center of middleView. I tried the following but the center values are returned as 0.0, 0.0.
self.view.addSubview(self.view2ForHomeController)
self.view2ForHomeController.fillSuperview()
let xPosition = self.view2ForHomeController.middleView.frame.midX
let yPostion = self.view2ForHomeController.middleView.frame.midY
print(xPosition) // ------- PRINTS ZERO
print(yPostion) // ------- PRINTS ZERO
let circle = UIView(frame: CGRect(x: xPosition, y: yPostion, width: 100, height: 100))
circle.backgroundColor = UIColor.green
self.view2ForHomeController.middleView.addSubview(circle)
var progressCircle = CAShapeLayer()
progressCircle.frame = self.view.bounds
let lineWidth: CGFloat = 10
let rectFofOval = CGRect(x: lineWidth / 2, y: lineWidth / 2, width: circle.bounds.width - lineWidth, height: circle.bounds.height - lineWidth)
let circlePath = UIBezierPath(ovalIn: rectFofOval)
progressCircle = CAShapeLayer ()
progressCircle.path = circlePath.cgPath
progressCircle.strokeColor = UIColor.white.cgColor
progressCircle.fillColor = UIColor.clear.cgColor
progressCircle.lineWidth = 10.0
progressCircle.frame = self.view.bounds
progressCircle.lineCap = CAShapeLayerLineCap(rawValue: "round")
circle.layer.addSublayer(progressCircle)
circle.transform = circle.transform.rotated(by: CGFloat.pi/2)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1.1
animation.duration = 1
animation.repeatCount = MAXFLOAT
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
progressCircle.add(animation, forKey: nil)
Frame is in terms of superview/superlayer coordinates. If you are going to say
circle.layer.addSublayer(progressCircle)
then you must give progressCircle a frame in terms of the bounds of circle.layer.
As for centering, in general, to center a sublayer in its superlayer you say:
theSublayer.position = CGPoint(
x:theSuperlayer.bounds.midX,
y:theSuperlayer.bounds.midY)
And to center a subview in its superview you say:
theSubview.center = CGPoint(
x:theSuperview.bounds.midX,
y:theSuperview.bounds.midY)
The bounds are the layer/view's coordinates in its local coordinate system. The frame is the layer/view's coordinates in its parents coordinate system. Since layers do not participate in auto layout, you should implement UIViewController.viewDidLayoutSubviews (or UIView.layoutSubLayers) and set the frame of the layer to the bounds of its super layer's view (the backing layer of a UIView essentially is the CoreGraphics version of that view).
This also means you need to recompute your path in they layout method. if that is expensive then draw your path in unit space and use the transform of the layer to scale it up to the view's dimensions instead.
The solution is to first add the subviews and then create the entire circular progress view at
override func viewDidLayoutSubviews(){
super.viewDidLayoutSubviews()
// Add and position the circular progress bar and its layers here
}

How to create a ruler / scale slider with Swift 3?

I'm trying to recreate a temperature ruler similar to the Coinbase app for an app in Swift 3. Unfortunately, I'm not sure if I follow the right approach.
In my current experiment, I used a UIScrollView element and placed / drawn lines at a certain distance (with a loop and UIBezierPath). In the next step I want to read the user input. With the current approach I have to use the X-position of the UIScrollView to convert things to read the current temperature. That seems to me a relatively inaccurate thing to be? My result looks like this.
// UI SCROLL VIEW
var scrollView: UIScrollView!
scrollView = UIScrollView(frame: CGRect(x: 0, y: 120, width: 400, height: 100))
scrollView.contentSize = CGSize(width: 2000, height: 100)
scrollView.showsHorizontalScrollIndicator = false
let minTemp = 0.0
let maxTemp = 36.8
let interval = 0.1
// LINES
let lines = UIBezierPath()
// DRAW TEMP OTHER LINES
for temp in stride(from: minTemp, to: maxTemp, by: interval)
{
let isInteger = floor(temp) == temp
let height = (isInteger) ? 20.0 : 10.0
let oneLine = UIBezierPath()
oneLine.move(to: CGPoint(x: temp*50, y: 0))
oneLine.addLine(to: CGPoint(x: temp*50, y: height))
lines.append(oneLine)
// INDICATOR TEXT
if(isInteger)
{
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 40, height: 21))
label.center = CGPoint(x: temp*50, y: height+15)
label.font = UIFont(name: "HelveticaNeue",
size: 10.0)
label.textAlignment = .center
label.text = "\(temp) °C"
scrollView.addSubview(label)
}
}
// DESIGN LINES IN LAYER
let shapeLayer = CAShapeLayer()
shapeLayer.path = lines.cgPath
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 1
// ADD LINES IN LAYER
scrollView.layer.addSublayer(shapeLayer)
view.addSubview(scrollView)
self.view = view
The Coinbase app also struck me that there is some kind of adaptive feedback (at least on the iPhone X) when I move the slider. The iPhone vibrates easily when you come across a line, similar to the UIPickerView. I do not have that with my approach and I strongly doubt that the developer has programmed it in manually on Coinbase... So maybe there's a smarter, more up-to-date approach to how to recreate such a ruler natively in Swift?
you can using this pod
this will allow you to using that as horizontal or vertical
https://github.com/farshidce/RKMultiUnitRuler

How to flip a card while scaling it in Swift

I'm trying to take a card UIView object, which has both back and front image subviews in it, and flip it while scaling it larger until mid flip and then scaling it back down. I have it working with just the flipping part, using UIView.transition(with:duration:options:animations:completion:), but this doesn't seem to be the correct solution, as all animations in the block only occur halfway through the full animation, which makes sense, since that's the point the views need to be added/removed.
I'm guessing I need to drop down to a lower level here, but I'm not that familiar with animations beyond the UIView layer. Any suggestions on how to add this scaling functionality to the card flip?
I'd put the both sides of the card under the layers of the view, you can do so by setting the contents of a CALayer. Then you apply the CABasicAnimation on the sublayers.
This is a simple demo which should run on the playground, without scaling implemented, you can modify the matrix by CATransform3DRotate(t:angle:x:y:z:) yourself.
var t = CATransform3DMakeRotation(CGFloat(M_PI), 0, 1, 0)
t.m34 = 0.001 // set a 3D perspective to simulate real flipping
let anim = CABasicAnimation(keyPath: "transform")
anim.byValue = NSValue(caTransform3D: t)
anim.duration = 3
anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
anim.fillMode = kCAFillModeForwards
anim.isRemovedOnCompletion = true
let cardContainer = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 100))
cardContainer.backgroundColor = UIColor.clear
// layer1: front side of the card
let layer1 = CALayer()
layer1.frame = CGRect(x: 0, y: 0, width: 200, height: 100)
layer1.backgroundColor = UIColor.red.cgColor
layer1.isDoubleSided = false
// layer2: back side of the card
let layer2 = CALayer()
layer2.frame = CGRect(x: 0, y: 0, width: 200, height: 100)
layer2.backgroundColor = UIColor.blue.cgColor
layer2.isDoubleSided = false
layer2.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0, 1, 0)
cardContainer.layer.addSublayer(layer1)
cardContainer.layer.addSublayer(layer2)
layer1.add(anim, forKey: "anim1")
layer2.add(anim, forKey: "anim2")

I want to shake the google map marker in iOS Swift

I am working on a project where I wants to shake the marker on google Map.
I am using the custom marker icon to represent on the map.
Like a head of person is shaking.
I don't know how to do it and search a lot but didn't find any solution.
You can add a CAKeyframeAnimation or CABasicAnimation to your marker.iconView!.layer we add a UIView with a frame bigger than our UIImageView inside then we need to adjust the anchor point of your UIImageView to bottom in vertical and horizontally centered this will be the point that will work as pivot in our animation, our animation will be a rotation animation in Z plane from -30 to 30 grades to achieve the animation desired.This is the simplest way to do this but you can also define a custom class and make a lot of other things
let marker = GMSMarker(position: CLLocationCoordinate2D(latitude: 22.404963, longitude: -79.961755))
//we need a bigger UIView to avoid the clip problem with the UIImageView
marker.iconView = UIView(frame: CGRect(x: 0, y: 0, width: 60, height: 40))
let imageView = UIImageView(frame: CGRect(x: (marker.iconView?.frame.width)!/2 - 14, y: (marker.iconView?.frame.height)! - 36, width: 28, height: 36))
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(named: "iconomapa")
marker.iconView?.addSubview(imageView)
imageView.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0) //we need adjust anchor point to achieve the desired behavior
imageView.layer.frame = CGRect(x: (marker.iconView?.frame.width)!/2 - 14, y: (marker.iconView?.frame.height)! - 36, width: 28, height: 36) //we need adjust the layer frame
let animation = CAKeyframeAnimation()
animation.keyPath = "transform.rotation.z"
animation.values = [ 0, -30 * .pi / 180.0, 30 * .pi / 180.0 , 0]
animation.keyTimes = [0, 0.33 , 0.66 , 1]
animation.duration = 1;
animation.isAdditive = false;
animation.isRemovedOnCompletion = true
animation.repeatCount = .infinity
marker.iconView!.subviews[0].layer.add(animation, forKey: "shakeAnimation")
marker.map = self.mapView
here is how it looks
Hope this helps
You can use marker property to rotate your marker ,
marker.rotation = (you_angle_in_degree) * M_PI / 180.0f;
// convert degree to radian
The Horizontally Shake animation you can get after some time interval change the degree for of the rotation , you can use timer ,
// create a timer
timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
func timerAction() {
// Place you aniamtion logic here ,
// every 0.5 second change the rotation of your maker
}

Resources