Who came first? IBAction or ViewDidLoad - ios

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.

Related

Swift: How can I make sure the seque does not run before the http task is finished?

I want the http task to run, setting the globally defined variable jwt. Then and only then do I want to run the seque that passes the jwt to my next activity.
I know that they are running out of order because the print statements are out of order. Capture is at the bottom. I redacted the actual jwt but where the black box is, is the jwt string.
Main view controller
import UIKit
var jwt = ""
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func submitLogin() {
let url = URL(string: "http://example.com")
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
guard error == nil else {
print(error!)
return
}
guard let data = data else {
print("Data is empty")
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data, options:.allowFragments) as? [String:Any] {
jwt = json["jwt"] as! String
print("Checkpoint 1 " + jwt)
}
} catch let err{
print(err.localizedDescription)
}
}
task.resume()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "loginSubmit" {
if let toViewController = segue.destination as? HomeActivity {
print("Checkpoint 2 " + jwt)
toViewController.token = jwt
}
}
}
}
Second View Controller
import UIKit
class HomeActivity: UIViewController {
var token:String!
override func viewDidLoad() {
super.viewDidLoad()
renderInbox()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func renderInbox() {
print("Printing token" + token)
}
}
P.S I have only been doing Swift for less than a week. So any general tips or the like would be welcome. Such as how to pass the jwt to the seque without using a global variable. Or what that is after checkpoint 1 in the output picture.
P.P.S Sorry about the code being in and out of the Stack Overflow code blocks. If you know how to fix it let me know and I'll try
It sounds like you have a single button that triggers both a segue and a URLSession request. Since the request is asynchronous, the segue will always start before the request completes. The button triggers both at the same time, and there is no way that I know of to tell the segue to wait.
If you want the segue to start after the request, you'll need to remove the existing segue from the button (just click it and delete in Interface Builder), create a manual segue (with an identifier), and then present the segue manually using performSegue.
To create a manual segue in Interface Builder, control-drag from the ViewController icon (yellow circle with a white square inside) in the top bar of your source ViewController to anywhere in your destination ViewController.
A menu will pop up and let you select what kind of transition you want. Once it's created, go to the Attributes inspector and set a descriptive identifier.
Now you add code in the URLSession's callback to manually invoke your new segue. Add this line right after "Checkpoint 1":
performSegue(withIdentifier: "Your Identifier Here", sender: self)
Now your segue should happen only after the request is complete. This is also nice because if the request fails or you don't like the response, you can skip performing the segue at all.
No need to use segue here.
In the call back of the task, you can use presentViewController method to present the new view controller (or pushViewController if you have a navigation controller).

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.

iOS Swift: best way to pass data to other VCs after query is completed

Context: iOS App written in Swift 3 powered by Firebase 3.0
Challenge: On my app, the user's currentScore is stored on Firebase. This user can complete/un-complete tasks (that will increase/decrease its currentScore) from several ViewControllers.
Overview of the App's architecture:
ProfileVC - where I fetch the currentUser's data from Firebase & display the currentScore.
⎿ ContainerView
⎿ CollectionViewController - users can update their score from here
⎿ DetailsVC - (when users tap on a collectionView cell) - again users can update their score from here.
Question: I need to pass the currentScore to the VCs where the score can be updated. I thought about using prepare(forSegue) in cascade but this doesn't work since it passes "nil" before the query on ProfileVC is finished.
I want to avoid having a global variable as I've been told it's bad practice.
Any thoughts would be much appreciated.
Why don't you create a function that will pull all data before you do anything else.
So in ViewDidLoad call...
pullFirebaseDataASYNC()
Function will look like below...
typealias CompletionHandler = (_ success: Bool) -> Void
func pullFirebaseDataASYNC() {
self.pullFirebaseDataFunction() { (success) -> Void in
if success {
// Perform all other functions and carry on as normal...
Firebase function may look like...
func pullFirebaseDataFunction(completionHandler: #escaping CompletionHandler) {
let refUserData = DBProvider.instance.userDataRef
refUserData.observeSingleEvent(of: .value, with: { snapshot in
if let dictionary = snapshot.value as? [String: AnyObject] {
self.userCurrentScore = dictionary["UserScore"] as! Int
completionHandler(true)
}
})
}
Then when you segue the information across...
In ProfileVC
Create 2 properties
var containerVC: ContainerVC!
var userCurrentScore = Int()
Include the below function in ProfileVC...
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ProfileToContainerSegue" {
let destination = segue.destination as! ContainerVC
containerVC = destination
containerVC.userCurrentScore = userCurrentScore
}
}
In ContainerVC create a property
var userCurrentScore = Int()
Ways to improve could be an error message to make sure all the information is pulled from Firebase before the user can continue...
Then the information can be segued across the same way as above.
Try instantiation, first embed a navigation controller to your first storyboard, and then give a storyboardID to the VC you are going to show.
let feedVCScene = self.navigationController?.storyboard?.instantiateViewController(withIdentifier: "ViewControllerVC_ID") as! ViewController
feedVCScene.scoreToChange = current_Score // scoreToChange is your local variable in the class
// current_Score is the current score of the user.
self.navigationController?.pushViewController(feedVCScene, animated: true)
PS:- The reason why instantiation is much healthier than a modal segue storyboard transfer , it nearly removes the memory leaks that you have while navigating to and fro, also avoid's stacking of the VC's.

Pass Data using Closures

I know that there are multiple approaches to pass data back from one controller to another like Delegates, NSNotifications. I am using another way using Closures to pass data data back. I just want to know is it safe way how I pass any data using blocks like below or should I avoid this approach.
First ViewController (where I make object of Second ViewController)
#IBAction func push(sender: UIButton) {
let v2Obj = storyboard?.instantiateViewControllerWithIdentifier("v2ViewController") as! v2ViewController
v2Obj.completionBlock = {(dataReturned) -> ()in
//Data is returned **Do anything with it **
print(dataReturned)
}
navigationController?.pushViewController(v2Obj, animated: true)
}
Second ViewController (where data is passed back to First VC)
import UIKit
typealias v2CB = (infoToReturn :NSString) ->()
class v2ViewController: UIViewController {
var completionBlock:v2CB?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func returnFirstValue(sender: UIButton) {
guard let cb = completionBlock else {return}
cb(infoToReturn: returnFirstValue)
}
#IBAction func returnSecondValue(sender: UIButton) {
guard let cb = completionBlock else {return}
cb(infoToReturn: returnSecondValue)
}
}
That's a very good and reasonable approach and much better than notifications.
Looking at the evolution of Cocoa API you will notice that Apple has replaced more and more delegate API with blocks / closures over the years.

How to load view after Alamofire finished its job?

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.

Resources