'Animating 3 dots in swift iOS

I have a label and an image view. My goal is to have 3 dots animating infront of the label and to be at the foot of the label at the end as shown here desired result

I have been able to make this design with the dots moving fine on 12promax only however I can not figure out how to make this work always on different phone screen sizes as intended. How do I code it so no matter the screen size or uilabel fontsize It will achieve the same result?

Animate ImageView (3 dots)

  func showAnimatingDotsInImageView(dots: UIImageView)
 {
        let newX = view.bounds.width / 896 * 20
        let lay = CAReplicatorLayer()
        lay.frame = CGRect(x: newX,y: 0,width: dots.bounds.width,height: dots.bounds.height)
        let bar = CALayer()
        bar.frame = CGRect(x: 0,y: (dots.bounds.height/2) + 8 ,width: 8,height: 8)  //make the objs smaller or bigger
        bar.cornerRadius = bar.frame.width / 2  //make a circle, if you uncomment this you will get rects
        bar.backgroundColor = UIColor.black.cgColor   //colour of the objs
        lay.addSublayer(bar)
        lay.instanceCount = 3   //How many instances / objs do you want to see
        lay.instanceTransform = CATransform3DMakeTranslation(15, 0, 0) //1st arg is the spacing between the instances
        let anim = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
        anim.fromValue = 1.0
        anim.toValue = 0.2
        anim.duration = 1
        anim.repeatCount = .infinity
        bar.add(anim, forKey: nil)
        lay.instanceDelay = anim.duration / Double(lay.instanceCount)
    
        dots.layer.addSublayer(lay)    // add to the view

        }

'Retrieving boxes from the main menu' is a UILabel and the dots are animating on an UIImageView



Solution 1:[1]

You're pretty close... just a few changes should get you where you want to be.

If you have a UILabel and a UIImageView (or plain UIView) constrained to the label's right-edge, you should be able to position your CAReplicatorLayer without having to worry about "screen size."

Take a look at how I have modified your code:

// baseline = to put the bottom of the dots at the baseline of the text in the label
// dotXOffset = gap between end of label and first dot
// dotSize = dot width and height
// dotSpacing = gap between dots
func showAnimatingDotsInImageView(dotsView: UIView, baseline: CGFloat, dotXOffset: CGFloat, dotSize: CGFloat, dotSpacing: CGFloat) {
    let lay = CAReplicatorLayer()
    let bar = CALayer()
    bar.frame = CGRect(x: dotXOffset, y: baseline - dotSize, width: dotSize, height: dotSize)
    bar.cornerRadius = bar.frame.width / 2  // we want round dots
    bar.backgroundColor = UIColor.black.cgColor
    lay.addSublayer(bar)
    lay.instanceCount = 3   //How many instances / objs do you want to see
    lay.instanceTransform = CATransform3DMakeTranslation(dotSpacing, 0, 0) //1st arg is the spacing between the instances
    let anim = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
    anim.fromValue = 1.0
    anim.toValue = 0.2
    anim.duration = 1
    anim.repeatCount = .infinity
    bar.add(anim, forKey: nil)
    lay.instanceDelay = anim.duration / Double(lay.instanceCount)
    dotsView.layer.addSublayer(lay)    // add to the view
}

Here's a complete example:

class SimpleViewController: UIViewController {
    
    let testLabel = UILabel()
    let testDotsView = UIView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        testLabel.font = .systemFont(ofSize: 24.0)
        
        testLabel.text = "Retrieving boxes"
        
        // so we can see the label frame
        testLabel.backgroundColor = .cyan
        
        testLabel.translatesAutoresizingMaskIntoConstraints = false
        testDotsView.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(testLabel)
        view.addSubview(testDotsView)
        
        // always respect safe area
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            // let's constrain the label
            //  40-pts from Leading
            //  40-pts from Bottom
            testLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            testLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
            
