UIView on UITableViewCell updating without animating - swift

I had a regression and I don't know what could have caused it.
Desired: smooth animation
Current: update without animation.
Here is the code that caused the break. I don't know why.
var pillModel: PillModel? {
didSet {
guard let pillModel = pillModel else { return }
DispatchQueue.main.async {
self.movePill(pillModel.side)
}
movingPill.backgroundColor = pillModel.movingPillColor
leftLabel.textColor = pillModel.leftLabelColor
rightLabel.textColor = pillModel.rightLabelColor
leftLabel.text = pillModel.leftTekst
rightLabel.text = pillModel.rightTekst
movingPill.layer.applySketchShadow(color: movingPill.backgroundColor!, alpha: 0.7, y: 3)
pillContainer.layoutSubviews()
commonStyle()
}
}
Here is some relevant code.
func movePill( _ sideTouched: Side, _ completion: (() -> ())? = nil) {
constrainPillTo(sideTouched)
pillModel?.side = sideTouched
UIView.animate(withDuration: 0.85, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: 1, options: .transitionCrossDissolve, animations: {
[weak self] in
guard let selfy = self else {return}
selfy.movingPill.backgroundColor = .orange//selfy.pillModel?.movingPillColor
selfy.leftLabel.textColor = .yellow//selfy.pillModel?.leftLabelColor
selfy.rightLabel.textColor = .green//selfy.pillModel?.rightLabelColor
selfy.movingPill.layer.applySketchShadow(color: selfy.movingPill.backgroundColor ?? .socialBlue,
alpha: 0.7, y: 3)
//selfy.pillContainer.layoutSubviews()
}) { _ in
completion?()
}
}

It turns out I created a cycle. When I moved the code out of the pillModel property's didSet method into its own function, the issue was resolved completely.

Related

need my animaiton to run continously until stopped, and not started over even on susequent call

I have the following animation. This works well however if image.flash is called in an if statement or a loop, the animation starts over and looks very unsmooth.
I need a way to allow the first call to the flash function to continue until stopped.
even if the function is called subsequent times.
extension UIView {
func flash() {
self.alpha = 0;
UIView.animate(withDuration: 0.5, delay: 0.0,
options: [.curveEaseInOut, .autoreverse, .repeat],
animations: { [weak self] in self?.alpha = 1.0 })
}
func stopFlash() {
layer.removeAllAnimations()
alpha = 0
}
}
Check if view is already in the middle of animation, to don't repeat your animation like using guard so before you add new animation remove previous ones.
func flash() {
// check if self is in the middle of view or containing an specific of animation
guard self.layer.animationKeys() == nil else { return }
self.alpha = 0;
UIView.animate(withDuration: 0.5, delay: 0.0,
options: [.curveEaseInOut, .autoreverse, .repeat],
animations: { [weak self] in
self?.alpha = 1.0
print(self?.layer.animationKeys()) // print type of animation
})
}

How to hide label when its text is nil in swift

I am adding badgeLabel to cartBtn.. here I am unable to convert integer to string and string to integer to hide badgeLabel if its count is 0
code:
public var badgeText: String? {
didSet {
let wasNil = NSString(string: badgeText ?? "0").integerValue <= 0
badgeLabel.isHidden = badgeText == nil
badgeLabel.text = badgeText
setSize()
if animated {
let animations: () -> Void = { [weak badgeLabel] in
badgeLabel?.layer.transform = CATransform3DMakeScale(1, 1, 1)
}
if wasNil {
badgeLabel.layer.transform = CATransform3DMakeScale(0.1, 0.1, 0.1)
}
UIView.animate(withDuration: 0.5,
delay: 0.2,
usingSpringWithDamping: 0.3,
initialSpringVelocity: 0.3,
options: UIView.AnimationOptions(),
animations: animations,
completion: nil)
}
}
}
public var badgeBackgroundColor = UIColor.red {
didSet {
badgeLabel.backgroundColor = badgeBackgroundColor
}
}
like this adding count to label
let cartQty = UserDefaults.standard.value(forKey: "cartCount")
cartBtn.badgeText = cartQty as? String
here if cartBtn.badgeText is nil then also i am showing badgeLabel but if its nil i don't want to show badgeLabel.. how to do hat.. please do help
It looks like you're not actually using the animations when wasNil is true. Also don't use NSString, it's an older API now and you can just use a easy Int initializer to go from String to Int.
Here's some updated code:
public var badgeText: String? {
didSet {
// changing this variable name for clarity.
let isTextAString: Bool = Int(badgeText ?? "") == nil
badgeLabel.isHidden = isTextAString
badgeLabel.text = badgeText
setSize()
if animated {
UIView.animate(withDuration: 0.5,
delay: 0.2,
usingSpringWithDamping: 0.3,
initialSpringVelocity: 0.3,
options: UIView.AnimationOptions()) {
self.badgeLabel?.layer.transform = CATransform3DMakeScale(1, 1, 1)
// You can just do this `if` statement inside the animation block
if isTextAString {
self.badgeLabel.layer.transform = CATransform3DMakeScale(0.1, 0.1, 0.1)
}
} completion: { (_) in }
}
}
}

