Swift Circular Movement Animation of an image [duplicate] - ios

This question already has answers here:
UIView animation along a round path?
(3 answers)
Closed 4 years ago.
Creating an app where a an image moves upon the press of a button(repeated 3 times). The image currently has a linear movement but what I need is a circular movement animation for it.
The image should start from the left of the screen, go towards top and then to the right of the screen so there are 3 points where the image stops. This has to be done with a circular animation.
enum TimeOfDay: String {
case Dimineata = "Dimineata"
case Pranz = "Pranz"
case Seara = "Seara"
}
class ViewController: UIViewController {
#IBOutlet weak var timeOfDayLabel: UILabel!
#IBOutlet weak var sunImage: UIImageView!
var timeOfDay = TimeOfDay.Dimineata
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func didTapNext(_ sender: Any) {
switch timeOfDay {
case .Dimineata:
timeOfDay = .Pranz
sunImage.image = UIImage(named: "LunchSun")
UIView.animate(withDuration: 1) {
self.sunImage.frame = CGRect(x: self.view.center.x-50, y: 120, width: 100, height: 100)
}
case .Pranz:
timeOfDay = .Seara
sunImage.image = UIImage(named: "NightSun")
UIView.animate(withDuration: 1) {
self.sunImage.frame = CGRect(x: self.view.frame.width-50, y: 166, width: 100, height: 100)
}
default:
timeOfDay = .Dimineata
sunImage.image = UIImage(named: "NightSun")
UIView.animate(withDuration: 1) {
self.sunImage.frame = CGRect(x: self.view.frame.origin.x-50, y: 166, width: 100, height: 100)
}
}
timeOfDayLabel.text = timeOfDay.rawValue.uppercased()
}
}

Create a circular path
Create a CAKeyframeAnimation
Set the animation.path to the circular path
Add that animation to the layer of the image view
Set the frame of image view to the last point in the path
Example code:
var imageView : UIImageView!
// Adapt these constants to your case
let initialX: CGFloat = 40
let imageViewWidth: CGFloat = 60
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
imageView = UIImageView()
imageView.backgroundColor = .red
imageView.frame = CGRect(x: initialX,
y: view.frame.height / 2.0 - 20,
width: 60,
height: 40)
imageView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(imageView)
}
#IBAction func animate(_ sender: Any) {
//You can customize this path
let circularPath = UIBezierPath(arcCenter: view.center,
radius: view.frame.width / 2 - initialX - imageViewWidth / 2,
startAngle: -.pi,
endAngle: 0,
clockwise: true)
//Create the animation
let animation = CAKeyframeAnimation(keyPath: #keyPath(CALayer.position))
animation.duration = 2 // In seconds
animation.repeatCount = 1 //At maximum it could be MAXFLOAT if you want the animation to seem to loop forever
animation.path = circularPath.cgPath
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) //Optional
//Add the animation to the layer of the image view
imageView.layer.add(animation, forKey: "myAnimation")
//prevent the imageView from going back to its original coordinates
imageView.frame.origin.x = circularPath.cgPath.currentPoint.x - imageView.frame.width / 2
imageView.frame.origin.y = circularPath.cgPath.currentPoint.y - imageView.frame.height / 2
}

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)

Cloud moving animation in Swift? Is it possible?

