I am working in Swift. When a user presses a UIButton it calls a function ButtonPressed(). I would like ButtonPressed() to do two things:
Update the UIView by removing the current buttons and texts, then uploading some new text.
Call function TimeConsumingCalculation(). TimeConsumingCalculation is the complicated part of my app and does some calculations which take about 20 seconds or so to complete.
Right now, I have the code in the basic order:
ButtonPressed(){
self.Button.removeFromSuperview()
TimeConsumingCalculation()
}
However, it will not remove the button or do any other UI updates or additions until after the TimeConsumingCalculation is complete. I have read and attempted a few guides on closures and asynchronous functions, but have had no luck. Is there a special property with UIView that is causing it to be updated last?
As a side note - I have already attempted putting all UI actions in a separate function and calling it first. It doesn't work. The time consuming function does not take any variables from the buttons or UI or anything like that.
Thanks!
It seems like timeConsumingCalculation() is blocking the main queue, which is in charge of UI updates. Try calling it like this instead and use the isHidden property to hide the button instead of removing it from the view completely.
ButtonPressed(){
self.Button.isHidden = true
DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async {
self.timeConsumingCalculation()
}
}
here you call timeConsumingCalculation() asynchronously on a background thread. The quality of service we give it is userInitiated, read more about quality of service classes here
Related
I ran into a problem with my UI, which is not updating immediately.
I am calling someCustomView.isHidden = false first. After that I create a new instance of a new View Controller. Inside the new VCs viewDidLoad(), I am loading a "new Machine Learning Model", which takes some time.
private func someFuncThatGetsCalled() {
print("1")
self.viewLoading.isHidden = false
print("2")
performSegue(withIdentifier: "goToModelVCSegue", sender: nil)
}
As soon as I press the button that calls this function, "1" and "2" is printed in the console. However the view is not getting visible before the viewDidLoad() of my new VC is finished.
Is there any possibility to force update a UIView immediately? setNeedsDisplay() did not work for me.
Thanks for your help!
Use layoutIfNeeded() Apple Docs
layoutIfNeeded()
Lays out the subviews immediately, if layout updates are pending.
Use this method to force the view to update its layout immediately. When using Auto Layout, the layout engine updates the position of views as needed to satisfy changes in constraints. Using the view that receives the message as the root view, this method lays out the view subtree starting at the root. If no layout updates are pending, this method exits without modifying the layout or calling any layout-related callbacks.
So As a rule of thumb,
layoutifneeded : Immediate (current update cycle) , synchronous call
setNeedsLayout :relaxed ( wait till Next Update cycle) , asynchronous call
So, layoutIfNeeded says update immediately please, whereas setNeedsLayout says please update but you can wait until the next update cycle.
how to use
yourView.layoutIfNeeded()
You can also refer to the diagram to better remember the order of these passes
Source Apple docs on layoutIfNeeded
Image credit Medium artcle
A couple problems...
If you have a view controller that "takes some time" to load, you should not try to do it in that manner.
The app will be non-responsive and appear "frozen."
A much better approach would be:
on someFuncThatGetsCalled()
hide viewLoading and replace it with an activity indicator (spinner, or something else that let's the user know the app is not stuck)
instantiate your ModelVC
when ModelVC has finished its setup, have it inform the current VC (via delegate)
current VC then shows / navigates to the already instantiated and prepared ModelVC
Or, probably a better option... Move your time-consuming setup in ModelVC to a point after the view has appeared. You can show an activity indicator in viewDidLoad(). That is really the most common UX - you see it all the time when the new VC has to retrieve remote data to display - and it would fit wit what users have come to expect.
Using Swift , I animate multiple images with :
cp.animationImages = images
cp.animationDuration = TimeInterval(speed)
cp.animationRepeatCount=count
cp.startAnimating()
I use all sorts of delays to wait between them, but when I need to run a sequence of animations, I would like to have a delegate so each call will be finished with a certain tag, so I can then decide what to do.
I could not find how to use a block/delegate in Swift for this animation.
UIImageView does not support animation completion handlers, but there are extensions available:
https://github.com/gurmundi7/UIImageView-AnimationCompletionBlock
How do I get the UIActivityIndicatorView to display first, then execute other code?
I've experimented with using sleep, and it works but it doesn't "feel" right and adds an extra second to processing a bunch of core data stuff. I've also tried dispatching it to the main thread which only works some of the time. (I'm guessing when the rest of the block is executed outside of the main thread).
Ideally as soon as a user touches the button the instance of the UIActivityIndicatorView would display (which seems to happen where I've used it in other apps by itself or with other minimal processing).
Details: I have an IBAction connected to a button that executes a bunch of core data stuff, sometimes including images, that takes between 1 - 3 seconds to finish. When it finishes it dismisses the view controller. The view controller where this is executed is presented as a modal over current context.
Here is a code snippet:
// get the background queue
let bg_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_async(bg_queue, {
// long running code here...
dispatch_async(dispatch_get_main_queue(), {
self.activityIndicator.stopAnimating()
})
})
I have a seemingly simple problem that I cannot for the life of me seem to figure out. In my iOS App, I have a UICollectionView that triggers network operation upon tapping it that can take a few seconds to complete. While the information is being downloaded, I want to display a UIView that fills the cell with a UIActivityIndicatorView that sits in the square until the loading is done, and the segue triggered. The problem is that it never appears. Right now my code looks like:
myLoadView.hidden = NO;
//Network Operation
myLoadView.hidden = YES;
The App simply stops for a couple seconds, and then moves on the the next view. I'd imagine Grand Central Dispatch has somthing to do with the solution, however please keep in mind that this code takes place in prepareForSegue, and the network info needs to be passed to the next View. For this reason not finishing the download before switching scenes has an obvious problem. Any help would be VASTLY appreciated. Thanks!
iOS commits changes in the interfaces after working out a routine. Hence you should perform your network operation in a background thread and then get back back on the main and perform the "show my view now thing". Have a look the below code for reference.
myLoadView.hidden = NO;
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
//Network Operation
dispatch_async(dispatch_get_main_queue(), ^{
myLoadView.hidden = YES;
});
});
Your network operation seems to be carried out on the main thread, aka UI thread. This blocks all further UI calls, including the call to unhide a view, until completion.
To resolve this, make your call asynchronous.
You should read this in full, if you haven't already.
As mentioned by other answers, the problem is that the UIView change doesn't happen until the current method finishes running, which is where you are blocking. Before GCD was available I would split methods in two and use performSelector:withObject:afterDelay (to run the second part also on the UI loop) or performSelectorInBackground:withObject: at the end of the first method. This would commit all the waiting animaations first, then do the actual tasks in the second method.
Well the better option for this type of indication is by using the custom HUD libraries like SVProgressHUD or MBProgressHUD
I am trying to update a UIProgressView progress bar that I have in a UIView during a long resource loading process. Let's say I'm loading a bunch of bitmaps from a NIB file (like maybe a hundred). After I load 10, I issue a .progress to the UIProgressView that is part of a UIView that is already being displayed. So, I issue:
myView.myProgressView.progress=0.2;
Then, I load another 10 bitmaps, and issue:
myView.myProgressView.progress=0.4;
etc., etc. When the app runs, the progress bar doesn't advance. It simply stays at its initial position. At the risk of sounding like a complete moron, do I have to load my resources on a separate thread so the OS can update the UI, or, is there an easier way? Thanks for any assistance.
Yes. Load them on a separate thread. Or just use something like performSelector:
[self performSelector:#selector(setProgressBar) withObject:nil afterDelay:0.0];
(and create a setProgressBar function which reads the current value from a member variable and updates the UI)
You could run a step of the runloop after each update of the UI:
SInt32 result;
do {
result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, TRUE);
} while(result == kCFRunLoopRunHandledSource);
It could have other bad consequences (for example, enable user to interact with the UI, execute delegates of view controller such as viewDidAppear before they should be executed, etc) so be very, very careful.