How do I stop background call while going to another viewcontroller? - ios

I don't know how to stop this call without using the timer concept. Today, when trying to profile the project I came across this allocation memory get increasing every time because of following function:
func startAnimation(index: Int) {
UIView.animate(withDuration: 4.0, delay: 0.5, options:[UIViewAnimationOptions.allowUserInteraction, UIViewAnimationOptions.curveEaseInOut], animations: {
self.view.backgroundColor = self.colors[index]
}) { (finished) in
var currentIndex = index + 1
if currentIndex == self.colors.count { currentIndex = 0 }
self.startAnimation(index: currentIndex)
}
}

Do one simple thing, mention one flag,
Initially it should,
var isViewDissappear = false
Then,
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
isViewDissappear = true
}
Then check that flag while giving call again,
func startAnimation(index: Int) {
UIView.animate(withDuration: 4.0, delay: 0.5, options:[UIViewAnimationOptions.allowUserInteraction, UIViewAnimationOptions.curveEaseInOut], animations: {
self.view.backgroundColor = self.colors[index]
}) { (finished) in
var currentIndex = index + 1
if currentIndex == self.colors.count { currentIndex = 0 }
if !self.isViewDissappear {
self.startAnimation(index: currentIndex)
}
}
}
that's it.

You are making a retain cycle by capturing strong reference to self to the animation closure. Which means that your self is owned by the closure, and closure owned by self - this causes a leak mentioned by you (increase of memory usage). You can break the cycle by capturing weak reference to self by using capture list (read the docs). Try this:
func startAnimation(index: Int) {
UIView.animate(withDuration: 4.0, delay: 0.5, options:[UIViewAnimationOptions.allowUserInteraction, UIViewAnimationOptions.curveEaseInOut], animations: { [weak self]
self?.view.backgroundColor = self?.colors[index]
}) { (finished) in
var currentIndex = index + 1
if currentIndex == self?.colors.count { currentIndex = 0 }
self?.startAnimation(index: currentIndex)
}
}

Related

Alpha is not working on labels and buttons in Swift

I have I'm trying to using fade in and fade out transition when any controller present and dismiss. For this, when new controller present I set alpha value of all the component/asset of the view like label, buttons etc to Zero in viewDidLoad and view didAppear I set back the alpha below to 1.
Below is the code I'm using:
override func viewDidLoad() {
super.viewDidLoad()
imageView1.isHidden = true
imageView2.isHidden = true
self.setupScreen()
setAssetsAlphaZero()
}
private func setupScreen() {
switch loginVM.loginScreenType {
case .resetPassword:
presentResetPasswordScreen()
default: break
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
animateAssetsWithIdentity()
}
func animateAssetsWithIdentity() {
self.view.layoutIfNeeded()
self.view.setNeedsDisplay()
hideShowAssets(hidden: false)
UIView.animate(withDuration: 0.5, animations: { [weak self] in
self?.passwordTextField.alpha = 1
self?.forgotPasswordButton.alpha = 1
self?.emailTextField.alpha = 1
self?.loginButton.alpha = 1
self?.loginLbl.alpha = 1
self?.emailImg.alpha = 1
self?.emailLbl.alpha = 1
self?.passwordImg.alpha = 1
self?.passwordLbl.alpha = 1
self?.signUpBtn.alpha = 1
self?.view.layoutIfNeeded()
self?.view.setNeedsDisplay()
}) {[weak self] (complete) in
self?.signUpBtn.isEnabled = true
self?.view.layoutIfNeeded()
self?.view.setNeedsDisplay()
}
self.enableDisableButton()
}
func setAssetsAlphaZero() {
self.view.layoutIfNeeded()
self.view.setNeedsDisplay()
UIView.animate(withDuration: 0.5, animations: {[weak self] in
self?.passwordTextField.alpha = 0
self?.forgotPasswordButton.alpha = 0
self?.emailTextField.alpha = 0
self?.loginButton.alpha = 0
self?.loginLbl.alpha = 0
self?.emailImg.alpha = 0
self?.emailLbl.alpha = 0
self?.passwordImg.alpha = 0
self?.passwordLbl.alpha = 0
self?.signUpBtn.alpha = 0
self?.view.layoutIfNeeded()
self?.view.setNeedsDisplay()
}) {[weak self] (complete) in
self?.hideShowAssets(hidden: true)
self?.signUpBtn.isEnabled = false
self?.view.layoutIfNeeded()
self?.view.setNeedsDisplay()
}
}
func hideShowAssets(hidden : Bool ) {
self.passwordTextField.isHidden = hidden
self.forgotPasswordButton.isHidden = hidden
self.emailTextField.isHidden = hidden
self.loginButton.isHidden = hidden
self.loginLbl.isHidden = hidden
self.emailImg.isHidden = hidden
self.emailLbl.isHidden = hidden
self.passwordImg.isHidden = hidden
self.passwordLbl.isHidden = hidden
self.signUpBtn.isHidden = hidden
}
And when the controller dismiss than with help of delegate i notify the previous controller.
#IBAction func signUpButtonTapped(_ sender: UIButton) {
setAssetsAlphaZero()
self.delegate?.dissmissVC()
delay(0.5) {[weak self] in
self?.dismiss(animated: false, completion: nil)
}
}
To present the controller I used modalPresentationStyle = .overFullScreen because I need the present we controller should be transparent.
I don't think you need to call setNeedsDisplay. Because it will react to function drawRect as mention here. Also some suggestions, i think it's better to call setAssetsAlphaZero() then add delay 0.5 sec then call animateAssetsWithIdentity() . Hopefully this is helpful
When do I need to call setNeedsDisplay in iOS?