            // constrain dots view to
            //  Top of label
            //  Trailing of label
            testDotsView.topAnchor.constraint(equalTo: testLabel.topAnchor),
            testDotsView.leadingAnchor.constraint(equalTo: testLabel.trailingAnchor, constant: 0.0),
            // dots image view Width and Height can be 0 (we can draw the layer outside the bounds)
            testDotsView.heightAnchor.constraint(equalToConstant: 0.0),
            testDotsView.widthAnchor.constraint(equalToConstant: 0.0),
            
        ])
        
        // get the label font's baseline y-value
        let bl: CGFloat = testLabel.font.ascender
        showAnimatingDotsInImageView(dotsView: testDotsView, baseline: bl, dotXOffset: 4.0, dotSize: 4.0, dotSpacing: 8.0)
    }
    
    // baseline = to put the bottom of the dots at the baseline of the text in the label
    // dotXOffset = gap between end of label and first dot
    // dotSize = dot width and height
    // dotSpacing = gap between dots
    func showAnimatingDotsInImageView(dotsView: UIView, baseline: CGFloat, dotXOffset: CGFloat, dotSize: CGFloat, dotSpacing: CGFloat) {
        let lay = CAReplicatorLayer()
        let bar = CALayer()
        bar.frame = CGRect(x: dotXOffset, y: baseline - dotSize, width: dotSize, height: dotSize)
        bar.cornerRadius = bar.frame.width / 2  // we want round dots
        bar.backgroundColor = UIColor.black.cgColor
        lay.addSublayer(bar)
        lay.instanceCount = 3   //How many instances / objs do you want to see
        lay.instanceTransform = CATransform3DMakeTranslation(dotSpacing, 0, 0) //1st arg is the spacing between the instances
        let anim = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
        anim.fromValue = 1.0
        anim.toValue = 0.2
        anim.duration = 1
        anim.repeatCount = .infinity
        bar.add(anim, forKey: nil)
        lay.instanceDelay = anim.duration / Double(lay.instanceCount)
        dotsView.layer.addSublayer(lay)    // add to the view
    }

}

Edit - in response to "krishan kumar" comment...

To get the animation to resume when returning from the background, you'll want to add a Notification Observer.

It will be much easier to use a custom UIView subclass for the "animated dots" view, so here's a quick example:

class DotsView: UIView {
    
    // baseline = to put the bottom of the dots at the baseline of the text in the label
    // dotXOffset = gap between end of label and first dot
    // dotSize = dot width and height
    // dotSpacing = gap between dots

    public var baseline: CGFloat = 0
    public var dotXOffset: CGFloat = 4.0
    public var dotSize: CGFloat = 4.0
    public var dotSpacing: CGFloat = 8.0

    private let lay = CAReplicatorLayer()
    private let bar = CALayer()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        lay.addSublayer(bar)
        layer.addSublayer(lay)
    }
    public func beginAnimating() {
        bar.frame = CGRect(x: dotXOffset, y: baseline - dotSize, width: dotSize, height: dotSize)
        // we want round dots
        bar.cornerRadius = bar.frame.width / 2.0
        bar.backgroundColor = UIColor.black.cgColor
        //How many instances / objs we want to see
        lay.instanceCount = 3
        //1st arg is the spacing between the instances
        lay.instanceTransform = CATransform3DMakeTranslation(dotSpacing, 0, 0)
        let anim = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
        anim.fromValue = 1.0
        anim.toValue = 0.2
        anim.duration = 1
        anim.repeatCount = .infinity
        bar.add(anim, forKey: nil)
        // so the dots animate in sequence
        lay.instanceDelay = anim.duration / Double(lay.instanceCount)
    }
    public func stopAnimating() {
        layer.removeAllAnimations()
    }
}

and an example controller showing how to use it, including starting/stopping the animation when the app moves between foreground and background:

class SimpleViewController: UIViewController {
    
    let testLabel = UILabel()
    
    // custom DotsView
    let testDotsView = DotsView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBackground
        
        testLabel.font = .systemFont(ofSize: 24.0)
        
        testLabel.text = "Retrieving boxes"
        
        // so we can see the label frame
        testLabel.backgroundColor = .cyan
        
        testLabel.translatesAutoresizingMaskIntoConstraints = false
        testDotsView.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(testLabel)
        view.addSubview(testDotsView)
        
        // always respect safe area
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            // let's constrain the label
            //  40-pts from Leading
            //  40-pts from Bottom
            testLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            testLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
            
            // constrain dots view to
            //  Top of label
            //  Trailing of label
            testDotsView.topAnchor.constraint(equalTo: testLabel.topAnchor),
            testDotsView.leadingAnchor.constraint(equalTo: testLabel.trailingAnchor, constant: 0.0),
            // dots image view Width and Height can be 0 (we can draw the layer outside the bounds)
            testDotsView.heightAnchor.constraint(equalToConstant: 0.0),
            testDotsView.widthAnchor.constraint(equalToConstant: 0.0),
            
        ])
        
        // get the label font's baseline y-value
        testDotsView.baseline = testLabel.font.ascender
        
        // use defaults or set values here
        //testDotsView.dotXOffset = 4.0
        //testDotsView.dotSize = 4.0
        //testDotsView.dotSpacing = 8.0
        
        testDotsView.beginAnimating()

        // we want to
        //  Stop the Dots animation when the app goes into the Background, and
        //  Start the Dots animation when the app Enters the Foreground
        NotificationCenter.default.addObserver(self, selector: #selector(myEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(myEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
    }
    
    @objc func myEnterBackground() {
        testDotsView.stopAnimating()
    }
    @objc func myEnterForeground() {
        testDotsView.beginAnimating()
    }
    
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1