How to load view after Alamofire finished its job? - ios

My Issue:
I am trying to load data from Server, through Alamofire, before SubViewController Load its view. After writing the code, I failed to solve the problem of Async Feature of Alamofire. The view is always be loaded in the SubViewController before Alamofire finished its job.
Part Of My Code:
ParentViewController:
Leading the way to SubViewController through PrepareForSegue().
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "CellDetailSegue" {
if let indexPaths = self.dateCollectionView.indexPathsForSelectedItems() {
let subViewController = segue.destinationViewController as! SubViewConroller
}
}
SubViewController:
Test whether the data has been loaded by print() in the its viewDidLoad() and load the data by dataRequest() in viewWillAppear()
class SubViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var availablePeriods401 = [String]()
var availablePeriods403 = [String]()
var availablePeriods405 = [String]()
override func viewDidLoad() {
super.viewDidLoad()
self.dataRequest(self.availablePeriods401)
self.dataRequest(self.availablePeriods403)
self.dataRequest(self.availablePeriods405)
print(self.availablePeriods401.count)
print(self.availablePeriods403.count)
print(self.availablePeriods405.count)
}
func dataRequest(_ target: [String]) {
Alamofire.request(.POST, "http://httpbin.org/get", parameters: ["foo": "bar"]).responseJSON {
.
.
.
target = Result
}
}
}
Problem Description:
Three variables in the SubViewController can not be assigned the valid values after view was loaded.
Three Outputs' results are all 0.
But I can get valid count if I set print() in the dataRequest().
My Question:
How to make sure that Alamofire finishes its job?
Where Shall I put the Alamofire Request Function? viewWillApper()? viewDidApper()?
Should I even finished requesting job in ParentViewController's PrepareForSegue() ?
Please teach me how to solve this problem.
A big appreciation for your guide and time.
Ethan Joe

You should call Alamofire Request Function in viewDidLoad function. and you should reload table data when you got response from completion block(from where you print the data).
You can reload tableview like,
self.tableView.reloadData()
hope this will help :)

The first thing I noticed is that you are doing 3 asynchronous requests, not one. You could use a completion handler but which one? I think you have 2 options.
Nest the network calls so that the completion of one starts the next one. The downside to this approach is that they will run sequentially and if you add more, you have to continue nesting. An approach like this might be OK if you are only doing 2 calls but beyond that it will get more and more difficult.
Use a semaphore to wait until all the data is loaded from all the remote calls. Use the completion handler to signal the semaphore. If you are going to use this approach, then it must be done on a background thread because use of a semaphore will block the thread and you don't want that happening on the main thread.
These three calls will all happen simultaneously. And the functions will return even though AlamoFire has not completed.
self.dataRequest(self.availablePeriods401)
self.dataRequest(self.availablePeriods403)
self.dataRequest(self.availablePeriods405)
These will execute, whether AlamoFire has completed or not.
print(self.availablePeriods401.count)
print(self.availablePeriods403.count)
print(self.availablePeriods405.count)
Using semaphores would look something like this:
override func viewWillAppear(animated: Bool) {
// maybe show a "Please Wait" dialog?
loadMyData() {
(success) in
// hide the "Please Wait" dialog.
// populate data on screen
}
}
func loadMyData(completion: MyCompletionHandler) {
// Do this in an operation queue so that we are not
// blocking the main thread.
let queue = NSOperationQueue()
queue.addOperationWithBlock {
let semaphore = dispatch_semaphore_create(0)
Alamofire.request(.POST, "http://httpbin.org/get", parameters: ["foo": "bar1"]).responseJSON {
// This block fires after the results come back
// do something
dispatch_semaphore_signal(semaphore);
}
Alamofire.request(.POST, "http://httpbin.org/get", parameters: ["foo": "bar2"]).responseJSON {
// This block fires after the results come back
// do something
dispatch_semaphore_signal(semaphore);
}
Alamofire.request(.POST, "http://httpbin.org/get", parameters: ["foo": "bar3"]).responseJSON {
// This block fires after the results come back
// do something
dispatch_semaphore_signal(semaphore);
}
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
completion(true)
}
}
Apple Docs - Grand Central Dispatch
How to use semaphores
The question I have for you is what are you going to do if some, bit not all of the web calls fail?

First create a global bool variable with false
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "CellDetailSegue" && boolVar {
if let indexPaths = self.dateCollectionView.indexPathsForSelectedItems() {
let subViewController = segue.destinationViewController as! SubViewConroller
}
}
call prepare segue with segue name and boolVar true from almofire block.