UIViewPropertyAnimator not respecting duration and delay parameters

I have a very strange issue in my app. I'm using UIViewPropertyAnimator to animate changing images inside UIImageView.
Sounds like trivial task but for some reaso my view ends up changing the image instantly so I end up with images flashing at light speed instead of given duration and delay parameters.
Here's the code:
private lazy var images: [UIImage?] = [
UIImage(named: "widgethint"),
UIImage(named: "shortcuthint"),
UIImage(named: "spotlighthint")
]
private var imageIndex = 0
private var animator: UIViewPropertyAnimator?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
animator = repeatingAnimator()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
animator?.stopAnimation(true)
}
private func repeatingAnimator() -> UIViewPropertyAnimator {
return .runningPropertyAnimator(withDuration: 2, delay: 6, options: [], animations: {
self.imageView.image = self.images[self.imageIndex]
}, completion: { pos in
if self.imageIndex >= self.images.count - 1 {
self.imageIndex = 0
} else {
self.imageIndex += 1
}
self.animator = self.repeatingAnimator()
})
}
So, according to the code animation should take 2 seconds to complete and start after 6 seconds delay, but it starts immediately and takes milliseconds to complete so I end up with horrible slideshow. Why could it be happening?
I also tried using the UIViewPropertyAnimator(duration: 2, curve: .linear) and calling repeatingAnimator().startAnimation(afterDelay: 6) but the result is the same.
Okay, I've figured it out, but it's still a bit annoying that such simple task cannot be done using the "Modern" animation API.
So, animating images inside UIImageView is apparently not supported by UIViewPropertyAnimator, during debugging I tried animating view's background color and it was working as expected. So I had to use Timer instead and old UIView.transition(with:) method
Here's the working code:
private let animationDelay: TimeInterval = 2.4
private var animationTimer: Timer!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setAnimation(true)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
setAnimation(false)
}
private func setAnimation(_ enabled: Bool) {
guard enabled else {
animationTimer?.invalidate()
animationTimer = nil
return
}
animationTimer = Timer.scheduledTimer(withTimeInterval: animationDelay, repeats: true) { _ in
UIView.transition(with: self.imageView, duration: 0.5, options: [.curveLinear, .transitionCrossDissolve], animations: {
self.imageView.image = self.images[self.imageIndex]
}, completion: { succ in
guard succ else {
return
}
if self.imageIndex >= self.images.count - 1 {
self.imageIndex = 0
} else {
self.imageIndex += 1
}
})
}
}
Hopefully it will save someone some headaches in the future