How can i set the same IBAction to multiple buttons?

I'm trying to link multiple buttons with the same IBAction, to run similar but different code. The code is to set an image that was clicked on another view controller into the UIImageView under the button.
All the buttons link to the same view controller but with a different segue.
I tried to write a if statements but I didn't seem to have it right. I have named each corresponding UIImage view: technologyImageViewTwo, technologyImageViewThree ...etc
below is the code I used for the first button which works with the corresponding UIImageView named technologyImageView
#IBAction func setTechnology(segue:UIStoryboardSegue) {
dismiss(animated: true) {
if let technology = segue.identifier{
self.persona.technology = technology
self.technologyView.technologyImageView.image = UIImage(named: technology)
}
//animating scale up of image
let scaleUp = CGAffineTransform.init(scaleX: 0.1, y:0.1)
self.technologyView.technologyImageView.transform = scaleUp
self.technologyView.technologyImageView.alpha = 0
//animating bounce effect
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.7, options: [], animations: {
self.technologyView.technologyImageView.transform = .identity
self.technologyView.technologyImageView.alpha = 1
}, completion: nil)
}
I expect that each button should go to the segued view controller and the image selected will show up under the corresponding button. E.g if I click on the button 'Technology 2' and choose an image, the image shows up in the UIImageview named technologyImageViewTwo.
There are a couple of options, that you can choose from:
Option 1:
This option would be more preferred, that is to use the tag property on the component, this will allow you to identify the index of the button when it has been actioned.
https://developer.apple.com/documentation/uikit/uiview/1622493-tag
#IBAction func action(_ sender: Any) {
dismiss(animated: true) {
var imageView: UIImageView!
let index = (sender as? UIView)?.tag ?? 0
switch index {
case 1:
persona.technology = <#T##String#>
imageView = technologyView.technologyImageViewTwo
case 2:
persona.technology = <#T##String#>
imageView = technologyView.technologyImageViewThree
default:
persona.technology = <#T##String#>
imageView = technologyView.technologyImageView
}
if let technology = persona.technology {
imageView.image = UIImage(named: technology)
}
//animating scale up of image
let scaleUp = CGAffineTransform.init(scaleX: 0.1, y:0.1)
imageView.transform = scaleUp
imageView.alpha = 0
//animating bounce effect
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.7, options: [], animations: {
imageView.transform = .identity
imageView.alpha = 1
})
}
}
Option 2:
If you want to continue with the code you have then you could use a segue identifier that contains components that you can be split out and enable you to identify them more efficiently:
segueIdentifier-1, segueIdentifier-2, segueIdentifier-3
func setTechnology(segue: UIStoryboardSegue) {
dismiss(animated: true) {
var imageView: UIImageView!
let identifierComponents = segue.identifier?.components(separatedBy: "-")
let index = Int(identifierComponents?.last ?? "0")
switch index {
case 1:
imageView = technologyView.technologyImageViewTwo
case 2:
imageView = technologyView.technologyImageViewThree
default:
imageView = technologyView.technologyImageView
}
if let technology = identifierComponents?.first {
self.persona.technology = technology
imageView.image = UIImage(named: technology)
}
//animating scale up of image
let scaleUp = CGAffineTransform.init(scaleX: 0.1, y:0.1)
imageView.transform = scaleUp
imageView.alpha = 0
//animating bounce effect
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.7, options: [], animations: {
imageView.transform = .identity
imageView.alpha = 1
})
}
}
You could use three separate IBActions that all call the same function with the UIImageView number as a parameter.
func setTechnology(segue:UIStoryboardSegue, imageViewNumber: Int) {
dismiss(animated: true) {
var imageView: UIImageView!
if imageViewNumber == 0 {
imageView = self.technologyView.technologyImageView
} else if imageView == 1 {
imageView = self.technologyView.technologyImageViewTwo
} else {
imageView = self.technologyView.technologyImageViewThree
}
if let technology = segue.identifier{
self.persona.technology = technology
imageView.image = UIImage(named: technology)
}
//animating scale up of image
let scaleUp = CGAffineTransform.init(scaleX: 0.1, y:0.1)
imageView.transform = scaleUp
imageView.alpha = 0
//animating bounce effect
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.7, options: [], animations: {
imageView.transform = .identity
imageView.alpha = 1
}, completion: nil)
}
And then call this function from inside the three separate IBActions.
#IBAction func tappedButtonZero(segue:UIStoryboardSegue) {
self.setTechnology(segue: segue, imageViewNumber: 0)
}
#IBAction func tappedButtonOne(segue:UIStoryboardSegue) {
self.setTechnology(segue: segue, imageViewNumber: 1)
}
#IBAction func tappedButtonTwo(segue:UIStoryboardSegue) {
self.setTechnology(segue: segue, imageViewNumber: 2)
}
You could drag multiple buttons to your #IBAction and assign a unique tag value to each button. Then, use a switch statement to do whatever's unique to the button you pressed.
#IBAction func tappedButton(_ sender: UIButton) {
switch sender.tag {
case 1:
print("one")
case 2:
print("two")
case 3:
print("three")
default:
break
}
}