Related

Swift control flow

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
submitTapped()
if let scheduleController = segue.destination as? ScheduleController {
scheduleController.jsonObject = self.info
}
}
In submitTapped(), self.info is assigned a value. But when I run my app, self.info is reported as "nil". I tried setting breakpoints at each of the three lines, and it seems that submitTapped() doesn't execute until after this function is finished.
Why is this? Does it have to deal with threads? How can I get submitTapped() to execute before the rest? I'm just trying to move from one view controller to another while also sending self.info to the next view controller.
UPDATE:
I ended up figuring it out (for the most part) thanks to the answer below + my own testing.
#IBAction func submitTapped() {
update() { success in
if success {
DispatchQueue.main.async {
self.performSegue(withIdentifier: "showScheduler", sender: nil)
}
}
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// I'll probably check the segue identifier here once I have more "actions" implemented
let destinationVC = segue.destination as! ScheduleController
destinationVC.jsonObject = self.info
}
public func update(finished: #escaping (Bool) -> Void) {
...
self.info = jsonObject //get the data I need
finished(true)
...
}
The network request is an asynchronous task that occurs in the background and takes some time to complete. Your prepareForSegue method call will finish before the data comes back from the network.
You should look at using a completionHandler and also only triggering the segue once you have the data.
so your submitTapped function (probably best to rename this to update or something) will make the network request and then when it gets the data back will set the self.info property and then call performSegueWithIdentifier.
func update(completion: (Bool) -> Void) {
// setup your network request.
// perform network request, then you'll likely parse some JSON
// once you get the response and parsed the data call completion
completion(true)
}
update() { success in
// this will run when the network response is received and parsed.
if success {
self.performSegueWithIdentifier("showSchedular")
}
}
UPDATE:
Closures, Completion handlers an asynchronous tasks can be very difficult to understand at first. I would highly recommend looking at this free course which is where I learnt how to do it in Swift but it takes some time.
This video tutorial may teach you basics quicker.

viewDidLoad() is running before Alamofire request in prepareforsegue has a chance to finish

I have two viewControllers that are giving me trouble. One is called notificationAccessViewController.swift and the other is called classChooseViewController.swift. I have a button in the notificationAccessViewController that triggers a segue to the classChooseViewController. What I need to do is within the prepareForSegue function, perform an Alamofire request and then pass the response to the classChooseViewController. That works! BUT, not fast enough.
Below is my prepareForSegue function within the notificationAccessViewController
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let DestViewController = segue.destination as! classChooseViewController
Alamofire.request("MYURL.com").responseJSON { (response) in
if let JSON : NSDictionary = response.result.value as! NSDictionary?{
let classNameArray = JSON["className"] as! NSArray
print("---")
print(classNameArray)
DestViewController.classNameArray = classNameArray
}
}
}
I have it printing out classNameArray to the console, which it does SUCCESSFULLY. Although, in my classChooseViewController.swift, I also print out the classNameArray to the console and it prints with nothing inside of it. This is because the viewDidLoad() function of the classChooseViewController.swift is running before the prepareForSegue function has finished. I want to know what I can do to ensure that the classChooseViewController does not load until the prepareForSegue function has FINISHED running.
Below is my code for classChooseViewController
class classChooseViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var classNameArray: NSArray = []
override func viewDidLoad() {
super.viewDidLoad()
print(classNameArray)
}
}
I have attached a picture of what the console reads. You can see that the classNameArray prints from the classChooseViewControllers viewDidLoad() function BEFORE the prepareForSegue() function because the "---" prints out after.
Your design is wrong.
The Alamofire request function is asynchronous. It returns immediately, before the results are ready. Then it invokes the completion handler that you pass in.
I tend to agree with Paul that you should make the request in the destination view controller, not in the current view controller.
If you DO want to fetch the data in Alamofire before you segue to the other view controller then you'll need to write a method that starts the request, then invokes the segue to the other view controller from the Alamofire.request() call's completion handler.
If you're invoking the new view controller from a button press, you need to not link the button directly to a segue, but instead write an IBAction method that triggers the AlamoFire request call, then invokes the segue (or instantiates the view controller directly and pushes/presents it manually) from the completion handler.
Something like this:
#IBAction buttonAction(sender: UIButton) {
Alamofire.request("MYURL.com").responseJSON {
(response) in
if let JSON : NSDictionary = response.result.value as! NSDictionary? {
let classNameArray = JSON["className"] as! NSArray
print("---")
print(classNameArray)
let theClassChooseViewController = storyboard.instantiateViewController(withIdentifier:"ClassChooseViewController" as ClassChooseViewController
theClassChooseViewController.classNameArray = classNameArray
presentViewController(theClassChooseViewController,
animated: true,
completion: nil)
}
}
}
By the way, class names and types should start with an upper case letter, and variable names should start with a lower-case letter. This is a very strong convention in both Swift and Objective-C, and you'll confuse the hell out of other iOS/Mac developers unless you follow the convention.

