Swift catching async function in different view - ios

I have an async function (geocodeAddressString) that gets called in the prepare for segue function. The problem is that this async function returns after the segue is executed. The async function below converts an address string into coordinates. I can't call this async function before this prepare function as the user address field can be edited by the user at any time. I've tried calling the async function when the save button is pressed and then coding the segue when the async function returns, but then the loading time between the button being pressed and the segue occurring is too long.
So my question is if there is any way that I can catch this async method in the new view? Then I could just replace some temporary coordinates with the actual returned values. Any help is appreciated, Thanks!
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
//convert address to coordinates
let geoCoder = CLGeocoder()
geoCoder.geocodeAddressString(addressTxt.text!) { (placemarks, error) in
guard
let placemarks = placemarks,
let location = placemarks.first?.location
else {
// handle no location found
return
}
let tempCoord = CLLocationCoordinate2D(latitude: (location.coordinate.latitude), longitude: (location.coordinate.longitude))
self.donatedItem = DonatedItem(self.titleTxt.text!, self.itemImg.image!, self.donated, self.descTxt.text!, expirationDate, tempCoord, (user?.uid)!, (self.donatedItem?.itemID)!, self.addressTxt.text!, reserved: false, reservedBy: "NA")
self.donatedItem?.updateItem()
}
switch(segue.identifier ?? "") {
case "saveItem":
homeViewController.isReturningSegue = true
homeViewController.tempItem = self.donatedItem
default:
print("Undefined segue")
}
}

Of course you can. Post a notification to notify the new view. Or use closure. For example, this is notification.
NotificationCenter.default.post(name: Notification.Name(rawValue: "locationUpdated"), object: coordinate)
This is how to do this with closure.
typealias closure = (String) -> Void
class A: UIViewController {
var coordinateUpdated: closure? // call this after coordinate updated
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let b = segue.destination as? B {
coordinateUpdated = b.coordinateUpdated
}
}
}
class B: UIViewController {
let coordinateUpdated: closure = { (coordinate: String) in
// update here
}
}

You could instead pass on addressTxt.text! and then call the async method in the next view controller .
In the current view controller:
homeViewController.isReturningSegue = true
homeViewController.tempItem = self.donatedItem
homeViewController.address = self.addressTxt.text!
In the next view controller:
func viewWillAppear() {
supper.viewWillAppear()
let geoCoder = CLGeocoder()
geoCoder.geocodeAddressString(self.address) { ... }
}
If geocodeAddressString is taking too long (and affecting the UX) you could also put up a progress indicator. For instance, MBProgressHUD is a pretty common framework for such tasks.

Related

How do I pass my array data coming from api to another ViewController?

I'm new in iOS.
I'm trying to pass my array data to another view controller through prepareforsegue method.
And this api is called inside the #IBAction func ButtonTapped( ).
class FirstVc {
var location = [Any]()
self.clientrequest.request(url: "http://api.details.in/api/users/alllocation", method: .GET, completion: {
res , err in
let json = try! JSONSerialization.data(withJSONObject: res , options: .prettyPrinted)
let decode = try! JSONDecoder().decode(SelectLocation.self, from:json)
self.location = decode.states
//I'm getting all locations here
print(self.location)
})
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.destination is SecondVc
{
let vc = segue.destination as? SecondVc
//here I'm assigning the variable and while I'm printing location here then also I'm getting the value
vc?.mineSpillere2 = self.location
}
}
class SecondVc{
#IBOutlet weak var selectState: UILabel!
var mineSpillere2 = [Any]()
override func viewDidLoad() {
//While I'm trying to print mineSpillere2 here I', getting []
selectState.text = mineSpillere2 as? String
}
}
One problem might be that you don't actually assign the variable (because vc may be nil). Also you may want to use segueIdentifiers, especially if you have more than one. To make sure that you do so, you may want to use something like this:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let vc = segue.destination as? SecondVc //, segue.identifier = "YOUR_SEGUE_ID"
{
vc.mineSpillere2 = self.location
} else { print("SecondVc couldn't be initialised!") }
}
First, the problem is that web service request is not executed asynchronously but synchronously. In other words, your segue is already performed before decode.states is assigned to the location variable.
The solution is to call performSegue function in the completion block:
self.clientrequest.request(url: "http://api.details.in/api/users/alllocation", method: .GET, completion: {
res , err in
let json = try! JSONSerialization.data(withJSONObject: res , options: .prettyPrinted)
let decode = try! JSONDecoder().decode(SelectLocation.self, from:json)
self.location = decode.states
self.performSegue(identifier: "your identifier",
sender: nil)
//I'm getting all locations here
print(self.location)
})
Second,
Make sure your segue is assigned from VC1 to VC2 not Button to VC2.
There are several way to pass this in another ViewController. Good way is make a model class and pass model object to another vc .
Or might be another short and easy solution is make Global variable. So you can access it from any ViewController class. like
var myGloblArray= [Any]()
//add your result array in this array , you can access it from second VC Directly.
class FirstVc {
...
}

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.

Swift 3 pass data through completion with userID