Animating UIView isHidden subviews

I have a UIView EmptyCollectionView, which I display when my UICollectionView is empty. The way I have this working is that I create the UIView and addSubview in viewDidLoad of my ViewController, then change toggle isHidden property of the view (as well as the collectionview) as needed.
I'd like to polish things up a little now I have the core function working, and I wan't to add some subtle animation to the subviews contained in my empty view, such as making the contained imageview bounce on display.
So my question is, what is the best way to detect when the UIView is being shown (i.e. is there a viewDidAppear type callback I could use)?
Supplementary question: I'm new to this... Is adding the empty view and toggling the isHidden property a good way of doing this? Or should I be doing it a different way? (i.e. should I instead be creating and destroying the view as needed, rather than keeping it around)
Thanks
The best way in my opinion is to extend UIView
extension UIView {
func fadeIn(_ duration: TimeInterval = 0.2, onCompletion: (() -> Void)? = nil) {
self.alpha = 0
self.isHidden = false
UIView.animate(withDuration: duration,
animations: { self.alpha = 1 },
completion: { (value: Bool) in
if let complete = onCompletion { complete() }
}
)
}
func fadeOut(_ duration: TimeInterval = 0.2, onCompletion: (() -> Void)? = nil) {
UIView.animate(withDuration: duration,
animations: { self.alpha = 0 },
completion: { (value: Bool) in
self.isHidden = true
if let complete = onCompletion { complete() }
}
)
}
}
So you just have to call view.fadeIn() for a default 0.2 sec animation, or view.fadeIn(1) to make it last one second.
You can even add a completion event:
view.fadeOut(0.5, onCompletion: {
print("Animation completed, do whatever you want")
})
This works, I hope it can help you. To hide view:
UIView.animate(withDuration: 0.3/*Animation Duration second*/, animations: {
self.EmptyCollectionView.alpha = 0
}, completion: {
(value: Bool) in
self.EmptyCollectionView.isHidden = true
})
To show view:
self.EmptyCollectionView.isHidden = false
UIView.animate(withDuration: 0.3, animations: {
self.EmptyCollectionView.alpha = 1
}, completion: nil)
Swift 4.2 extension to allow animation when setting isHidden on any UIView class:
extension UIView {
func setIsHidden(_ hidden: Bool, animated: Bool) {
if animated {
if self.isHidden && !hidden {
self.alpha = 0.0
self.isHidden = false
}
UIView.animate(withDuration: 0.25, animations: {
self.alpha = hidden ? 0.0 : 1.0
}) { (complete) in
self.isHidden = hidden
}
} else {
self.isHidden = hidden
}
}
}
You can animate the alpha property of EmptyCollectionView to either 0 to hide or 1 to show
UIView.animate(withDuration: 0.5) {
self.EmptyCollectionView.alpha = 0
}
Also make sure that isOpaque property is set to False to enable Transparency of the view
Swift 5
import UIKit
extension UIView {
func animateSetHidden(_ hidden: Bool, duration: CGFloat = CATransaction.animationDuration(), completion: #escaping (Bool)->() = { _ in}) {
if duration > 0 {
if self.isHidden && !hidden {
self.alpha = 0
self.isHidden = false
}
UIView.animate(withDuration: duration, delay:0, options: .beginFromCurrentState) {
self.alpha = hidden ? 0 : 1
} completion: { c in
if c {
self.isHidden = hidden
}
completion(c)
}
} else {
self.isHidden = hidden
self.alpha = hidden ? 0 : 1
completion(true)
}
}
}