We are following this tutorial to http://www.invasivecode.com/weblog/core-animation-scroll-layer-cascrolllayer
As per their tutorial animation will stop based on the layer bounds reached. How to do the cloud moving animation by using core animation or in Imageview?
Is it possible?
Gif or Lottie animation not needed here, we excepting through code.
Note that we have tried their code and its working but lack is there as i mentioned above.
How to keep on moving animation like cloud does?
Think twice before devoting this question.
Code:
class ViewController: UIViewController {
var translation: CGFloat = 0.0
public var currentWidth : CGFloat = {
let width = UIScreen.main.bounds.width
return width
}()
public var currentHeight : CGFloat = {
let width = UIScreen.main.bounds.height
return width
}()
lazy var scrollLayer : CAScrollLayer = {
let scrollLayer = CAScrollLayer() // 8
scrollLayer.bounds = CGRect(x: 0.0, y: 0.0, width: currentWidth, height: currentHeight) // 9
scrollLayer.position = CGPoint(x: self.view.bounds.size.width/2, y: self.view.bounds.size.height/2) // 10
scrollLayer.borderColor = UIColor.black.cgColor // 11
// scrollLayer.borderWidth = 5.0 // 12
scrollLayer.scrollMode = CAScrollLayerScrollMode.horizontally // 13
return scrollLayer
}()
lazy var displayLink: CADisplayLink = {
let displayLink = CADisplayLink(target: self, selector: #selector(scrollLayerScroll))
if #available(iOS 15.0, *) {
displayLink.preferredFrameRateRange = CAFrameRateRange(minimum: 5.0, maximum: 8.0, __preferred: 6.0)
} else {
displayLink.preferredFramesPerSecond = 5
// Fallback on earlier versions
}
return displayLink
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
if let img = UIImage(named: "new") { // 1
let imageSize = img.size
let layer = CALayer() // 2
layer.bounds = CGRect(x: 0.0, y: 0.0, width: currentWidth * 50, height: currentHeight * 5) // 3
// layer.position = CGPoint(x: imageSize.width/2, y: imageSize.height/2) // 4
layer.contents = img.cgImage // 5
view.layer.addSublayer(scrollLayer) // 6
scrollLayer.addSublayer(layer) // 7
}
displayLink.add(to: RunLoop.current, forMode: .common)
}
#objc func scrollLayerScroll() {
let newPoint = CGPoint(x: translation, y: 0.0)
scrollLayer.scroll(newPoint)
translation += 10.0
// if translation > 1600.0 {
//// stopDisplayLink()
// }
}
func stopDisplayLink() {
displayLink.invalidate()
}
}
Here setting width too high for animating the layer extra time
layer.bounds = CGRect(x: 0.0, y: 0.0, width: currentWidth * 50, height: currentHeight * 5) // 3
We ended up with following solution by rob mayoff
Animate infinite scrolling of an image in a seamless loop
Please find the updated code.
class CloudPassingVC: UIViewController {
#IBOutlet weak var imageToAnimate: UIImageView!
var cloudLayer: CALayer? = nil
var cloudLayerAnimation: CABasicAnimation? = nil
override func viewDidLoad() {
super.viewDidLoad()
cloudScroll()
}
func cloudScroll() {
let cloudsImage = UIImage(named: "clouds")
let cloudPattern = UIColor(patternImage: cloudsImage!)
cloudLayer = CALayer()
cloudLayer!.backgroundColor = cloudPattern.cgColor
cloudLayer!.transform = CATransform3DMakeScale(1, -1, 1)
cloudLayer!.anchorPoint = CGPoint(x: 0, y: 1)
let viewSize = imageToAnimate.bounds.size
cloudLayer!.frame = CGRect(x: 0, y: 0, width: (cloudsImage?.size.width ?? 0.0) + viewSize.width, height: viewSize.height)
imageToAnimate.layer.addSublayer(cloudLayer!)
let startPoint = CGPoint.zero
let endPoint = CGPoint(x: -(cloudsImage?.size.width ?? 0.0), y: 0)
cloudLayerAnimation = CABasicAnimation(keyPath: "position")
cloudLayerAnimation!.timingFunction = CAMediaTimingFunction(name: .linear)
cloudLayerAnimation!.fromValue = NSValue(cgPoint: startPoint)
cloudLayerAnimation!.toValue = NSValue(cgPoint: endPoint)
cloudLayerAnimation!.repeatCount = 600
cloudLayerAnimation!.duration = 10.0
applyCloudLayerAnimation()
}
func applyCloudLayerAnimation() {
cloudLayer!.add(cloudLayerAnimation!, forKey: "position")
}
}

how to programmatically show a colored disk behind a UIImageView, in swift on an iOS device

