stopping an asynchronous call once it's out in the wild in swift - ios

I have some problems with my version of this loadingOverlay singleton.
What's supposed to happen, is it comes onto the screen, with a view and a label that has the text, "Loading, please wait." or something like that. then if loading is longer than 2 seconds (i've changed it to 10 for debugging) the text changes to a random cute phrase.
first of all the animation that should change the text doesn't seem to happen. instead, the text just instantly changes.
more importantly, If, for some reason, my asynchronous call block is executed multiple times, I only want the most recent call to it to run, and I want the previous instances of it to terminate before running.
I was reading about callbacks and promises, which look promising. Is that a swifty pattern to follow?
by the way, as I'm learning swift and iOS, I've been experimenting, and I tried [unowned self] and now i'm experimenting with [weak self], but I'm not really certain which is most appropriate here.
// from http://stackoverflow.com/questions/33064908/adding-removing-a-view-overlay-in-swift/33064946#33064946
import UIKit
class LoadingOverlay{
static let sharedInstance = LoadingOverlay()
//above swifty singleton syntax from http://krakendev.io/blog/the-right-way-to-write-a-singleton
var overlayView = UIView()
var spring: CASpringAnimation!
var springAway: CASpringAnimation!
var hidden = false
private init() {} //This line prevents others from using the default () initializer for this class
func setupSpringAnimation(startY: CGFloat, finishY: CGFloat) {
overlayView.layer.position.y = startY
spring = CASpringAnimation(keyPath: "position.y")
spring.damping = 10
spring.fromValue = startY
spring.toValue = finishY
spring.duration = 1.0
spring.fillMode = kCAFillModeBackwards
}
func showOverlay() {
print("show overlay")
overlayView.alpha = 1
hidden = false
if let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate,
let window = appDelegate.window {
setupSpringAnimation(-window.frame.height / 2, finishY: window.frame.height / 2)
let overlayViewFramesize = 0.65 * min(window.frame.height, window.frame.width)
overlayView.frame = CGRectMake(0, 0, overlayViewFramesize, overlayViewFramesize)
overlayView.center = window.center
overlayView.backgroundColor = UIColor.greenColor()
overlayView.clipsToBounds = true
overlayView.layer.cornerRadius = overlayViewFramesize / 8
let label = UILabel(frame: CGRectMake(0,0,overlayViewFramesize * 0.8 , overlayViewFramesize))
label.text = " \nLoading, please wait\n "
label.tag = 12
overlayView.addSubview(label)
label.lineBreakMode = NSLineBreakMode.ByWordWrapping
label.numberOfLines = 0 //as many as needed
label.sizeToFit()
label.textAlignment = NSTextAlignment.Center
label.center = CGPointMake(overlayViewFramesize / 2, overlayViewFramesize / 2)
overlayView.bringSubviewToFront(label)
window.addSubview(overlayView)
overlayView.layer.addAnimation(spring, forKey: nil)
RunAfterDelay(10.0) {
if self.hidden == true { return }
//strongSelf boilerplate code technique from https://www.raywenderlich.com/133102/swift-style-guide-april-2016-update?utm_source=raywenderlich.com+Weekly&utm_campaign=ea47726fdd-raywenderlich_com_Weekly4_26_2016&utm_medium=email&utm_term=0_83b6edc87f-ea47726fdd-415681129
UIView.animateWithDuration(2, delay: 0, options: [UIViewAnimationOptions.CurveEaseInOut, UIViewAnimationOptions.BeginFromCurrentState, UIViewAnimationOptions.TransitionCrossDissolve], animations: { [weak self] in
guard let strongSelf = self else { return }
(strongSelf.overlayView.viewWithTag(12) as! UILabel).text = randomPhrase()
(strongSelf.overlayView.viewWithTag(12) as! UILabel).sizeToFit()
print ((strongSelf.overlayView.viewWithTag(12) as! UILabel).bounds.width)
(strongSelf.overlayView.viewWithTag(12) as! UILabel).center = CGPointMake(overlayViewFramesize / 2, overlayViewFramesize / 2)
}, completion: { (finished: Bool)in
print ("animation to change label occured")})
}
}
}
func hideOverlayView() {
hidden = true
UIView.animateWithDuration(1.0, delay: 0.0, options: [UIViewAnimationOptions.BeginFromCurrentState], animations: { [unowned self] in
//I know this is clunky... what's the right way?
(self.overlayView.viewWithTag(12) as! UILabel).text = ""
self.overlayView.alpha = 0
}) { [unowned self] _ in
//I know this is clunky. what's the right way?
for view in self.overlayView.subviews {
view.removeFromSuperview()
}
self.overlayView.removeFromSuperview()
print("overlayView after removing:", self.overlayView.description)
}
//here i have to deinitialize stuff to prepare for the next use
}
deinit {
print("Loading Overlay deinit")
}
}

