This question could be closed as a duplicate, but because there are a couple things that are different and the solution may not completely obvious I'm posting this answer...
For reference, this would be the duplicate post: https://stackoverflow.com/a/50074722/6257435
and the related code can be found here: https://gist.github.com/ArtFeel/ad4b108f026e53723c7457031e291bc8
However, links can go away, so I'll include all the code below.
First, as mentioned in the comments, you are adding a new instance of your animation view every time viewDidAppear()
is called. So, let's move the view creation to viewDidLoad()
:
class PersistentAnimationViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// do this here in viewDidLoad
// NOT in viewDidAppear!
let x = AnimationView(frame: .zero)
x.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(x)
x.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true
x.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor).isActive = true
x.widthAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.widthAnchor).isActive = true
x.heightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.heightAnchor).isActive = true
}
}
Next, it's generally a bad idea to start animations (or other activity) in a class init
, so we'll put the animation code in didMoveToSuperview()
instead.
Also, you were executing foregroundView.layer.cornerRadius = self.frame.width / 2
during init, when the view may or may not have a frame. That should go in layoutSubviews()
.
We can get better control of animations by using CABasicAnimation
instead of UIView.animate(...)
, so we'll make that change as well.
Finally, we'll use the referenced extensions and custom layer subclass to enable the animation to pause/resume when the app goes into / comes back from the background.
So, your AnimationView
class (modified):
class AnimationView: UIView {
private let backgroundView = UIView()
private let foregroundView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
// setting up the views
backgroundView.backgroundColor = .green
foregroundView.backgroundColor = .orange
// if you want rounded corners (or a "circle" view)
// put this in layoutSubviews()
//foregroundView.layer.cornerRadius = self.frame.width / 2
// adding the views
addSubview(backgroundView)
addSubview(foregroundView)
// activating the views
backgroundView.translatesAutoresizingMaskIntoConstraints = false
foregroundView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
backgroundView.topAnchor.constraint(equalTo: self.topAnchor),
backgroundView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
backgroundView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
backgroundView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
foregroundView.topAnchor.constraint(equalTo: self.topAnchor),
foregroundView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
foregroundView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
foregroundView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
])
// make the layer animation
// pause when app goes to background
// resume when app comes to foreground
// see below
self.foregroundView.layer.makeAnimationsPersistent()
}
// don't start the animation during init
// this is a reasonable place
override func didMoveToSuperview() {
super.didMoveToSuperview()
let animation = CABasicAnimation(keyPath:"transform.scale.xy")
animation.beginTime = CACurrentMediaTime()
animation.fromValue = 1.0
animation.toValue = 0.3
// set desired timing, or leave it at the default
//animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
animation.duration = 2.0
animation.repeatCount = .infinity
animation.autoreverses = true
self.foregroundView.layer.add(animation, forKey: "scale")
}
override func layoutSubviews() {
super.layoutSubviews()
// here in layoutSubviews we have a valid frame/bounds
// so do anything like rounding corners, etc
foregroundView.layer.cornerRadius = bounds.width / 2
}
}
and the required extension / layer subclass code:
// from: https://stackoverflow.com/a/50074722/6257435
// extensions and LayerPersistentHelper class found here:
// https://gist.github.com/ArtFeel/ad4b108f026e53723c7457031e291bc8
public extension CALayer {
var isAnimationsPaused: Bool {
return speed == 0.0
}
func pauseAnimations() {
if !isAnimationsPaused {
let currentTime = CACurrentMediaTime()
let pausedTime = convertTime(currentTime, from: nil)
speed = 0.0
timeOffset = pausedTime
}
}
func resumeAnimations() {
let pausedTime = timeOffset
speed = 1.0
timeOffset = 0.0
beginTime = 0.0
let currentTime = CACurrentMediaTime()
let timeSincePause = convertTime(currentTime, from: nil) - pausedTime
beginTime = timeSincePause
}
}
public extension CALayer {
static private var persistentHelperKey = "CALayer.LayerPersistentHelper"
func makeAnimationsPersistent() {
var object = objc_getAssociatedObject(self, &CALayer.persistentHelperKey)
if object == nil {
object = LayerPersistentHelper(with: self)
let nonatomic = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
objc_setAssociatedObject(self, &CALayer.persistentHelperKey, object, nonatomic)
}
}
}
public class LayerPersistentHelper {
private var persistentAnimations: [String: CAAnimation] = [:]
private var persistentSpeed: Float = 0.0
private weak var layer: CALayer?
public init(with layer: CALayer) {
self.layer = layer
addNotificationObservers()
}
deinit {
removeNotificationObservers()
}
}
private extension LayerPersistentHelper {
func addNotificationObservers() {
let center = NotificationCenter.default
let enterForeground = UIApplication.willEnterForegroundNotification
let enterBackground = UIApplication.didEnterBackgroundNotification
center.addObserver(self, selector: #selector(didBecomeActive), name: enterForeground, object: nil)
center.addObserver(self, selector: #selector(willResignActive), name: enterBackground, object: nil)
}
func removeNotificationObservers() {
NotificationCenter.default.removeObserver(self)
}
func persistAnimations(with keys: [String]?) {
guard let layer = self.layer else { return }
keys?.forEach { (key) in
if let animation = layer.animation(forKey: key) {
persistentAnimations[key] = animation
}
}
}
func restoreAnimations(with keys: [String]?) {
guard let layer = self.layer else { return }
keys?.forEach { (key) in
if let animation = persistentAnimations[key] {
layer.add(animation, forKey: key)
}
}
}
}
@objc extension LayerPersistentHelper {
func didBecomeActive() {
guard let layer = self.layer else { return }
restoreAnimations(with: Array(persistentAnimations.keys))
persistentAnimations.removeAll()
if persistentSpeed == 1.0 { // if layer was playing before background, resume it
layer.resumeAnimations()
}
}
func willResignActive() {
guard let layer = self.layer else { return }
persistentSpeed = layer.speed
layer.speed = 1.0 // in case layer was paused from outside, set speed to 1.0 to get all animations
persistAnimations(with: layer.animationKeys())
layer.speed = persistentSpeed // restore original speed
layer.pauseAnimations()
}
}