Checking the current view state after block/closure completion - ios

Within a asynchronously executed block/closure, I want to get a check on my current state before I executed anything within that block.
A common example of where this presents itself is segueing to the next View Controller after a NSURLsession request.
Here's an example:
#IBAction func tappedButton(sender: UIButton) {
//This closure named fetchHistorical goes to the internet and fetches an array
//The response is then sent to the next view controller along with a segue
Order.fetchHistorical(orderID, completionHandler: { (resultEnum) -> () in
switch resultEnum {
case .Success(let result):
let orderItemsArray = result.orderItems!.allObjects
self.performSegueWithIdentifier("showExchanges", sender: orderItemsArray)
default:
let _ = errorModal(title: "Error", message: "Failed!")
}
})
}
Assume that the user has been impatient and tapped this button 3 times.
That would mean this function will be called three times and each time it would attempt to segue to the next view controller (iOS nicely blocks this issue with "Warning: Attempt to present on whose view is not in the window hierarchy!")
I wanted to know how do you folks tackle this problem? Is it something like ... within the closure, check if you are still in the present viewcontroller ... if you are, then segueing is valid. If not, you probably have already segued and don't execute the segue again.
***More generally, how are you checking the current state within the closure because the closure is executed asynchronously?

Since the closure isn't executing on the main thread the state would be in accurate if you check it here (as you stated). You can use GCD to go to the main thread and check the state there. There are a couple of ways you can keep this code from running multiple times. If it will take some time to perform the calculations you can use an acitivity indicator to let the user know the app is busy at the moment. If you want the user to still have the option of pressing the button you can put a tag like:
var buttonWasTapped:Bool = false //class property
#IBAction func tappedButton(sender: UIButton) {
if !self.buttonWasTapped{
self.buttonWasTapped = true
}
}
Then change it back to false on viewDidAppear so they can press once every time that page is shown.

When starting some task that will take some time to complete I would do two things;
Show some sort of activity indicator so that the user knows something is happening
Disable the button so that there is further indication that the request has been received and to prevent the user from tapping multiple times.
It is important that you consider not only the correct operation of your app but also providing a good user experience.

Related

Will dismissing a vc before a DispatchGroup finishes crash the app?

I know that you have to balance out the calls with DispatchGroup for both .enter() and .leave().
The question is if I started a dispatchGroup.enter() and I dismissed/popped the vc before the calls were balanced, would the app crash? Basically, would deinit() change the outcome because the vc is no longer in memory?
eg. This starts, but before the 3rd background task is completed, the vc is dismissed or popped. .leave() only ran twice but it's supposed to run 3 times. This is just a simple example to get the idea across:
func viewDidLoad()
let group = DispatchGroup()
// there are 3 background tasks
for task in backgroundTasks {
group.enter()
someBackgroundTask(... completion: { (_) in
group.leave()
})
}
group.notify...
}
No, failing to call leave will not, itself, cause a crash. (It likely is not relevant here, but calling leave too many times will crash.)
What will happen, though, is that until all of the enter calls are offset by leave calls, the dispatch group will not be released. Worse, anything captured strongly by the completion handler and the notify closures will not get released. So you really do want to ensure that all enter calls are eventually offset by leave calls.
But I would first suggest confirming that leave is not getting called enough times. Typically the exact opposite problem occurs, that leave is called the correct number of times, but that one or more of these background tasks finish after the view controller was dismissed, and does not handle this scenario gracefully.
For what it is worth, if someBackgroundTask is doing something that is no longer needed after the view controller is dismissed, we would:
make someBackgroundTask a cancelable task, if possible;
make sure the various closures do not maintain a strong reference back to the view controller (by using [weak self] pattern), eliminating strong reference cycles;
make sure the closure gracefully handles if the view controller is deallocated by the time the closure runs, e.g.
group.notify(queue: .main) { [weak self] in
guard let self = self else { return }
...
}
and
in deinit of the view controller (or somewhere equivalent) cancel the tasks that are no longer needed (again, assuming it is a cancelable task).

IOS Swift 3 UILabels in View don't get rendered correctly