How would i go about fading in and out from an array of different words or sentences?

So i built a fade in fade out for a label animation in Swift already. But I want to fade in and out from an array of different sentences, so each time fade in and fade out, it would be different staff.
override func viewDidLoad()
{
super.viewDidLoad()
label.alpha = 0
animatedText()
}
func animatedText(){
UIView.animateWithDuration(2.0, animations: {
self.label.alpha = 1.0
}, completion: {
(Completed: Bool) -> Void in
UIView.animateWithDuration(1.0, delay: 2.0, options: UIViewAnimationOptions.CurveLinear, animations: {
self.label.alpha = 0
}, completion: {
(Completed: Bool) -> Void in
self.animatedText()
})
})
}
Try this function:
class ViewController: UIViewController {
#IBOutlet weak var label1: UILabel!
#IBOutlet weak var label2: UILabel!
var sentences = ["The cat sat.", "The rhino ate.", "The monkey laughed", "The zebra ran.", "The fish swam", "The potato grew."]
override func viewDidLoad() {
super.viewDidLoad()
label1.text = sentences[0]
label2.text = sentences[1]
label1.alpha = 1.0
label2.alpha = 0.0
}
var counter = 1
#IBAction func animateBtnPressed(_ sender: UIButton) {
fadeText(counter: &counter, duration: 2.0)
}
func fadeText(counter: inout Int, duration: TimeInterval) {
if counter < sentences.count {
if counter % 2 != 0 {
UIView.animate(withDuration: duration, animations: {
self.label1.alpha = 0.0
self.label2.alpha = 1.0
}, completion: { finished in
if finished {
counter += 1
if counter <= (self.sentences.count - 1) {
self.label1.text = self.sentences[counter]
} else {
return
}
self.fadeText(counter: &counter, duration: duration)
}
})
} else {
UIView.animate(withDuration: duration, animations: {
self.label1.alpha = 1.0
self.label2.alpha = 0.0
}, completion: { finished in
if finished {
counter += 1
if counter <= (self.sentences.count - 1) {
self.label2.text = self.sentences[counter]
} else {
return
}
self.fadeText(counter: &counter, duration: duration)
}
})
}
}
}
}
Assumptions:
There is an array named sentences (you can alter this) on the ViewController that contains Strings.
There is a countervariable that starts at 1
The UILabels are overlapping in the storyboard.
Other than that you just have to call the function with the counter variable with an & operator in front, because for fadeText to alter the counter variable it needs the address, in memory, of the counter variable, not the value (notice the inout in the parameters of fadeText).
If you have any questions feel free to ask!
I would advise creating an int variable outside the scope of the animatedText() function and simply adding 1 to this variable each time your completion block is called. You can then update the label's text to the string in the array using this int variable as the array's index each time the function is called.

wait until scrollToRowAtIndexPath is done in swift

