I am trying to use a CABasicAnimation for the timing function with custom objects (not UIView).
I'm trying to implement #CIFilter's answer from here which is to use the CALayer's presentation layer that is animated to evaluate the timing function.
I'm doing it all in viewDidAppear, so a valid view exists, but no matter what I do, the Presentation layer is always nil.
Note that I have to add the animation to the view's layer and not the layer I've added to it for it to animate at all. And if I uncomment the lines commented out below I can see that the animation works (but only when animating the root layer). Regardless, the Presentation layer is nil.
I've looked at dozen's of tutorials and SO answers, and it seems this should just work, so I suppose I must be doing something stupid.
I am just trying to use the CoreAnimation timing functions. I have UICubicTimingParameters working, but seems like going the CA route offers much more functionality which would be nice.
import UIKit
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let newView = UIView(frame: view.frame)
view.addSubview(newView)
let evaluatorLayer = CALayer()
evaluatorLayer.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
evaluatorLayer.borderWidth = 8.0
evaluatorLayer.borderColor = UIColor.purple.cgColor
evaluatorLayer.timeOffset = 0.3
evaluatorLayer.isHidden = true
// evaluatorLayer.isHidden = false
newView.layer.addSublayer(evaluatorLayer)
let basicAnimation = CABasicAnimation(keyPath: "bounds.origin.x")
basicAnimation.duration = 1.0
basicAnimation.fromValue = 0.0
basicAnimation.toValue = 100.0
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
basicAnimation.speed = 0.0
// basicAnimation.speed = 0.1
newView.layer.add(basicAnimation, forKey: "evaluate")
if let presentationLayer = newView.layer.presentation() {
let evaluatedValue = presentationLayer.bounds.origin.x
print("evaluatedValue: \(evaluatedValue)")
}
else {
print(evaluatorLayer.presentation())
}
}
}
Not sure if your code is going to do what you expect, but...
I think the reason .presentation() is nil is because you haven't given UIKit an opportunity to apply the animation.
Try this:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let newView = UIView(frame: view.frame)
view.addSubview(newView)
let evaluatorLayer = CALayer()
evaluatorLayer.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
evaluatorLayer.borderWidth = 8.0
evaluatorLayer.borderColor = UIColor.purple.cgColor
evaluatorLayer.timeOffset = 0.3
evaluatorLayer.isHidden = true
// evaluatorLayer.isHidden = false
newView.layer.addSublayer(evaluatorLayer)
let basicAnimation = CABasicAnimation(keyPath: "bounds.origin.x")
basicAnimation.duration = 1.0
basicAnimation.fromValue = 0.0
basicAnimation.toValue = 100.0
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
basicAnimation.speed = 0.0
// basicAnimation.speed = 0.1
newView.layer.add(basicAnimation, forKey: "evaluate")
DispatchQueue.main.async {
if let presentationLayer = newView.layer.presentation() {
let evaluatedValue = presentationLayer.bounds.origin.x
print("async evaluatedValue: \(evaluatedValue)")
}
else {
print("async", evaluatorLayer.presentation())
}
}
if let presentationLayer = newView.layer.presentation() {
let evaluatedValue = presentationLayer.bounds.origin.x
print("immediate evaluatedValue: \(evaluatedValue)")
}
else {
print("immediate", evaluatorLayer.presentation())
}
}
My debug output is:
immediate nil
async evaluatedValue: 0.0
Edit
I'm still not sure what your goal is, but give this a try...
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let newView = UIView(frame: view.frame)
view.addSubview(newView)
let evaluatorLayer = CALayer()
evaluatorLayer.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
evaluatorLayer.borderWidth = 8.0
evaluatorLayer.borderColor = UIColor.purple.cgColor
evaluatorLayer.isHidden = true
//evaluatorLayer.isHidden = false
newView.layer.addSublayer(evaluatorLayer)
let basicAnimation = CABasicAnimation(keyPath: "bounds.origin.x")
basicAnimation.duration = 1.0
basicAnimation.fromValue = 0.0
basicAnimation.toValue = 100.0
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
basicAnimation.speed = 0.0
//basicAnimation.speed = 1.0
// set timeOffset on the animation, not on the layer itself
basicAnimation.timeOffset = 0.3
// add animation to evaluatorLayer
evaluatorLayer.add(basicAnimation, forKey: "evaluate")
DispatchQueue.main.async {
// get presentation layer of evaluatorLayer
if let presentationLayer = evaluatorLayer.presentation() {
let evaluatedValue = presentationLayer.bounds.origin.x
print("async evaluatedValue: \(evaluatedValue)")
}
else {
print("async", evaluatorLayer.presentation())
}
}
}
In this example, we apply the .timeOffset on the animation, not on the layer. And, we add the animation to the evaluatorLayer, not to the newView.layer.
Output (for my quick test):
async evaluatedValue: 30.000001192092896
Here, I am creating a vanishing circular layer animation. My code is following:
import UIKit
class VanishingLoader: UIView {
private let loaderLayer = CAShapeLayer()
private var layerPaths = [UIBezierPath]()
private let animationframeDuration = 0.25
private var lastFrameDuration = 0.0
weak var parentView: UIView?
var parenViewCenter: CGPoint?{
if let frame = parentView?.bounds{
return CGPoint(x: frame.midX, y: frame.midY)
}
return nil
}
let animationGroup = CAAnimationGroup()
required init?(coder: NSCoder) {
super.init(coder: coder)
}
init(onView view: UIView) {
super.init(frame: view.frame)
parentView = view
layerPaths = preparedPathList
if layerPaths.count > 0 {
let path = layerPaths[0]
addVanishingStrokeLayer(path, withStrokeColor: UIColor.systemGreen, andStrokeWidth: nil)
}
}
private func addVanishingStrokeLayer(_ path: UIBezierPath, withStrokeColor strokeColor:UIColor?, andStrokeWidth strokeWidth: CGFloat?){
let initialPath = layerPaths[0].cgPath
loaderLayer.path = initialPath
loaderLayer.fillColor = UIColor.clear.cgColor
if let loaderLayerColor = strokeColor{
loaderLayer.strokeColor = loaderLayerColor.cgColor
}else{
loaderLayer.strokeColor = defaultStrokeColor.cgColor
}
if let lineWidth = strokeWidth{
loaderLayer.lineWidth = lineWidth
}else{
loaderLayer.lineWidth = 5.0
}
loaderLayer.add(vanishingAnimationGroup(), forKey: "vanishingGroupAnimation")
self.layer.addSublayer(loaderLayer)
}
var preparedPathList: [UIBezierPath]{
var pathList = [UIBezierPath]()
var multiplier : CGFloat = 0.3
for _ in 0..<8 {
if let path = getPathForLayer(withMultiplier: multiplier){
pathList.append(path)
multiplier += 0.10
}
}
return pathList
}
private func getPathForLayer(withMultiplier multiplier: CGFloat = 1.0)->UIBezierPath?{
let radius = 5.0
let path = UIBezierPath(arcCenter: .zero , radius: radius, startAngle: 0, endAngle: 4 * CGFloat.pi, clockwise: true)
return path
}
private func prepareForwardAnimationFrame(forPathCount count: Int)->CASpringAnimation{
let initialPath = layerPaths[count]
let finalPath = layerPaths[count + 1]
let pathAnimation = CASpringAnimation(keyPath: "path")
pathAnimation.fromValue = initialPath.cgPath
pathAnimation.toValue = finalPath.cgPath
pathAnimation.initialVelocity = 1
pathAnimation.damping = 5
pathAnimation.beginTime = lastFrameDuration
pathAnimation.duration = animationframeDuration
return pathAnimation
}
private func vanishingAnimationGroup()->CAAnimationGroup{
var animationPaths = [CASpringAnimation]()
for count in 0..<(layerPaths.count - 1){
let anim = prepareForwardAnimationFrame(forPathCount: count)
animationPaths.append(anim)
lastFrameDuration = anim.beginTime + animationframeDuration
}
animationGroup.animations = animationPaths
animationGroup.duration = (Double(animationPaths.count) * animationframeDuration)
animationGroup.repeatCount = .infinity
animationGroup.fillMode = .forwards
animationGroup.isRemovedOnCompletion = false
return animationGroup
}
public func hide(){
self.removeFromSuperview()
}
public func show(){
if let view = parentView{
view.addSubview(self)
}
}
}
From the ViewController, have to add the following two lines to see the effect.
let loader = VanishingLoader(onView: placeHolderImageView)
loader.show()
The problem, I am facing is when the animation sequence finishes, it goes back to the initial path which is the smallest of the seven paths and that gives a sudden jerking effect. Please help me.
First of all, this should be a key frame animation, not a grouped animation. Second, the change from last to first doesn’t animate because you didn’t animate it. Add another key frame that performs the missing animation.
I rewrote your animation as a key frame animation, and so far, I have this, but I don't think it is what you want, so please say more clearly what the intended effect is supposed to be:
how to make a line dash animation like ig story?i try to make dynamic line dash ,but failed ;
( CABasicAnimation *rotateAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
rotateAnimation.fromValue = #0;
rotateAnimation.toValue = #(M_PI_2*3);
CABasicAnimation * strokeStartAnimation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
strokeStartAnimation.fromValue = #(0);
strokeStartAnimation.toValue = #0.6;
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.animations = #[rotateAnimation,strokeStartAnimation];
animationGroup.duration = 1.5;
animationGroup.fillMode = kCAFillModeForwards;
animationGroup.removedOnCompletion = NO;
animationGroup.delegate = self;
animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.circlarShapeLayer addAnimation:animationGroup forKey:#"animationGroup"];
I would probably look into CAReplicatorLayer. You can do some interesting things with it. I took a quick stab in Playgrounds. You can play with the the timing and instance delay to see if you can get it to your liking. I also did not put a lot of thought into the instance color change. You could also set a gradient image to a layer and mask it with the CAShapeLayer and add that to the replicator. The only other way would be to add the layers manually which you could do using the same code that the CAReplicatorLayer uses in the below code and you would then have more control. You would just have a for loop and keep rotating the layer around or using a different portion of the stroke. Then keep and array of those and animate when needed.
import Foundation
import UIKit
import PlaygroundSupport
class InstagramProfileSpinner : UIView{
var circlePiece : CAShapeLayer = CAShapeLayer()
var replicator : CAReplicatorLayer = CAReplicatorLayer()
lazy var imageView : UIImageView = {
let img = UIImageView(frame: self.bounds)
img.layer.cornerRadius = self.bounds.width/2
img.layer.masksToBounds = true
img.contentMode = .scaleAspectFill
return img
}()
var avatarURL : URL?{
didSet{
configureAvatar()
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUpShapeLayer()
self.addSubview(imageView)
}
override init(frame: CGRect) {
super.init(frame: frame)
setUpShapeLayer()
self.addSubview(imageView)
}
func setUpShapeLayer(){
if let sublayers = self.layer.sublayers,sublayers.contains(replicator){} else{
circlePiece = CAShapeLayer()
circlePiece.frame = self.bounds
circlePiece.path = self.pathForCircle()
circlePiece.strokeColor = UIColor(red: 254/255, green: 136/255, blue: 7/255, alpha: 1).cgColor
circlePiece.lineWidth = 2.5
circlePiece.lineJoin = .round
circlePiece.lineCap = .round
circlePiece.fillColor = UIColor.clear.cgColor
circlePiece.strokeEnd = 0.025
let count = 1/circlePiece.strokeEnd
//set up replicator
replicator.instanceCount = Int(count)
let angle = (2.0*Double.pi)/Double(count)
replicator.instanceTransform = CATransform3DMakeRotation(CGFloat(angle), 0.0, 0.0, 1.0)
replicator.instanceRedOffset = -0.002
replicator.instanceGreenOffset = -0.04
replicator.instanceBlueOffset = -0.02
replicator.addSublayer(circlePiece)
self.layer.addSublayer(replicator)
}
}
func pathForCircle()->CGPath{
let path = UIBezierPath(ovalIn: self.bounds.insetBy(dx: 5, dy: 5))
return path.cgPath
}
func animateSpinner(){
let basic = CABasicAnimation(keyPath: "strokeEnd")
basic.fromValue = circlePiece.strokeEnd
basic.toValue = circlePiece.strokeEnd/5
basic.duration = 1
basic.autoreverses = true
basic.repeatCount = .infinity
replicator.instanceDelay = 0.25
circlePiece.add(basic, forKey: "littleStrokes")
}
func removeAnimation(){
if let animation = circlePiece.presentation(),
let stroke = animation.value(forKeyPath: "strokeEnd") as? CGFloat{
let final = CABasicAnimation(keyPath: "strokeEnd")
final.toValue = circlePiece.strokeEnd
final.duration = 1
replicator.instanceDelay = 0
circlePiece.add(final, forKey: "littleStrokes")
}
}
func configureAvatar(){
guard let url = self.avatarURL else{return}
self.imageView.image = nil
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let dt = data{
DispatchQueue.main.async {
self.imageView.image = UIImage(data: dt)
}
}
}.resume()
}
override func layoutSubviews() {
super.layoutSubviews()
replicator.frame = self.bounds
let inset = self.bounds.width * 0.08
imageView.frame = self.bounds.insetBy(dx: inset, dy:inset)
imageView.layer.cornerRadius = imageView.frame.width/2
}
}
class ViewController:UIViewController{
var check = true
var circle = InstagramProfileSpinner(frame: CGRect(x: 30, y: 30, width: 100, height: 100))
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(circle)
let url = URL(string: "https://images.pexels.com/photos/450271/pexels-photo-450271.jpeg?auto=compress&cs=tinysrgb&h=750&w=1260")
circle.avatarURL = url
circle.animateSpinner()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 10) {
self.circle.removeAnimation()
}
}
}
let viewController = ViewController()
PlaygroundPage.current.liveView = viewController
PlaygroundPage.current.needsIndefiniteExecution
I have animation
func startRotate360() {
let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.fromValue = 0
rotation.toValue = Double.pi * 2
rotation.duration = 1
rotation.isCumulative = true
rotation.repeatCount = Float.greatestFiniteMagnitude
self.layer.add(rotation, forKey: "rotationAnimation")
}
What I want is ability to stop animation by setting its repeat count to 1, so it completes current rotation (simply remove animation is not ok because it looks not good)
I try following
func stopRotate360() {
self.layer.animation(forKey: "rotationAnimation")?.repeatCount = 1
}
But I get crash and in console
attempting to modify read-only animation
How to access writable properties ?
Give this a go. You can in fact change CAAnimations that are in progress. There are so many ways. This is the fastest/simplest. You could even stop the animation completely and resume it without the user even noticing.
You can see the start animation function along with the stop. The start animation looks similar to yours while the stop grabs the current rotation from the presentation layer and creates an animation to rotate until complete. I also smoothed out the duration to be a percentage of the time needed to complete based on current rotation z to full rotation based on the running animation. Then I remove the animation with the repeat count and add the new animation. You can see the view rotate smoothly to the final position and stop. You will get the idea. Drop it in and run it and see what you think. Hit the button to start and hit it again to see it finish rotation and stop.
import UIKit
class ViewController: UIViewController {
var animationView = UIView()
var button = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
animationView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
animationView.backgroundColor = .green
animationView.center = view.center
self.view.addSubview(animationView)
let label = UILabel(frame: animationView.bounds)
label.text = "I Spin"
animationView.addSubview(label)
button = UIButton(frame: CGRect(x: 20, y: animationView.frame.maxY + 60, width: view.bounds.width - 40, height: 40))
button.setTitle("Animate", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(ViewController.pressed), for: .touchUpInside)
self.view.addSubview(button)
}
func pressed(){
if let title = button.titleLabel?.text{
let trans = CATransition()
trans.type = "rippleEffect"
trans.duration = 0.6
button.layer.add(trans, forKey: nil)
switch title {
case "Animate":
//perform animation
button.setTitle("Stop And Finish", for: .normal)
rotateAnimationRepeat()
break
default:
//stop and finish
button.setTitle("Animate", for: .normal)
stopAnimationAndFinish()
break
}
}
}
func rotateAnimationRepeat(){
//just to be sure because of how i did the project
animationView.layer.removeAllAnimations()
let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.fromValue = 0
rotation.toValue = Double.pi * 2
rotation.duration = 0.5
rotation.repeatCount = Float.greatestFiniteMagnitude
//not doing cumlative
animationView.layer.add(rotation, forKey: "rotationAnimation")
}
func stopAnimationAndFinish(){
if let presentation = animationView.layer.presentation(){
if let currentRotation = presentation.value(forKeyPath: "transform.rotation.z") as? CGFloat{
var duration = 0.5
//smooth out duration for change
duration = Double((CGFloat(Double.pi * 2) - currentRotation))/(Double.pi * 2)
animationView.layer.removeAllAnimations()
let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.fromValue = currentRotation
rotation.toValue = Double.pi * 2
rotation.duration = duration * 0.5
animationView.layer.add(rotation, forKey: "rotationAnimation")
}
}
}
}
Result:
2019 typical modern syntax
Setup the arc and the layer like this:
import Foundation
import UIKit
class RoundChaser: UIView {
private let lineThick: CGFloat = 10.0
private let beginFraction: CGFloat = 0.15
// where does the arc drawing begin?
// 0==top, .25==right, .5==bottom, .75==left
private lazy var arcPath: CGPath = {
let b = beginFraction * .pi * 2.0
return UIBezierPath(
arcCenter: bounds.centerOfCGRect(),
radius: bounds.width / 2.0 - lineThick / 2.0,
startAngle: .pi * -0.5 + b,
// recall that .pi * -0.5 is the "top"
endAngle: .pi * 1.5 + b,
clockwise: true
).cgPath
}()
private lazy var arcLayer: CAShapeLayer = {
let l = CAShapeLayer()
l.path = arcPath
l.fillColor = UIColor.clear.cgColor
l.strokeColor = UIColor.purple.cgColor
l.lineWidth = lineThick
l.lineCap = CAShapeLayerLineCap.round
l.strokeStart = 0
l.strokeEnd = 0
// if both are same, it is hidden. initially hidden
layer.addSublayer(l)
return l
}()
then initialization is this easy
open override func layoutSubviews() {
super.layoutSubviews()
arcLayer.frame = bounds
}
finally animation is easy
public func begin() {
CATransaction.begin()
let e : CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
e.duration = 2.0
e.fromValue = 0
e.toValue = 1.0
// recall 0 will be our beginFraction, see above
e.repeatCount = .greatestFiniteMagnitude
self.arcLayer.add(e, forKey: nil)
CATransaction.commit()
}
}
Maybe this is not the best solution but it works, as you say you can not modify properties of the CABasicAnimation once is created, also we need to remove the rotation.repeatCount = Float.greatestFiniteMagnitude, if notCAAnimationDelegatemethodanimationDidStop` is never called, with this approach the animation can be stoped without any problems as you need
step 1: first declare a variable flag to mark as you need stop animation in your custom class
var needStop : Bool = false
step 2: add a method to stopAnimation after ends
func stopAnimation()
{
self.needStop = true
}
step 3: add a method to get your custom animation
func getRotate360Animation() ->CAAnimation{
let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.fromValue = 0
rotation.toValue = Double.pi * 2
rotation.duration = 1
rotation.isCumulative = true
rotation.isRemovedOnCompletion = false
return rotation
}
step 4: Modify your startRotate360 func to use your getRotate360Animation() method
func startRotate360() {
let rotationAnimation = self.getRotate360Animation()
rotationAnimation.delegate = self
self.layer.add(rotationAnimation, forKey: "rotationAnimation")
}
step 5: Implement CAAnimationDelegate in your class
extension YOURCLASS : CAAnimationDelegate
{
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if(anim == self.layer?.animation(forKey: "rotationAnimation"))
{
self.layer?.removeAnimation(forKey: "rotationAnimation")
if(!self.needStop){
let animation = self.getRotate360Animation()
animation.delegate = self
self.layer?.add(animation, forKey: "rotationAnimation")
}
}
}
}
This works and was tested
Hope this helps you
I am loading a UIWebView and in the meantime I wan't to show a blank page with this activity indicator spinning (siri activity indicator). From what I have understand you can not change the image, but can't I use that image and create an animation with it rotating 360° and looping? Or will that drain the battery?
something like this?:
- (void)webViewDidStartLoad:(UIWebView *)webView {
//set up animation
[self.view addSubview:self.loadingImage];
//start animation
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
//stop animation
[self.loadingImage removeFromSuperview];
}
What should I do?
Thanks in advance!
Most of this is found in Stack Overflow. Let me summarize:
Create an UIImageView which will serve as an activity indicator (inside storyboard scene, NIB, code ... wherever you wish). Let's call it _activityIndicatorImage
Load your image: _activityIndicatorImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"activity_indicator"]];
You need to use animation to rotate it. Here is the method I use:
+ (void)rotateLayerInfinite:(CALayer *)layer
{
CABasicAnimation *rotation;
rotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
rotation.fromValue = [NSNumber numberWithFloat:0];
rotation.toValue = [NSNumber numberWithFloat:(2 * M_PI)];
rotation.duration = 0.7f; // Speed
rotation.repeatCount = HUGE_VALF; // Repeat forever. Can be a finite number.
[layer removeAllAnimations];
[layer addAnimation:rotation forKey:#"Spin"];
}
Inside my layoutSubviews method I initiate rotation. You could place this in your webViewDidStartLoad and webViewDidFinishLoad if this is better for your case:
- (void)layoutSubviews
{
[super layoutSubviews];
// some other code
[Utils rotateLayerInfinite:_activityIndicatorImage.layer];
}
You could always always stop rotation using [_activityIndicatorImage.layer removeAllAnimations];
You may use this beautiful loader inspired from Tumblr app:
Asich/AMTumblrHud
Swift 5
Another answer working perfect
Step 1.
Create swift file "CustomLoader.swift" and put this code in that file
import UIKit
import CoreGraphics
import QuartzCore
class CustomLoader: UIView
{
//MARK:- NOT ACCESSABLE OUT SIDE
fileprivate var duration : CFTimeInterval! = 1
fileprivate var isAnimating :Bool = false
fileprivate var backgroundView : UIView!
//MARK:- ACCESS INSTANCE ONLY AND CHANGE ACCORDING TO YOUR NEEDS *******
let colors : [UIColor] = [.red, .blue, .orange, .purple]
var defaultColor : UIColor = UIColor.red
var isUsrInteractionEnable : Bool = false
var defaultbgColor: UIColor = UIColor.white
var loaderSize : CGFloat = 80.0
/// **************** ****************** ////////// **************
//MARK:- MAKE SHARED INSTANCE
private static var Instance : CustomLoader!
static let sharedInstance : CustomLoader = {
if Instance == nil
{
Instance = CustomLoader()
}
return Instance
}()
//MARK:- DESTROY TO SHARED INSTANCE
#objc fileprivate func destroyShardInstance()
{
CustomLoader.Instance = nil
}
//MARK:- SET YOUR LOADER INITIALIZER FRAME ELSE DEFAULT IS CENTER
func startAnimation()
{
let win = UIApplication.shared.keyWindow
backgroundView = UIView()
backgroundView.frame = (UIApplication.shared.keyWindow?.frame)!
backgroundView.backgroundColor = UIColor.init(white: 0, alpha: 0.4)
win?.addSubview(backgroundView)
self.frame = CGRect.init(x: ((UIScreen.main.bounds.width) - loaderSize)/2, y: ((UIScreen.main.bounds.height) - loaderSize)/2, width: loaderSize, height: loaderSize)
self.addCenterImage()
self.isHidden = false
self.backgroundView.addSubview(self)
self.layer.cornerRadius = loaderSize/2
self.layer.masksToBounds = true
backgroundView.accessibilityIdentifier = "CustomLoader"
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.NSExtensionHostDidBecomeActive, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(CustomLoader.ResumeLoader), name: NSNotification.Name.NSExtensionHostDidBecomeActive, object: nil)
self.layoutSubviews()
}
//MARK:- AVOID STUCKING LOADER WHEN CAME BACK FROM BACKGROUND
#objc fileprivate func ResumeLoader()
{
if isAnimating
{
self.stopAnimation()
self.AnimationStart()
}
}
override func layoutSubviews()
{
super.layoutSubviews()
self.backgroundColor = defaultbgColor
UIApplication.shared.keyWindow?.isUserInteractionEnabled = isUsrInteractionEnable
self.AnimationStart()
}
#objc fileprivate func addCenterImage()
{
/// add image in center
let centerImage = UIImage(named: "Logo")
let imageSize = loaderSize/2.5
let centerImgView = UIImageView(image: centerImage)
centerImgView.frame = CGRect(
x: (self.bounds.width - imageSize) / 2 ,
y: (self.bounds.height - imageSize) / 2,
width: imageSize,
height: imageSize
)
centerImgView.contentMode = .scaleAspectFit
centerImgView.layer.cornerRadius = imageSize/2
centerImgView.clipsToBounds = true
self.addSubview(centerImgView)
}
//MARK:- CALL IT TO START THE LOADER , AFTER INITIALIZE THE LOADER
#objc fileprivate func AnimationStart()
{
if isAnimating
{
return
}
let size = CGSize.init(width: loaderSize , height: loaderSize)
let dotNum: CGFloat = 10
let diameter: CGFloat = size.width / 5.5 //10
let dot = CALayer()
let frame = CGRect(
x: (layer.bounds.width - diameter) / 2 + diameter * 2,
y: (layer.bounds.height - diameter) / 2,
width: diameter/1.3,
height: diameter/1.3
)
dot.backgroundColor = colors[0].cgColor
dot.cornerRadius = frame.width / 2
dot.frame = frame
let replicatorLayer = CAReplicatorLayer()
replicatorLayer.frame = layer.bounds
replicatorLayer.instanceCount = Int(dotNum)
replicatorLayer.instanceDelay = 0.1
let angle = (2.0 * M_PI) / Double(replicatorLayer.instanceCount)
replicatorLayer.instanceTransform = CATransform3DMakeRotation(CGFloat(angle), 0.0, 0.0, 1.0)
layer.addSublayer(replicatorLayer)
replicatorLayer.addSublayer(dot)
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.toValue = 0.4
scaleAnimation.duration = 0.5
scaleAnimation.autoreverses = true
scaleAnimation.repeatCount = .infinity
scaleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
dot.add(scaleAnimation, forKey: "scaleAnimation")
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotationAnimation.toValue = -2.0 * Double.pi
rotationAnimation.duration = 6.0
rotationAnimation.repeatCount = .infinity
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
replicatorLayer.add(rotationAnimation, forKey: "rotationAnimation")
if colors.count > 1 {
var cgColors : [CGColor] = []
for color in colors {
cgColors.append(color.cgColor)
}
let colorAnimation = CAKeyframeAnimation(keyPath: "backgroundColor")
colorAnimation.values = cgColors
colorAnimation.duration = 2
colorAnimation.repeatCount = .infinity
colorAnimation.autoreverses = true
dot.add(colorAnimation, forKey: "colorAnimation")
}
self.isAnimating = true
self.isHidden = false
}
//MARK:- CALL IT TO STOP THE LOADER
func stopAnimation()
{
if !isAnimating
{
return
}
UIApplication.shared.keyWindow?.isUserInteractionEnabled = true
let winSubviews = UIApplication.shared.keyWindow?.subviews
if (winSubviews?.count)! > 0
{
for viw in winSubviews!
{
if viw.accessibilityIdentifier == "CustomLoader"
{
viw.removeFromSuperview()
// break
}
}
}
layer.sublayers = nil
isAnimating = false
self.isHidden = true
self.destroyShardInstance()
}
//MARK:- GETTING RANDOM COLOR , AND MANAGE YOUR OWN COLORS
#objc fileprivate func randomColor()->UIColor
{
let randomRed:CGFloat = CGFloat(drand48())
let randomGreen:CGFloat = CGFloat(drand48())
let randomBlue:CGFloat = CGFloat(drand48())
return UIColor(red: randomRed, green: randomGreen, blue: randomBlue, alpha: 1.0)
}
override func draw(_ rect: CGRect)
{
}
}
find the func name and "addCenterImage" and replace the image name with your custom image.
Step 2
Create the AppDelegate class instance out side of the AppDelegate class like this.
var AppInstance: AppDelegate!
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate
{ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
AppInstance = self
}
Step 3.
put these two func in your AppDelegate
//MARK: - Activity Indicator -
func showLoader()
{
CustomLoader.sharedInstance.startAnimation()
}
func hideLoader()
{
CustomLoader.sharedInstance.stopAnimation()
}
Step 4. Use the functions like this whenever you want to animate your loader and stop.
AppInstance.showLoader()
AppInstance.hideLoader()
HAPPY LOADING...
SWIFT 4 Sweet And Simply just put extension UIView{}
Modified answer of #gandhi Mena
if you want to create your own custom Loading indicator
Create a UIView extension which create and customize your brand logo as a custom indicator put this code in you global declaration file.
extension UIView{
func customActivityIndicator(view: UIView, widthView: CGFloat?,backgroundColor: UIColor?, textColor:UIColor?, message: String?) -> UIView{
//Config UIView
self.backgroundColor = backgroundColor //Background color of your view which you want to set
var selfWidth = view.frame.width
if widthView != nil{
selfWidth = widthView ?? selfWidth
}
let selfHeigh = view.frame.height
let loopImages = UIImageView()
let imageListArray = ["image1", "image2"] // Put your desired array of images in a specific order the way you want to display animation.
loopImages.animationImages = imageListArray
loopImages.animationDuration = TimeInterval(0.8)
loopImages.startAnimating()
let imageFrameX = (selfWidth / 2) - 30
let imageFrameY = (selfHeigh / 2) - 60
var imageWidth = CGFloat(60)
var imageHeight = CGFloat(60)
if widthView != nil{
imageWidth = widthView ?? imageWidth
imageHeight = widthView ?? imageHeight
}
//ConfigureLabel
let label = UILabel()
label.textAlignment = .center
label.textColor = .gray
label.font = UIFont(name: "SFUIDisplay-Regular", size: 17.0)! // Your Desired UIFont Style and Size
label.numberOfLines = 0
label.text = message ?? ""
label.textColor = textColor ?? UIColor.clear
//Config frame of label
let labelFrameX = (selfWidth / 2) - 100
let labelFrameY = (selfHeigh / 2) - 10
let labelWidth = CGFloat(200)
let labelHeight = CGFloat(70)
// Define UIView frame
self.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width , height: UIScreen.main.bounds.size.height)
//ImageFrame
loopImages.frame = CGRect(x: imageFrameX, y: imageFrameY, width: imageWidth, height: imageHeight)
//LabelFrame
label.frame = CGRect(x: labelFrameX, y: labelFrameY, width: labelWidth, height: labelHeight)
//add loading and label to customView
self.addSubview(loopImages)
self.addSubview(label)
return self }}
Hide an indicator something like this you can remove subview at the top from the subview stack. put this code in the same globally declared swift file.
func hideLoader(removeFrom : UIView){
removeFrom.subviews.last?.removeFromSuperview()
}
Now you can shoot at the mark by this code
To display activity indicator in your view controller put this code when you want to display.
self.view.addSubview(UIView().customActivityIndicator(view: self.view, widthView: nil, backgroundColor:"Desired color", textColor: "Desired color", message: "Loading something"))
To hide animating loader you can user above function you defined in the globally. In your ViewController.swift where you want to hide put this line of code.
hideLoader(removeFrom: self.view)
imageListArray looks like this.
I've faced a similar issue lately. And this is my solution. Basically, it's what topic starter initially wanted: blank page with custom activity indicator on it.
I have partly used #Azharhussain Shaikh answer but I've implemented auto-layout instead of using frames and added a few other refinements with the intention to make usage as simple as possible.
So, it's an extension for UIView with two methods: addActivityIndicator() and removeActivityIndicator()
extension UIView {
func addActivityIndicator() {
// creating a view (let's call it "loading" view) which will be added on top of the view you want to have activity indicator on (parent view)
let view = UIView()
// setting up a background for a view so it would make content under it look like not active
view.backgroundColor = UIColor.white.withAlphaComponent(0.7)
// adding "loading" view to a parent view
// setting up auto-layout anchors so it would cover whole parent view
self.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
view.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
view.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
view.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
view.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
// creating array with images, which will be animated
// in my case I have 30 images with names activity0.png ... activity29.png
var imagesArray = [UIImage(named: "activity\(0)")!]
for i in 1..<30 {
imagesArray.append(UIImage(named: "activity\(i)")!)
}
// creating UIImageView with array of images
// setting up animation duration and starting animation
let activityImage = UIImageView()
activityImage.animationImages = imagesArray
activityImage.animationDuration = TimeInterval(0.7)
activityImage.startAnimating()
// adding UIImageView on "loading" view
// setting up auto-layout anchors so it would be in center of "loading" view with 30x30 size
view.addSubview(activityImage)
activityImage.translatesAutoresizingMaskIntoConstraints = false
activityImage.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
activityImage.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
activityImage.widthAnchor.constraint(equalToConstant: 30).isActive = true
activityImage.heightAnchor.constraint(equalToConstant: 30).isActive = true
}
func removeActivityIndicator() {
// checking if a view has subviews on it
guard let lastSubView = self.subviews.last else { return }
// removing last subview with an assumption that last view is a "loading" view
lastSubView.removeFromSuperview()
} }
"Rotating" effect is achieved by those 30 images you've put in imagesArray. Each image is a new frame of a rotating indicator like this.
Usage. In your view controller for showing an activity indicator simply put:
view.addActivityIndicator()
For removing an activity indicator:
view.removeActivityIndicator()
For example, in case of using it with table view (like I do) it can be used like this:
func setLoadingScreen() {
view.addActivityIndicator()
tableView.isScrollEnabled = false
}
func removeLoadingScreen() {
view.removeActivityIndicator()
tableView.isScrollEnabled = true
}
It works in Swift 4.
Swift 5.0 version of accepted Answer
public extension UIImageView {
func spin(duration: Float) {
let rotation = CABasicAnimation(keyPath: "transform.rotation")
rotation.fromValue = 0
rotation.toValue = 2 * Double.pi
rotation.duration = 0.7
rotation.repeatCount = duration
layer.add(rotation, forKey: "spin")
}
func stopSpinning() {
layer.removeAllAnimations()
}
}
Without Image , you can use third party library
for objective C (also support in iOS 6) https://github.com/shebinkoshy/UIControllsRepo
for swift https://github.com/shebinkoshy/Activity-Indicator-Swift
Advantages
-> Able to set colors for spinner
-> Available in different sizes like tiny, small, medium, large, very large
-> Able to set Title (center and bottom) for medium, large, very large sizes
You can set an images to your activityIndicator. I created a function for add custom image to activityIndicator. Here is what I created.
public func showProgressView(view: UIView) -> UIImageView {
let containerView = UIView()
let progressView = UIView()
var activityIndicatorImageView = UIImageView()
if let statusImage = UIImage(named: Constants.ActivityIndicatorImageName1) {
let activityImageView = UIImageView(image: statusImage)
containerView.frame = view.frame
containerView.backgroundColor = UIColor(hex: 0xffffff, alpha: 0.3)
progressView.frame = CGRectMake(0, 0, 80, 80)
progressView.center = CGPointMake(view.bounds.width / 2, view.bounds.height / 2)
progressView.backgroundColor = UIColor(hex: 0x18bda3, alpha: 0.7)
progressView.clipsToBounds = true
progressView.layer.cornerRadius = 10
activityImageView.animationImages = [UIImage(named: Constants.ActivityIndicatorImageName1)!,
UIImage(named: Constants.ActivityIndicatorImageName2)!,
UIImage(named: Constants.ActivityIndicatorImageName3)!,
UIImage(named: Constants.ActivityIndicatorImageName4)!,
UIImage(named: Constants.ActivityIndicatorImageName5)!]
activityImageView.animationDuration = 0.8;
activityImageView.frame = CGRectMake(view.frame.size.width / 2 - statusImage.size.width / 2, view.frame.size.height / 2 - statusImage.size.height / 2, 40.0, 48.0)
activityImageView.center = CGPointMake(progressView.bounds.width / 2, progressView.bounds.height / 2)
dispatch_async(dispatch_get_main_queue()) {
progressView.addSubview(activityImageView)
containerView.addSubview(progressView)
view.addSubview(containerView)
activityIndicatorImageView = activityImageView
}
}
return activityIndicatorImageView
}
You can call this method everywhere in your code. And just call the startAnimating method. If you want to hide just call the stopAnimating method.
it works in both SWITF 3 and 4
var activityIndicator = UIActivityIndicatorView()
var myView : UIView = UIView()
func viewDidLoad() {
spinnerCreation()
}
func spinnerCreation() {
activityIndicator.activityIndicatorViewStyle = .whiteLarge
let label = UILabel.init(frame: CGRect(x: 5, y: 60, width: 90, height: 20))
label.textColor = UIColor.white
label.font = UIFont.boldSystemFont(ofSize: 14.0)
label.textAlignment = NSTextAlignment.center
label.text = "Please wait...."
myView.frame = CGRect(x: (UIScreen.main.bounds.size.width - 100)/2, y: (UIScreen.main.bounds.size.height - 100)/2, width: 100, height: 100)
myView.backgroundColor = UIColor.init(white: 0.0, alpha: 0.7)
myView.layer.cornerRadius = 5
activityIndicator.center = CGPoint(x: myView.frame.size.width/2, y: myView.frame.size.height/2 - 10)
myView.addSubview(activityIndicator)
myView.addSubview(label)
myView.isHidden = true
self.window?.addSubview(myView)
}
#IBAction func activityIndicatorStart(_ sender: Any) {
myView.isHidden = false
self.activityIndicator.startAnimating()
self.view.isUserInteractionEnabled = false
self.view.bringSubview(toFront: myView)
}
#IBAction func activityIndicatorStop(_ sender: Any)() {
myView.isHidden = true
self.activityIndicator.stopAnimating()
self.view.isUserInteractionEnabled = true
}
You can create your custom activity Indicator with this in Swift 3 & 4:
Create a new file with name: UIViewExtension.Swift and copy this code and paste in your new file file:
import UIkit
extension UIView{
func customActivityIndicator(view: UIView, widthView: CGFloat? = nil,backgroundColor: UIColor? = nil, message: String? = nil,colorMessage:UIColor? = nil ) -> UIView{
//Config UIView
self.backgroundColor = backgroundColor ?? UIColor.clear
self.layer.cornerRadius = 10
var selfWidth = view.frame.width - 100
if widthView != nil{
selfWidth = widthView ?? selfWidth
}
let selfHeigh = CGFloat(100)
let selfFrameX = (view.frame.width / 2) - (selfWidth / 2)
let selfFrameY = (view.frame.height / 2) - (selfHeigh / 2)
let loopImages = UIImageView()
//ConfigCustomLoading with secuence images
let imageListArray = [UIImage(named:""),UIImage(named:""), UIImage(named:"")]
loopImages.animationImages = imageListArray
loopImages.animationDuration = TimeInterval(1.3)
loopImages.startAnimating()
let imageFrameX = (selfWidth / 2) - 17
let imageFrameY = (selfHeigh / 2) - 35
var imageWidth = CGFloat(35)
var imageHeight = CGFloat(35)
if widthView != nil{
imageWidth = widthView ?? imageWidth
imageHeight = widthView ?? imageHeight
}
//ConfigureLabel
let label = UILabel()
label.textAlignment = .center
label.textColor = .gray
label.font = UIFont.boldSystemFont(ofSize: 17)
label.numberOfLines = 0
label.text = message ?? ""
label.textColor = colorMessage ?? UIColor.clear
//Config frame of label
let labelFrameX = (selfWidth / 2) - 100
let labelFrameY = (selfHeigh / 2) - 10
let labelWidth = CGFloat(200)
let labelHeight = CGFloat(70)
//add loading and label to customView
self.addSubview(loopImages)
self.addSubview(label)
//Define frames
//UIViewFrame
self.frame = CGRect(x: selfFrameX, y: selfFrameY, width: selfWidth , height: selfHeigh)
//ImageFrame
loopImages.frame = CGRect(x: imageFrameX, y: imageFrameY, width: imageWidth, height: imageHeight)
//LabelFrame
label.frame = CGRect(x: labelFrameX, y: labelFrameY, width: labelWidth, height: labelHeight)
return self
}
}
And then you can use it in your ViewController like this:
import UIKit
class ExampleViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(UIView().customActivityIndicator(view: self.view,backgroundColor: UIColor.green))
}
//function for stop and desappear loading
func deseappearLoading(){
self.view.subviews.last?.removeFromSuperview()
}
}
Don't forget replace [UIImage(named:" "),UIImage(named:" "), UIImage(named:" ")] with your names of images and adjust the TimeInterval(1.3). Enjoy it.