'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
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 |