What I basically wanted, was to be able to delay a block of code, and possibly cancel it before it executes. I found the answer here:
GCD and Delayed Invoking

Related

Why does UILabel update kill animation?

I have a super simple UIView animation where the origin y value fails to animate to 88 on the first try if i change the input text set in the UILabels.
The animation runs fine on the 2nd attempt. It feels like an initialization problem. Running layoutSubViews and updateConstraints is not helping. Thanks for any tips on this.
func previewDisplay(notifView: UIView, hdrView: UIView) {
populateText()
self.notifView?.frame.origin.y = 0
self.notifView?.frame.size.height = 33
self.notifView?.layoutSubviews()
self.notifView?.updateConstraints()
self.notifView = notifView
self.closeBtn.isHidden = true
self.notifBodyLabel.isHidden = true
self.closeBtn.alpha = 0
self.notifBodyLabel.alpha = 0
UIView.animate(withDuration: 1.0, animations: {
self.notifView?.frame.origin.y = 88
}, completion: nil)
}
func populateText() {
if let info = notification?.userInfo as? Dictionary<String,String> {
// Check if value present before using it
if let t = info["title"] {
self.notifTitleMessageLabel.text = t
} else {
self.notifTitleMessageLabel.text = ""
}
if let b = info["body"] {
self.notifBodyLabel.text = b
} else {
self.notifBodyLabel.text = ""
}
}
}

Infinitely rotate two variables using CAAnimation

I am calling users "Weight" from firebase database in a label (which is stored under 'profile/uid') which has two categories "kg" and "lbs". I would like the label to be able to show user's weight in kg and then fade out to lbs and then back to kg again so on and so fourth. So far i have written the following code which is constantly fading in and out the weight in lbs and i'm unsure how to get it to display the weight in kg once lbs fades out:
func weight() {
let userRf = self.firDatabaseRef.child("profile").child("\(User!.uid)").observe(FIRDataEventType.value, with: { (snapshot) in
if snapshot.exists() {
if let weight = snapshot.childSnapshot(forPath: "weight").value as? [String: AnyObject] {
if let kg = weight["kg"] as? Float, let lbs = weight["lbs"] as? Int {
let animation: CATransition = CATransition()
animation.duration = 2.0
animation.type = kCATransitionFade
animation.repeatDuration = .infinity
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
self.displayWeight.layer.add(animation, forKey: "changeTextTransition")
self.displayWeight.text = "\(lbs) lbs"
}
}
}
}
}
One way is to use a repeating UIVIew animation along with a Timer:
// set up some helpful constants
let kgsText = "\(kgs) kgs"
let lbsText = "\(lbs) lbs"
let fadeDuration = 2.0
// keep track of which unit is being displayed
var isDisplayingKgs = true
// animate the fade in and out, repeating
UIView.animate(withDuration: fadeDuration, delay: 0, options: [.autoreverse, .repeat], animations: {
label.alpha = 0
}, completion: nil)
// start the timer to switch the text after a short delay
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + fadeDuration) {
self.displayWeight.text = lbsText
isDisplayingKgs = false
Timer.scheduledTimer(withTimeInterval: fadeDuration * 2, repeats: true, block: { timer in
self.displayWeight.text = isDisplayingKgs ? lbsText : kgsText
isDisplayingKgs = !isDisplayingKgs
})
}
This is assuming that your label starts out visible (alpha = 1). The dispatch delay is to ensure that the first transition occurs when the label is faded out the first time, but before the timer kicks in.
Here is what the animation looks like in action:

UIImageView wait for animation complete