To start some information about my setup: I have a Storyboard with 3 Views and 3 ViewControllers, one attached to each of the Views.
I'm switching the Views from a completion handler of a DataTask (http request) with self.performSegue(withIdentifier: "bookingSuccess", sender: nil). To be able to pass some data I use the prepare function where I assign my ViewModel to the destination Controller.
After that i prepare some UI Stuff in the ViewDidLoad() function and assign Values to my Labels in the ViewWillAppear() Method.
This works fine, but when the view switches, half of the UI Elements don't show and the button text is somehow also not there. Under some circumstances, which I don't know (most time after just waiting a few seconds or reopen without closing the APP these Elements are suddenly showing up.
I tried a lot of different things and nothing works. Sometimes a label shown up at the beginning, but I haven't got any Idea why it's not displaying. My IOS Target Version is either IOS 10 or IOS 11.0.1/3
Here are two images of the screens:
Directly after switching the View
After waiting about 10 seconds
This sounds like a problem with doing UI work on the background thread. Since you are calling self.performSegue(withIdentifier: "bookingSuccess", sender: nil) from the completion handler of the DataTask and that will be on the background thread so do:
DispatchQueue.main.async {
self.performSegue(withIdentifier: "bookingSuccess", sender: nil)
}

How to stop a previous viewController's activity after being popped

I have a navigation controller that segues, using push, to a second view controller. I use:
controller.navigationController!.popToRootViewController(animated: true)
And this works. This code causes my controller to be popped off the stack and it returns to the root view. However, I can still hear my code running (e.g. music/sound effects) in the root view.
I have explored a few avenues.
One is to create a function that runs when my back buttons is pressed. Currently, it removes all the actions/pauses all the music:
func pauseAndRemoveEverything() {
for child in fgNode.children { child.removeAllActions() }
for child in bgNode.children { child.removeAllActions() }
soundEffectsPlayer?.volume = 0
musicPlayer?.volume = 0
soundEffectsPlayer?.stop()
musicPlayer?.stop()
}
The second option is to use multi-threading and dispatching little blocks of code; then cancelling them all when the user hits the back button. Currently my code is all on timed blocks, and I don't know how the dispatch will affect this, or whether it is more trouble than it is worth.
I want the actions on the previous controller to completely cease on segue, and reduce the effect it has on memory and energy use when this occurs. How do I do this?

Code only works within a DispatchQueue.main.async block, why?

I wrote this code to select all the text when a user begins editing a UITextField:
#IBAction func onEditingBegin(_ sender: Any) {
print("editing began")
let textfield = sender as! UITextField
textfield.selectAll(nil)
}
But it wouldn't work until I enclosed the textfield.selectAll(nil) line in a DispatchQueue.main.async block:
DispatchQueue.main.async {
textfield.selectAll(nil)
}
Why is that?
I also printed out the name of the thread in onEditingBegin() and this was the result:
<NSThread: 0x60800006c880>{number = 1, name = main}
So it seems that it is already being fired on the main thread, but the code is still not working unless textfield.selectAll() is called inside of the DispatchQueue.main.async block.
The real effect of your call to DispatchQueue.main.async is to add a tiny, tiny delay. This delay, in particular, is exactly long enough to allow the current runloop to finish and the next runloop to start. The action that has caused an event to be sent to you, calling onEditingBegin, is thus permitted to complete. The text field now is editing, and so we are ready for the next step, namely to select its contents.
The trick you've discovered is actually something that is surprisingly often needed in iOS programming. Cocoa is a complicated framework, and manipulations of the interface sometimes can stumble over one another's feet, as here — while the user is starting to edit in the text field, you are trying to select the text field's text. Sometimes we just need the runloop to come around one more time in order to permit the interface to "settle down" before proceeding to the next step.

Best place to make network calls

Network Call :-
static func getProfile(parameters:[String:AnyObject], onComplete:[String:AnyObject]->()) {
var requiredData:[String:AnyObject] = [:]
Alamofire.request(.GET,API.getProfile,parameters: parameters).validate().responseJSON { (response) in
if let responseData = response.result.value {
if let jsonData = responseData as? [String:AnyObject] {
requiredData["UserName"] = jsonData["UName"]
requiredData["UserEmail"] = jsonData["UEmail"]
requiredData["UserMobileNo"] = jsonData["UPhone"]
requiredData["UserAddress"] = jsonData["UAddress"]
requiredData["UserCity"] = jsonData["UCity"]
}// Inner If
} // Main if
onComplete(requiredData)
}// Alamofire Closed
}// Func closed
Network Call within required VC :-
override func viewDidLoad() {
super.viewDidLoad()
let parameters:[String:AnyObject] = [
"WebKey": API.WebKey.value.rawValue,
"UId":NSUserDefaults.standardUserDefaults().integerForKey("UserId")
]
NetworkInterface.getProfile(parameters) { (responseDictionary) in
//print("Passed Data \(responseDictionary["UserName"])")
self.userData = responseDictionary
self.updateUI()
}
}
As far as i know, VC Lifecycle is somewhat as follows :-
init(coder aDecoder:NSCoder) -> viewDidLoad -> viewWillAppear -> viewWillDisappear
However, Even after view appears it takes few seconds for user Information to be displayed in those textfields. I thought viewDidLoad is the best place to make network calls.
I understand that network calls are async so it will take time to fetch required data from network and respond. However, network call was made in viewDidLoad so by the time view will appear, it should already have required data ? Should it not ?
So can anyone explain me which is the best place to make network calls and why? I want textfields to be updated with user Info as soon as view Appears.
Requests need to be fired in the viewWillAppear:, only this method notifies you that the screen is about to be shown. If you don't want to send requests every time the screen is shown, consider caching the data once you have it.
viewDidLoad is not the best candidate. It has nothing to do with the appearance of the screen. It's called right after a view controller's view is requested for the first time, not when the screen is showing up.
For example, if the screen was destroyed (by popping from a navigation controller), you'll receive viewDidLoad when you show it again (by pushing the screen to the navigation controller). Or if the app receives a memory warning, a current view is unloaded and loaded again, which ends up sending the view controller viewDidLoad.
viewDidLoad is tricky.
If you think that viewDidLoad will save you from fetching the data from the server multiple times: sometimes it will, sometimes it won't. Anyway, it's not the right tool to optimize networking, caching is!
Since remote requests are expensive (they take time and traffic), you want to understand when are they sent. viewWillAppear: gives you understanding. And in conjunction with caching you can make it optimal.
UPDATE
In most cases, it's not a good idea to send requests from the view controller directly. I would suggest creating a separate networking layer.
I think viewDidLoad is the correct place to make the network call if it fits that screen's need. i.e. you don't have to re-request the data at some point. For example if profile data has changed since the view was loaded.
As for network requests taking time, it's possible that your view appears before the network request is done. I suggest adding some loading indicator that you hide after the request completed.
Also, keep in mind that network requests can fail so you should deal with that by retrying the request or displaying an error message.

Resources