Swift, playing multiple animations consecutively from an array?

Sorry, I am new to swift. I can't get each animation to play consecutively and not all at once. I have tried using sleep(), but that then doesn't seem to allow the animation to play. This is how I find which animation to play.
for number in sequence {
switch number {
case 1:
print("blue")
animateB()
case 2:
print("green")
animateG()
case 3:
print("magenta")
animateM()
case 4:
print("orange")
animateO()
case 5:
print("yellow")
animateY()
case 6:
print("red")
animateR()
case 7:
print("purple")
animateP()
case 8:
print("cyan")
animateC()
default:
print("error")
}
}
And this is one of the functions i am using to animate. I realize this is probably very inefficient too, but wasn't sure how to make the function better.
private func animateB(){
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.toValue = 1.3
animation.duration = 0.5
animation.autoreverses = true
self.pulsatingB.add(animation, forKey: "pulsing")
}
Any help would be great thanks. :)
You can use CATransaction to chain the CAAnimations:
class ViewController: UIViewController {
// The animations, to be applied in order
var animationQueue = [() -> Void]()
#IBAction func animate(_ sender: Any) {
animationQueue.removeAll()
// Build the animation queue
for number in sequence {
switch number {
case 1:
print("blue")
animationQueue.append(animateB)
case 2:
print("green")
animationQueue.append(animateG)
// ....
default:
break
}
}
// Start the animation
applyNextAnimation()
}
func applyNextAnimation() {
guard !animationQueue.isEmpty else { return }
let animation = animationQueue.removeFirst()
// When an animation completes, call this function again to apply the next animation
CATransaction.begin()
CATransaction.setCompletionBlock({ self.applyNextAnimation() })
animation()
CATransaction.commit()
}
}
For a sequence of animations, a block based keyframe animation can often do the job, too, e.g.:
UIView.animateKeyframes(withDuration: 4.0, delay: 0, options: .repeat, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.25, animations: {
self.subview.transform = .init(scaleX: 0.5, y: 0.5)
})
UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.25, animations: {
self.subview.transform = .init(scaleX: 1.3, y: 1.3)
})
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.25, animations: {
self.subview.transform = .init(scaleX: 0.75, y: 0.75)
})
UIView.addKeyframe(withRelativeStartTime: 0.75, relativeDuration: 0.25, animations: {
self.subview.transform = .identity
})
}, completion: nil)
Or, if you have an array of functions:
let animations = [animateA, animateB, animateC, animateD]
UIView.animateKeyframes(withDuration: 4.0, delay: 0, options: .repeat, animations: {
for (index, animation) in animations.enumerated() {
UIView.addKeyframe(withRelativeStartTime: Double(index) / Double(animations.count), relativeDuration: 1 / Double(animations.count), animations: {
animation()
})
}
}, completion: nil)
Where,
func animateA() {
subview.transform = .init(scaleX: 0.5, y: 0.5)
}
func animateB() {
subview.transform = .init(scaleX: 1.3, y: 1.3)
}
...