I'm trying to run a check on Firebase to see if a user exists, then I need to check for specific vales before continuing. I currently have this:
func myFirebaseNetworkDataRequest(finished: () -> Void) {
if let user = FIRAuth.auth()?.currentUser {
self.userUUID = (user.uid)
getUser(userUUID: self.userUUID)
finished()
}
}
In my view did load:
myFirebaseNetworkDataRequest {
// perform further operations here after data is fetched
if AppState.sharedInstance.user == true {
//present 1st view controller
} else {
//present 2nd view controller
}
In my "getUser" function:
func getUser(userUUID: String) {
let userFacebookRef = FIRDatabase.database().reference(withPath: "users").child(userUUID)
//The rest of the Firebase function.
AppState.sharedInstance.user == results.active
//active is = to true
What currently happens is that if presents the 2nd view controller because firebase hasent finished yet. I realize I need a block because firebase is already asnyc but how do I send userUUID through the closure/block?
You can have your closure parameter have an parameter of it's own. Something like,
func myFirebaseNetworkDataRequest(finished: (_ isAuthenticated: Bool) -> Void)
Then in your viewDidLoad, you would make your request...
open override func viewDidLoad() {
self.myFirebaseNetworkDataRequest( (isAuthenticated) -> {
if isAuthenticated {
// Present VC1
} else {
// Present VC 2
}
}
}

Swift - Passing data coming from an API to another view controller

Please check the below code:
#IBAction func sendActivationCode(_ sender: UIButton) {
service.Register(phoneNumber: self.mobileNumberTxt.text!, callback: { (response) in
self.setCustomerValues(response: response)
})
}
func setCustomerValues(response: [String:Any]) {
registrationToken = (response["token"]! as! String)
code = response["code"] as! Int
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "toStep2" {
let vc = segue.destination as! Step2ViewController
vc.registrationToken = registrationToken
}
}
The problem is: prepare function is executed before setCustomerValues and I cannot use registrationToken variable in Step2ViewController.swift because it's nil.
Instead of connecting your segue from the button to Step2ViewController, connect it from the view controller. This way the segue will not automatically be performed when the button is touched.
Then call performSegue from within your setCustomerValues callback to perform the segue explicitly after getting the registration token. Note that if the callback is not on the main thread, you will need to dispatch_async to the main thread before calling performSegue.
You should push viewcontroller after self.setCustomerValues(response: response). Don't push viewcontroller when sendActivationCode
The best way to come out of this problem is to create an IBAction method from your button on a Touch Up Inside Event and not create any Segues on 'action' of your button.
Use the following code:
#IBAction func sendActivationCode(_ sender: UIButton) {
service.Register(phoneNumber: self.mobileNumberTxt.text!, callback: {
(response) in
let vc = self.storyboard?.instantiateViewControllerWithIdentifier("Step2ViewController") as! Step2ViewController
vc.registrationToken = (response["token"]! as! String)
vc.code = response["code"] as! Int
self.navigationController?.pushViewController(vc!, animated: true)
})
}

How to force asynchronously save a constant subclass?

Edit 1: I've restructured my ViewControllers to make it easier to get what I want done.
Edit 2: I realized something major was missing while adding notes to my code, another function overrides the first segue.
This ViewController is where the annotation is created; all I need from this view is for the touchMapCoordinates to be transferred to the other ViewController so I can save the PFGeoPoint in an array.
Edit 3
After long work on understanding what is going on and simplifying the code, i've came down to the final conclusion based off of Swift- variable not initialized before use (but it's not used) , that the current method that I'm trying to use will not work in any case or scenario due to it saving Asynchronously. If anyone knows a work around, then you have officially done something that hasn't been done before :).
Error that is showing up is
Constant 'boi' used before being initialized
Subclass that is declared in Appdata to be used anywhere within the project
import Foundation
import Parse
import MapKit
class MyAnnotation: PFObject, PFSubclassing, MKAnnotation {
// MARK: - Properties
#NSManaged var location: PFGeoPoint
// MARK: - Initializers
init(coordinate: CLLocationCoordinate2D) {
super.init()
self.location = PFGeoPoint(latitude: coordinate.latitude, longitude: coordinate.longitude)
print(location)
}
override class func initialize() {
struct Static {
static var onceToken : dispatch_once_t = 0;
}
dispatch_once(&Static.onceToken) {
self.registerSubclass()
}
}
// MARK: - PFSubclassing protocol
static func parseClassName() -> String {
return "AnnotationPins"
}
// MARK: - MKAnnotation protocol
var coordinate: CLLocationCoordinate2D {
return CLLocationCoordinate2DMake(location.latitude, location.longitude)
}
var title: String? = "Start Topic"
}
Where the code will all be saved asynchronously together
} else {
let imageData = UIImagePNGRepresentation(self.galleryCameraImage.image!)
let parseImageFile = PFFile(name: "upload_image.png", data: imageData!)
let boi : MyAnnotation
let textTitleandText = PFObject(className: "AnnotationPins")
textTitleandText["textTopic"] = userTopic.text
textTitleandText["textInformation"] = userText.text
textTitleandText["userUploader"] = PFUser.currentUser()
textTitleandText["imageFile"] = parseImageFile!
textTitleandText["location"] = boi.location
textTitleandText.saveInBackgroundWithBlock { (success: Bool, error: NSError?) -> Void in
if error == nil {
If anyone could help it would be really appreciated!
Over ride prepareForSegue method like below.
override func prepareForSegue(segue: UIStoryboardSegue, sender:
AnyObject?) {
if segue.identifier == "SegueID" {
let destinationVC = segue.destinationViewController as! DestinationViewController
// Create property in destinationView controller & assign required data from here.
}
}
Hope it helps.
Lets treat your Location data as a normal data to be transferred through segues.
You can use this method to configure your destination View controller variable(same type) that will hold your location data.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
//check your segue name
if segue.identifier == "YourSegueIdentifier" {
let destinationVC = segue.destinationViewController as! YourDestinationViewController
destinationVC.locationVariableInDestinationVC = locationVariableInCurrentVC
}
}
Above is the simplest way to pass data via segue, you can use the same approach for your location data too.
Hope that helps!!
Update: Based on your updated code
Move func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {} out of your handleLongPress function. PrepareForSegue function gets called automatically when there is any navigation happening through segues..
If you want to initiate a segue navigation programatically then assign a identifier to the segue and just call self.performSegueWithIdentifier("YourSegueIdentifier", sender: nil)

Resources