I have a UITableView with more cells than fit on screen. When I get a notification from my data model I want to jump to a specific row and show a very basic animation.
My code is:
func animateBackgroundColor(indexPath: NSIndexPath) {
dispatch_async(dispatch_get_main_queue()) {
NSLog("table should be at the right position")
if let cell = self.tableView.cellForRowAtIndexPath(indexPath) as? BasicCardCell {
var actColor = cell.backgroundColor
self.manager.vibrate()
UIView.animateWithDuration(0.2, animations: { cell.backgroundColor = UIColor.redColor() }, completion: {
_ in
UIView.animateWithDuration(0.2, animations: { cell.backgroundColor = actColor }, completion: { _ in
self.readNotificationCount--
if self.readNotificationCount >= 0 {
var legicCard = self.legicCards[indexPath.section]
legicCard.wasRead = false
self.reloadTableViewData()
} else {
self.animateBackgroundColor(indexPath)
}
})
})
}
}
}
func cardWasRead(notification: NSNotification) {
readNotificationCount++
NSLog("\(readNotificationCount)")
if let userInfo = notification.userInfo as? [String : AnyObject], let index = userInfo["Index"] as? Int {
dispatch_sync(dispatch_get_main_queue()){
self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: index), atScrollPosition: .None, animated: true)
self.tableView.layoutIfNeeded()
NSLog("table should scroll to selected row")
}
self.animateBackgroundColor(NSIndexPath(forRow: 0, inSection: index))
}
}
I hoped that the dispatch_sync part would delay the execution of my animateBackgroundColor method until the scrolling is done. Unfortunately that is not the case so that animateBackgroundColor gets called when the row is not visible yet -> cellForRowAtIndexPath returns nil and my animation won't happen. If no scrolling is needed the animation works without problem.
Can anyone tell my how to delay the execution of my animateBackgroundColor function until the scrolling is done?
Thank you very much and kind regards
Delaying animation does not seem to be a good solution for this since scrollToRowAtIndexPath animation duration is set based on distance from current list item to specified item. To solve this you need to execute animateBackgroudColor after scrollToRowAtIndexPath animation is completed by implementing scrollViewDidEndScrollingAnimation UITableViewDelegate method. The tricky part here is to get indexPath at which tableview did scroll. A possible workaround:
var indexPath:NSIndexpath?
func cardWasRead(notification: NSNotification) {
readNotificationCount++
NSLog("\(readNotificationCount)")
if let userInfo = notification.userInfo as? [String : AnyObject], let index = userInfo["Index"] as? Int{
dispatch_sync(dispatch_get_main_queue()){
self.indexPath = NSIndexPath(forRow: 0, inSection: index)
self.tableView.scrollToRowAtIndexPath(self.indexPath, atScrollPosition: .None, animated: true)
self.tableView.layoutIfNeeded()
NSLog("table should scroll to selected row")
}
}
}
func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
self.animateBackgroundColor(self.indexPath)
indexPath = nil
}
Here are my solution
1) Create a .swift file, and copy code below into it:
typealias SwagScrollCallback = (_ finish: Bool) -> Void
class UICollectionViewBase: NSObject, UICollectionViewDelegate {
static var shared = UICollectionViewBase()
var tempDelegate: UIScrollViewDelegate?
var callback: SwagScrollCallback?
func startCheckScrollAnimation(scroll: UIScrollView, callback: SwagScrollCallback?){
if let dele = scroll.delegate {
self.tempDelegate = dele
}
self.callback = callback
scroll.delegate = self
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
callback?(true)
if let dele = self.tempDelegate {
scrollView.delegate = dele
tempDelegate = nil
}
}
}
extension UICollectionView {
func scrollToItem(at indexPath: IndexPath, at scrollPosition: UICollectionView.ScrollPosition, _ callback: SwagScrollCallback?){
UICollectionViewBase.shared.startCheckScrollAnimation(scroll: self, callback: callback)
self.scrollToItem(at: indexPath, at: scrollPosition, animated: true)
}
}
2) Example:
#IBAction func onBtnShow(){
let index = IndexPath.init(item: 58, section: 0)
self.clv.scrollToItem(at: index, at: .centeredVertically) { [weak self] (finish) in
guard let `self` = self else { return }
// Change color temporarily
if let cell = self.clv.cellForItem(at: index) as? MyCell {
cell.backgroundColor = .yellow
cell.lbl.textColor = .red
}
// Reset
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.clv.reloadData()
}
}
}
3) My github code example: github here
I has similar problem. I just do this.
UIView.animateWithDuration(0.2,delay:0.0,options: nil,animations:{
self.tableView.scrollToRowAtIndexPath(self.indexPath!, atScrollPosition: .Middle, animated: true)},
completion: { finished in UIView.animateWithDuration(0.5, animations:{
self.animateBackgroundColor(self.indexPath)})})}

Resources