UIButton Heartbeat Animation

I've created a heart beat animation for a UIButton. However, there is no way to stop this animation as it's an endless code loop. After tinkering with numerous UIView animation code blocks I've been unable to get UIViewAnimationOptions.Repeat to produce what I need. If I could do that I could simply button.layer.removeAllAnimations() to remove the animations. What is a way to write this that allows for removal of the animation? I'm thinking a timer possibly but that could be kind of messy with multiple animations going on.
func heartBeatAnimation(button: UIButton) {
button.userInteractionEnabled = true
button.enabled = true
func animation1() {
UIView.animateWithDuration(0.5, delay: 0.0, options: [], animations: { () -> Void in
button.transform = CGAffineTransformMakeScale(2.0, 2.0)
button.transform = CGAffineTransformIdentity
}, completion: nil)
UIView.animateWithDuration(0.5, delay: 0.5, options: [], animations: { () -> Void in
button.transform = CGAffineTransformMakeScale(2.0, 2.0)
button.transform = CGAffineTransformIdentity
}) { (Bool) -> Void in
delay(2.0, closure: { () -> () in
animation2()
})
}
}
func animation2() {
UIView.animateWithDuration(0.5, delay: 0.0, options: [], animations: { () -> Void in
button.transform = CGAffineTransformMakeScale(2.0, 2.0)
button.transform = CGAffineTransformIdentity
}, completion: nil)
UIView.animateWithDuration(0.5, delay: 0.5, options: [], animations: { () -> Void in
button.transform = CGAffineTransformMakeScale(2.0, 2.0)
button.transform = CGAffineTransformIdentity
}) { (Bool) -> Void in
delay(2.0, closure: { () -> () in
animation1()
})
}
}
animation1()
}
This works perfectly. The damping and spring need to be tweaked a little bit but this solves the problem. removeAllAnimations() clears the animation and returns the button to it's normal state.
button.userInteractionEnabled = true
button.enabled = true
let pulse1 = CASpringAnimation(keyPath: "transform.scale")
pulse1.duration = 0.6
pulse1.fromValue = 1.0
pulse1.toValue = 1.12
pulse1.autoreverses = true
pulse1.repeatCount = 1
pulse1.initialVelocity = 0.5
pulse1.damping = 0.8
let animationGroup = CAAnimationGroup()
animationGroup.duration = 2.7
animationGroup.repeatCount = 1000
animationGroup.animations = [pulse1]
button.layer.addAnimation(animationGroup, forKey: "pulse")
This post was very helpful: CAKeyframeAnimation delay before repeating
Swift 5 code, works without the pause between pulses:
let pulse = CASpringAnimation(keyPath: "transform.scale")
pulse.duration = 0.4
pulse.fromValue = 1.0
pulse.toValue = 1.12
pulse.autoreverses = true
pulse.repeatCount = .infinity
pulse.initialVelocity = 0.5
pulse.damping = 0.8
switchButton.layer.add(pulse, forKey: nil)
I created something like this:
let animation = CAKeyframeAnimation(keyPath: "transform.scale")
animation.values = [1.0, 1.2, 1.0]
animation.keyTimes = [0, 0.5, 1]
animation.duration = 1.0
animation.repeatCount = Float.infinity
layer.add(animation, forKey: "pulse")
On your original question you mentioned that you want the animation to stop on command. I assume you would like it to start on command too. This solution will do both and it is quite simple.
func cutAnim(){
for view in animating {
///I use a UIView because I wanted the container of my button to be animated. UIButton will work just fine too.
(view.value as? UIView)?.layer.removeAllAnimations()
}
}
func pulse(button: UIButton, name: String){
///Here I capture that container
let container = button.superview?.superview
///Add to Dictionary
animating[name] = container
cutAnim()
UIView.animate(withDuration: 1, delay: 0.0, options:[UIViewAnimationOptions.repeat, UIViewAnimationOptions.autoreverse, .allowUserInteraction], animations: {
container?.transform = CGAffineTransform(scaleX: 1.15, y: 1.15)
///if you stop the animation half way it completes anyways so I want the container to go back to its original size
container?.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
}, completion: nil)
}
Call cutAnim() anywhere to stop an animation, inside a timer if you want.
To start the animation use a regular button action
#IBAction func buttonWasTappedAction(_ sender: Any) {
pulse(button: sender as! UIButton, name: "nameForDictionary")
}
Hope this helps.