I have an icon that I programmatically display.
How can I display a solid, colored disk behind the icon, at the same screen position?
Icon is opaque.
I will sometimes programmatically change screen position and disk's diameter and color.
Here's how I programmatically display the icon now.............
DispatchQueue.main.async
{
ViewController.wrist_band_UIImageView.frame = CGRect( x: screen_position_x,
y: screen_position_y,
width: App_class.screen_width,
height: App_class.screen_height)
}
UPDATE #1 for D.Mika below...
UPDATE #2 for D.Mika below...
import UIKit
import Foundation
class ViewController: UIViewController
{
static var circleView: UIView!
static let wrist_band_UIImageView: UIImageView = {
let theImageView = UIImageView()
theImageView.image = UIImage( systemName: "applewatch" )
theImageView.translatesAutoresizingMaskIntoConstraints = false
return theImageView
}()
override func viewDidLoad()
{
super.viewDidLoad()
view.addSubview( ViewController.wrist_band_UIImageView )
// Init disk image:
ViewController.circleView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0))
ViewController.circleView.backgroundColor = .red
ViewController.circleView.layer.cornerRadius = view.bounds.size.height / 2.0
ViewController.circleView.clipsToBounds = true
// Add view to view hierarchy below image view
ViewController.circleView.insertSubview(view, belowSubview: ViewController.wrist_band_UIImageView)
App_class.display_single_wearable()
}
}
class App_class
{
static var is_communication_established = false
static var total_packets = 0
static func display_single_wearable()
{
ViewController.wrist_band_UIImageView.frame = CGRect(x: 0, y: 0,
width: 100, height: 100)
ViewController.circleView.frame = CGRect( x: 0,
y: 0,
width: 100,
height: 100)
ViewController.circleView.layer.cornerRadius = 100
}
static func process_location_xy(text_location_xyz: String)
{}
} // App_class
You can display a colored circle using the following view:
circleView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0))
circleView.backgroundColor = .red
circleView.layer.cornerRadius = view.bounds.size.height / 2.0
circleView.clipsToBounds = true
// Add view to view hierarchy below image view
view.insertSubview(circleView, belowSubview: wrist_band_UIImageView)
You can change it's position and size by applying a new frame. But you have to change the layer's cornerRadius accordingly.
This is a simple example in a playground:
Edit:
I've modified the sample code to add the view the view hierarchy.
BUT, you still need to adjust the circle's frame, when you modify the image view's frame. (and the cornerRadius accordingly). For example
DispatchQueue.main.async
{
ViewController.wrist_band_UIImageView.frame = CGRect( x: screen_position_x,
y: screen_position_y,
width: App_class.screen_width,
height: App_class.screen_height)
circleView.frame = CGRect( x: screen_position_x,
y: screen_position_y,
width: App_class.screen_width,
height: App_class.screen_height)
circleView.layer.cornerRadius = App_class.screen_width
}
And you need to add a variable to the viewController holding the reference to the circle view, like:
var circleView: UIView!
The following answer works.
The answers from d.mika above did not work. Plus, he was insulting. ;-)
// Show red circle
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 200, y: 400), radius: CGFloat(40), startAngle: CGFloat(0), endAngle: CGFloat(Double.pi * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
// Change the fill color
shapeLayer.fillColor = UIColor.red.cgColor
// You can change the stroke color
shapeLayer.strokeColor = UIColor.red.cgColor
// You can change the line width
shapeLayer.lineWidth = 3.0
view.layer.addSublayer(shapeLayer)

Set values greater than 1 or less than 0 for UIViewPropertyAnimator's fractionComplete

I'm setting propertyAnimator?.fractionComplete to change the position of a view, based on my pan gesture's translation. Later, I'm planning to animate it back to CGPoint.zero once the finger lifts. Here's my code:
class ViewController: UIViewController {
let squareContainerView = UIView(frame: CGRect(x: 100, y: 70, width: 230, height: 30))
let squareView = UIView(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
let targetSquareViewOrigin = CGFloat(200)
let label = UILabel(frame: CGRect(x: 25, y: 140, width: 350, height: 30))
var propertyAnimator: UIViewPropertyAnimator?
var panGesture: UIPanGestureRecognizer!
var totalTranslation = CGFloat(0) {
didSet {
label.text = "Translation: \(totalTranslation.rounded()), percentage: \(totalTranslation / squareContainerView.frame.width)"
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(squareContainerView)
squareContainerView.addSubview(squareView)
view.addSubview(label)
squareContainerView.backgroundColor = UIColor.red
squareView.backgroundColor = UIColor.blue
label.text = "Translation: \(totalTranslation.rounded()), percentage: \(totalTranslation / squareContainerView.frame.width)"
panGesture = UIPanGestureRecognizer(target: self, action:(#selector(self.handleGesture(_:))))
squareContainerView.addGestureRecognizer(panGesture)
propertyAnimator = UIViewPropertyAnimator(duration: 1, curve: .linear)
propertyAnimator?.addAnimations {
self.squareView.frame.origin.x = self.targetSquareViewOrigin
}
}
#objc func handleGesture(_ sender: UIPanGestureRecognizer) {
totalTranslation += sender.translation(in: squareContainerView).x
let fractionComplete = totalTranslation / squareContainerView.frame.width
propertyAnimator?.fractionComplete = fractionComplete
sender.setTranslation(.zero, in: squareContainerView) /// reset to zero
}
}
The square view is moving, but when the percentage goes above 1 or below 0, it stops. This makes sense, as its original origin was CGPoint.zero and I set a 200 end value with addAnimations().
But, I want it to continue moving, even when the fraction complete goes over. In the documentation it says:
When defining a custom animator, you may allow values greater than 1.0 or less than 0.0 if you support animations running past their beginning or end points.
How can I enable this? Should I create a subclass, and if so, what methods should I override?

CALayer as left border don´t working?

I have two UITextField in a regular ViewController, and i put a green border in left side with CALayer.
my textfield
Here is the code that i developed:
let borderU = CALayer()
let borderP = CALayer()
let posx = CGFloat(2.0)
let height:CGFloat = (txtfUser?.frame.size.height)!
if #available(iOS 11.0, *) {
borderU.borderColor = UIColor(named: "primaryGreen")?.cgColor
borderP.borderColor = UIColor(named: "primaryGreen")?.cgColor
} else {
// Fallback on earlier versions
}
borderU.frame = CGRect(x: posx, y: -2, width: 2, height: height)
borderP.frame = CGRect(x: posx, y: -2, width: 2, height: height)
borderU.borderWidth = CGFloat(2.0)
borderP.borderWidth = CGFloat(2.0)
self.txtfUser?.layer.addSublayer(borderU)
self.txtfUser?.layer.masksToBounds = true
self.txtfPassWord?.layer.addSublayer(borderP)
self.txtfPassWord?.layer.masksToBounds = true
In next View controller i have nine UITextField and I need the same border in left side and i thought about that solution for don´t repeat the code nine times, but didn´t work:
let border = CALayer()
let posx = CGFloat(2.0)
let height:CGFloat = (txtfUser?.frame.size.height)!
if #available(iOS 11.0, *) {
border.borderColor = UIColor(named: "primaryGreen")?.cgColor
} else {
// Fallback on earlier versions
}
border.frame = CGRect(x: posx, y: -2, width: 2, height: height)
border.borderWidth = CGFloat(2.0)
self.txtfUser?.layer.addSublayer(border)
self.txtfUser?.layer.masksToBounds = true
self.txtfPassWord?.layer.addSublayer(border)
self.txtfPassWord?.layer.masksToBounds = true
Any help will be appreciated.
You can subclass your textField:
class TextField: UITextField {
private var leftLayer: CALayer!
override func awakeFromNib() {
super.awakeFromNib()
//create your layer here, and keep reference in leftLayer for further resize, color...
}
}

Resources