Who came first? IBAction or ViewDidLoad

I have a Button on First VC which is directed to two active states.
1) SecondVC
override func viewDidLoad() {
super.viewDidLoad()
subjectPickerView.dataSource = self
subjectPickerView.delegate = self
SwiftyRequest()
// Used the text from the First View Controller to set the label
}
func SwiftyRequest(){
print("SecondViewController METHOD BEGINS")
let jsonobj = UserDefaults.standard.object(forKey: "PostData")
let json = JSON(jsonobj as Any)
for i in 0 ..< json.count{
let arrayValue = json[i]["name"].stringValue
print(arrayValue)
self.subjects.append(arrayValue)
self.subjectPickerView.reloadAllComponents()
}
print(self.subjects)
}
2) IBAction of FirstVC
#IBAction func buttonPressed(_ sender: Any) {
Alamofire.request("http://localhost/AIT/attempt3.php",method: .post, parameters: ["something": semValue, "branch" : streamValue])
.responseJSON { response in
print(response.result)
if let JSON1 = response.result.value {
print("Did receive JSON data: \(JSON1)")
// JSONData.someData = JSON1 as AnyObject?
UserDefaults.standard.set(JSON1, forKey: "PostData")
UserDefaults.standard.synchronize()
}
else {
print("JSON data is nil.")
}
}
}
NOW, Whenever i pressed the button it calls the viewDidLoad of SecondVC before IBAction of FirstVC which is a bit problematic for my app! How can i decide the priority between these two function.
You have to think about what you want to happen. Clearly the Alamofire call is going to take some time. What do you want to do with the 2nd VC while that time elapses? What do you want to do if the call does not return at all?
This is a common problem when dependent on external resources. How do you manage the UI? Do you present the UI in a partial state? Do you put a popover saying something like "loading". Or do you wait for the resource to complete before presenting the 2nd VC at all?
We cannot make that decision for you, since it depends on your requirement. There are ways to implement each one, though. If the resource usually responds quickly you could show the VC in a partial state and then populate it on some kind of callback. Typically call backs are either (1) blocks (2) delegate methods or (3) notifications. There is also (less commonly) (4) KVO. You should probably research the pros and cons of each.

segue called twice when using DispatchQueue.main.async

I'm trying to perform a segue to a new view controller, but the segue is being called twice and the new view controller appears twice.I'm using a method that performs a GET request to an API to retrieve data.That method uses a completion handler.
func getSearchResultsForQuery(_ query: String, completionHandlerForSearchResultsForQuery: #escaping (_ success: Bool, _ error: NSError?) -> Void)
When the method completes successfully my segue is called, from within the main queue as is required.
I've set breakpoints so I could see what was going on and the execution jumps from the performSegue back up to the conditional that checks if the method was successful and then continues until the segue is called a second time. I've tried a purely programatic segue, but the result was the same.I also added a print statement, and if I comment out the segue the print statement is only called once.
I've used this same pattern a number of times before and never had a problem with it and I just can't figure out why this is happening.The only thing I'm doing different this time is using Swift 3 and using DispatchQueue.main.async instead of dispatch_async(dispatch_get_main_queue(). Here is the function which is giving me this problem:
#IBAction func search(_ sender: UIButton) {
let searchQuery = searchField.text
TIClient.sharedInstance().getSearchResultsForQuery(searchQuery!) { (success, error) in
if success {
print("Food items fetch successful")
DispatchQueue.main.async {
print("Perorming segue for food item: \(searchQuery)")
self.performSegue(withIdentifier: "showFoodItems", sender: self)
}
} else {
print("error: \(error)")
}
}
}
Edit: I never found out what the problem was, but completely deleting the story board and recreating it solved it.
I know this isn't a great way to fix this issue, Also I can't leave a comment due to low reputation but what happens if you wrap the whole if statement in DispatchQueue.main?
#IBAction func search(_ sender: UIButton) {
let searchQuery = searchField.text
TIClient.sharedInstance().getSearchResultsForQuery(searchQuery!) { (success, error) in
DispatchQueue.main.async {
if success {
print("Food items fetch successful")
self.performSegue(withIdentifier: "showFoodItems", sender: self)
} else {
print("error")
}
}
}
Would that yield a different result or still the same result? checking for Bool doesn't require too much processing power so I don't think putting it in a main queue is a bad thing but I'd do this to trouble shoot. Sorry I can't just comment on this.
Check in storyboard, maybe you set segue from your button action instead of controller.