I'm trying to execute 2 different animations, after the first complete, using isAnimating.
but, I see only the first animation...
if anims[0] == 1{
startAnimation(image : #imageLiteral(resourceName: "first"))
}
if anims[1] == 2{
while myView.isAnimating {
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.05))
}
}
startAnimation(image : #imageLiteral(resourceName: "second") , time : Int)
spriteSheet returns UIImage array after cropping..
func startAnimation(image : UIImage , time : Int){
myView.animationImages = image.spriteSheet(cols: 19, rows: 1)
myView.animationDuration = 1
myView.animationRepeatCount = time
myView.startAnimating()
}
You can always chain animations like
UIView.animate(withDuration: 1, animations: {
//do your animation here
}) { (state) in
UIView.animate(withDuration: 1, animations: {
//do your second animation
})
}
If you are using CABasicAnimations then you can use beginTime and duration to chain them up :)
var totalDuration = 0
let baseicAnim1 = CABasicAnimation()
baseicAnim1.beginTime = CACurrentMediaTime()
totalDuration += 10
baseicAnim1.duration = CFTimeInterval(totalDuration)
let basicAnim2 = CABasicAnimation()
basicAnim2.beginTime = CACurrentMediaTime() + CFTimeInterval(totalDuration)
totalDuration += 10
basicAnim2.duration = CFTimeInterval(totalDuration)
EDIT
Using while loop to keep checking if animation has completed its execution or not is never a suggested approach
EDIT :
Try this,
func startAnimation(image : UIImage , time : Int,completionBlock : (()->())?){
let animationDuration = 1
myView.animationImages = image.spriteSheet(cols: 19, rows: 1)
myView.animationDuration = animationDuration
myView.animationRepeatCount = time
myView.startAnimating()
DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration, execute: {
if let block = completionBlock {
block()
}
})
}
Now your startAnimation function takes completion block as its parameter and executes the completion block after animationDuration. So you can get to know when animation ends :)
to chain simply call
self.startAnimation(image: #imageLiteral(resourceName: "first"), time: 1) {
self.startAnimation(image: #imageLiteral(resourceName: "second"), time: 1, completionBlock: nil)
}
Hope it helps

View is not adding subviews asynchronously Swift

I'm working on Swift as iOS Developer and I'm having some trouble lately adding subviews in asynchronous way, I'm using dispatch_async(dispatch_get_main_queue() but still it doesn't work. The subviews are not adding one by one and in real time but I have to wait till the call is done then the UI is updated. My goal in this is to add subviews one by one and in real time while the for loop is still going. Below is a part of my code. If somebody can help me I would really appreciate it.
for i in 0 ..< self.allChannels.count {
let param = Params().getEpgParams(String(self.allChannels[i].channelNumber))
uNetwork().httpRequestNoLoading(self.utilityClass.currentServer , parameters: param, method: uNetwork.METHOD.POST, parent: self) { (epgResponse, extra_information) -> () in
if(extra_information != uNetwork.EXTRA_INFORMATION.SUCCESS){
self.presentViewController(self.utilityClass.alert("Error", message: "An Unexpected Error Happened"), animated: true, completion: nil)
} else if(epgResponse == nil){
self.presentViewController(self.utilityClass.alert("Error", message: "Couldn't get any response from the server"), animated: true, completion: nil)
} else {
do {
var titleArray = [String]()
var start = [String]()
var end = [String]()
var progressArray = [Float]()
let jsonObject = try NSJSONSerialization.JSONObjectWithData(epgResponse, options: []) as! NSDictionary
if let response_object = jsonObject["response_object"] as? [[String: AnyObject]] {
for menu in response_object{
titleArray.append(menu["title"] as! String)
start.append(menu["programstart"] as! String)
end.append(menu["programend"] as! String)
progressArray.append(menu["progress"] as! Float)
}
if(titleArray.count > 0){
currentTitle = titleArray[0]
currentStart = start[0][start[0].startIndex.advancedBy(12)..<start[0].startIndex.advancedBy(12+4)]
currentEnd = end[0][end[0].startIndex.advancedBy(12)..<end[0].startIndex.advancedBy(12+4)]
progressValue = Float(progressArray[0] / 100)
} else {
currentTitle = "Programet e \(self.allChannels[i].title)"
currentStart = "00:00"
currentEnd = "00:00"
progressValue = 0.5
}
dispatch_async(dispatch_get_main_queue(), {
indiSub[i].removeFromSuperview()
self.contentView = UIView(frame: CGRectMake(0, ((self.scrollView.frame.height / 4) * CGFloat(i)), self.epgView.frame.width, self.scrollView.frame.height / 4))
self.contentView.layer.masksToBounds = true
self.scrollView.addSubview(self.contentView)
let button = UIButton(frame: CGRectMake(0, ((self.scrollView.frame.height / 4) * CGFloat(i)), self.epgView.frame.width, self.scrollView.frame.height / 4))
button.tag = Int(self.allChannels[i].channelNumber)
button.addTarget(self, action: #selector(LiveTvTest.playFromEpg(_:)), forControlEvents: .TouchUpInside)
self.scrollView.addSubview(button)
let currentTime = UILabel(frame: CGRectMake((self.scrollView.frame.height / 4) + 25, 0, 60, self.contentView.frame.height / 3))
if currentStart.characters.count == 4{
currentTime.text = currentStart + " |"
} else if (currentStart.characters.count == 5){
currentTime.text = currentStart + " |"
}
currentTime.font = UIFont(name: "Helvetica Neue", size: 15)
currentTime.adjustsFontSizeToFitWidth = true
currentTime.textColor = UIColor(red:0.54, green:0.13, blue:0.13, alpha:1.0)
self.contentView.addSubview(currentTime)
let nextTime = UILabel(frame: CGRectMake((self.scrollView.frame.height / 4) + 25, currentTime.frame.height, 60, self.contentView.frame.height / 3))
if nextStart.characters.count == 4{
nextTime.text = nextStart + " |"
} else if (nextStart.characters.count == 5) {
nextTime.text = nextStart + " |"
}
nextTime.font = UIFont(name: "Helvetica Neue", size: 15)
nextTime.adjustsFontSizeToFitWidth = true
nextTime.textColor = UIColor(red:0.53, green:0.53, blue:0.53, alpha:1.0)
self.contentView.addSubview(nextTime)
})
} else {
print("something went wrong with response object")
}
} catch {
self.presentViewController(self.utilityClass.alert("Error", message: "Something went wrong, please try again later"), animated: true, completion: nil)
}
}
}
}
If you can ensure that your closure (that you hand in to httpRequestNoLoading) is running in an arbitrary thread, you could use dispatch_sync your UI-updates into the main thread; this will wait until the work item as executed. But if the close already runs in the main thread, this will surely deadlock.
Btw. you should also dispatch the presentViewController calls into the main thread.
I'd guess the problem is that you're setting up a set of http calls. Those calls get executed in parallel and return via the callback. then you're making several view updates very closely together, thus, even if the code is serial, the updates happen in quick succession.
That said, the other thing to keep in mind is that osx/ios batch their ui updates. so it could be that you're putting quite a bit of work on the mainQueue at once, and aren't getting a chance to see the work happen. Why not try adding in some random delays using dispatch_after to see if that makes things pop in at different times?

Initializer for conditional binding must have Optional type, not 'UIView'

From research on StackOverflow I've learned that this error is caused by attempting to bind a type that isn't an optional however it doesn't make sense in this situation because it used guard instead. Here's my code:
func animateTransition(_ transitionContext: UIViewControllerContextTransitioning) {
// Here, we perform the animations necessary for the transition
guard let fromVC = transitionContext.viewController(forKey: UITransitionContextFromViewControllerKey) else { return }
let fromView = fromVC.view
guard let toVC = transitionContext.viewController(forKey: UITransitionContextToViewControllerKey) else { return }
let toView = toVC.view
guard let containerView = transitionContext.containerView() else { return }
if self.presenting {
containerView.addSubview(toView)
}
let animatingVC = self.presenting ? toVC : fromVC
let animatingView = animatingVC.view
let appearedFrame = transitionContext.finalFrame(for: animatingVC)
var alpha: CGFloat = 1
if self.options.contains([.AlphaChange]) {
alpha = 0;
}
let initialAlpha = self.presenting ? alpha : 1
let finalAlpha = self.presenting ? 1: alpha
var dismissedFrame = appearedFrame
let startRect = CGRect(origin: appearedFrame.origin, size: containerView.bounds.size)
let offset = self.calculateStartPointOffset(startRect, options: self.options)
if options.contains([.Dissolve]) && !self.presenting {
dismissedFrame.size = containerView.bounds.size
dismissedFrame.origin = CGPointZero
} else {
dismissedFrame = CGRect(x: offset.x, y: offset.y, width: appearedFrame.width, height: appearedFrame.height)
}
let initialFrame = self.presenting ? dismissedFrame : appearedFrame
let finalFrame = self.presenting ? appearedFrame : dismissedFrame
animatingView?.frame = initialFrame
animatingView?.alpha = initialAlpha
let dumpingValue = CGFloat(self.options.contains([.Interactive]) ? 1 : 0.8)
UIView.animate(withDuration: self.transitionDuration(transitionContext), delay: 0, usingSpringWithDamping: dumpingValue, initialSpringVelocity: 0.2, options: [UIViewAnimationOptions.allowUserInteraction, UIViewAnimationOptions.beginFromCurrentState],
animations:
{ () -> Void in
animatingView?.frame = finalFrame
animatingView?.alpha = finalAlpha
})
{ (completed) -> Void in
if !self.presenting {
fromView?.removeFromSuperview()
self.tDelegate?.didDissmisedPresentedViewController()
}
let cancelled = transitionContext.transitionWasCancelled()
transitionContext.completeTransition(!cancelled)
}
}
Xcode show an error on this line:
guard let containerView = transitionContext.containerView() else { return }
transitionContext.containerView() was changed to return a non-optional, so you can't use it to initialize a variable in a conditional binding like a guard or if let.
You should remove the guard from that line:
let containerView = transitionContext.containerView()
The container view in which a presentation occurs. It is an ancestor of both the presenting and presented view controller's views.
This containerView is being passed to the animation controller and It's return a non-optional
NOTE:
if let/if var optional binding only works when the result of the right side of the expression is an optional. If the result of the right side is not an optional, you can not use this optional binding. The point of this optional binding is to check for nil and only use the variable if it's non-nil.

Resources