(Xcode 6 beta / Swift) performSegueWithIdentifier has delay before segue

I'm just learning Ios programming for the first time, with Swift and Xcode 6 beta.
I am making a simple test app that should call an API, and then segue programmatically to a different view to present the information that was retrieved.
The problem is the segue. In my delegate method didReceiveAPIResults, after everything has been successfully retrieved, I have:
println("--> Perform segue")
performSegueWithIdentifier("segueWhenApiDidFinish", sender: nil)
When the app runs, the console outputs --> Perform segue, but then there is about a 5-10 second delay before the app actually segues to the next view. During this time all the UI components are frozen.
I'm a little stuck trying to figure out why the segue doesn't happen immediately, or how to debug this!
Heres The Full View controller:
import UIKit
class ViewController: UIViewController, APIControllerProtocol {
#lazy var api: APIController = APIController(delegate: self)
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func didReceiveAPIResults(results: NSDictionary) {
println(results)
println("--> Perform segue")
performSegueWithIdentifier("segueWhenApiDidFinish", sender: nil)
}
#IBAction func getData(sender : AnyObject){
println("--> Get Data from API")
api.getInfoFromAPI()
}
}
And my API controller:
import UIKit
import Foundation
protocol APIControllerProtocol {
func didReceiveAPIResults(results: NSDictionary)
}
class APIController: NSObject {
var delegate: APIControllerProtocol?
init(delegate: APIControllerProtocol?) {
self.delegate = delegate
}
func getInfoFromAPI(){
let session = NSURLSession.sharedSession()
let url = NSURL(string: "https://itunes.apple.com/search?term=Bob+Dylan&media=music&entity=album")
let task = session.dataTaskWithURL(url, completionHandler: {data, response, error -> Void in
if(error) {
println("There was a web request error.")
return
}
var err: NSError?
var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions. MutableContainers, error: &err) as NSDictionary
if(err?) {
println("There was a JSON error.")
return
}
self.delegate?.didReceiveAPIResults(jsonResult)
})
task.resume()
}
}
UPDATE: Got this working based on Ethan's answer. Below is the exact code that ended up getting the desired behavior. I needed assign that to self to have access to self inside the dispatch_async block.
let that = self
if(NSThread.isMainThread()){
self.delegate?.didReceiveAPIResults(jsonResult)
}else
{
dispatch_async(dispatch_get_main_queue()) {
println(that)
that.delegate?.didReceiveAPIResults(jsonResult)
}
}
Interestingly, this code does not work if I remove the println(that) line! (The build fails with could not find member 'didReceiveAPIResults'). This is very curious, if anyone could comment on this...
I believe you are not on the main thread when calling
self.delegate?.didReceiveAPIResults(jsonResult)
If you ever are curious whether you are on the main thread or not, as an exercise, you can do NSThread.isMainThread() returns a bool.
Anyway, if it turns out that you are not on the main thread, you must be! Why? Because background threads are not prioritized and will wait a very long time before you see results, unlike the mainthread, which is high priority for the system. Here is what to do... in getInfoFromAPI replace
self.delegate?.didReceiveAPIResults(jsonResult)
with
dispatch_sync(dispatch_get_main_queue())
{
self.delegate?.didReceiveAPIResults(jsonResult)
}
Here you are using GCD to get the main queue and perform the UI update within the block on the main thread.
But be wear, for if you are already on the main thread, calling dispatch_sync(dispatch_get_main_queue()) will wait FOREVER (aka, freezing your app)... so be aware of that.
I have a delay problem with segue from a UITableView. I have checked and I appear to be on the main thread. I checked "NSThread.isMainThread()" during prepareForSegue. It always returns true.
I found a solution on Apple Developer forums! https://forums.developer.apple.com/thread/5861
This person says it is a bug in iOS 8.
I followed their suggestion to add a line of code to didSelectRowAtIndexPath...... Despatch_async.....
It worked for me, hopefully you too.

Resources