Aynchronous API call not returning data before variable initialization - swift

I am segueing from an initial view to a second view. The first view consists of a text field for a user to enter their search term (account name). That name gets segued to the second view.
I then take this name, and make a call to the Riot API, to return details of their account (name, id, and account ID, level, etc.).
I then update the GUI labels with their credentials (name, id, level).
Now is where this process falls apart; the program does not wait for the API call to complete before it moves forward.
The step breakdown I want is essentially in this order:
1)Use the search term from the segue to call the Riot API
2)Update the GUI with the credentials
3)Initialize some variables with the credentials returned from the Riot API
Instead, this is happening:
1)Use the search term from the segue to call the Riot API
2)Initialize some variables (with credentials from the API (but it cant because they aren't returned yet))
3)Update the GUI with the credentials
The program is skipping forwards and not waiting for data to be returned.
I have come to the understanding that this way of calling the API is 'asynchronous', and that it will not wait for the data to be retrieved before it continues - and the DispatchQueue.main.async{} is useful so long as everything else you need to code follows within the braces.
If you append code after task.resume() it will not use the data retrieved from the call because it isn't returned yet.
My question is essentially: how do I make the 'let task = UrlSession.shared... task.resume() wait for the users credentials so I can then proceed forward with the rest of the code, sequentially - rather than embedding the rest of this view's code within the DispatchQueue.main.async{} braces?
import UIKit
class ViewControllerProfile: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//test to see if we got the name from the segue:
print("from Segue: " + summonerName)
//execute on view load:
let theUser = Summoner(name: summonerName)
print(theUser.name!)
//API - Summoners Details By Summoner Name:
//Construct request for name in secondViewController(PROFILE)
let root:String = "https://euw1.api.riotgames.com"
let entryPoint:String = "/lol/summoner/v3/summoners/by-name/"
let key:String = "?api_key=<key>"
//theUser.name! is a search term (players name)
let completeURL = root + entryPoint + theUser.name! + key
let urlRecieved = URL(string: completeURL.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)!)
let task = URLSession.shared.dataTask(with: urlRecieved!){ (data, response, error) in
if error != nil{
print("ERROR")
}
else{
if let content = data{
do{
//Array:
let myJson = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as! [String:Any]
//We now extract the required information from the JSON output with keys:
//Class generics for future parsing:
let summonerID = myJson["id"] as? UInt64
let usersSummonerID:Int = Int(summonerID!)
print(usersSummonerID)
let accountID = myJson["accountId"] as? UInt64
let usersAccountID = Int(accountID!)
print(usersAccountID)
//Required elements for Profile Labels:
let extractName = myJson["name"] as? String
let extractLevel = myJson["summonerLevel"] as? UInt64
//We need to convert the extractLevel variable from UInt64 to String to update a label without an optional:
let usersLevel:String = "\(extractLevel!)"
//We update the gui now using the data we got from the API call:
DispatchQueue.main.async {
//We dispatch the user interface updates to the main thread here:
self.summonerLevelLbl.text = ("Level: " + "\(usersLevel)")
self.summonerNameLbl.text = extractName
//We dispatch the api-returned userObject's attributes to the main thread for assignment here:
self.summonerNameMain = extractName!
self.summonerIDMain = usersSummonerID
self.accountIDMain = usersAccountID
print("dispatch completed\n\n")
//all future code goes here?
}
}
catch{
print("SOMETHING WENT WRONG WITH SERIALIZATION OF NAME X")
}
}
}
}
task.resume()
theUser.name = summonerNameMain
theUser.accountID = accountIDMain
theUser.summonerID = summonerIDMain
print("ASSIGNMENT TEST")
print(theUser.name!)
print(theUser.summonerID!)
print(theUser.accountID!)
}
#IBOutlet weak var summonerNameLbl: UILabel!
#IBOutlet weak var summonerRankLbl: UILabel!
#IBOutlet weak var summonerLevelLbl: UILabel!
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//For Segue:
var summonerName = String()
//For Parsing:
var accountIDMain = Int()
var summonerIDMain = Int()
var summonerNameMain = String()
}
class Summoner:NSObject{
var name:String?
var summonerID:Int?
var accountID:Int?
var matchID:Int?
init(name: String){
self.name = name
}
}
After task.resume()
I want it to wait for the above code to complete before proceeding (let task = URLSession.shared.dataTask... up to task.resume())
Further API calls that require data from previous calls, will be necessary and preferably I would prefer to avoid embedding more code in the async.

Simply move these lines (and the print statements if you want):
theUser.name = summonerNameMain
theUser.accountID = accountIDMain
theUser.summonerID = summonerIDMain
to where you have your comment about "all future code".
Done.

Related

Swift launch view only when data received

I'm getting info from an API using the following function where I pass in a string of a word. Sometimes the word doesn't available in the API if it doesn't available I generate a new word and try that one.
The problem is because this is an asynchronous function when I launch the page where the value from the API appears it is sometimes empty because the function is still running in the background trying to generate a word that exists in the API.
How can I make sure the page launches only when the data been received from the api ?
static func wordDefin (word : String, completion: #escaping (_ def: String )->(String)) {
let wordEncoded = word.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let uri = URL(string:"https://dictapi.lexicala.com/search?source=global&language=he&morph=false&text=" + wordEncoded! )
if let unwrappedURL = uri {
var request = URLRequest(url: unwrappedURL);request.addValue("Basic bmV0YXlhbWluOk5ldGF5YW1pbjg5Kg==", forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
do {
if let data = data {
let decoder = JSONDecoder()
let empty = try decoder.decode(Empty.self, from: data)
if (empty.results?.isEmpty)!{
print("oops looks like the word :" + word)
game.wordsList.removeAll(where: { ($0) == game.word })
game.floffWords.removeAll(where: { ($0) == game.word })
helper.newGame()
} else {
let definition = empty.results?[0].senses?[0].definition
_ = completion(definition ?? "test")
return
}
}
}
catch {
print("connection")
print(error)
}
}
dataTask.resume()
}
}
You can't stop a view controller from "launching" itself (except not to push/present/show it at all). Once you push/present/show it, its lifecycle cannot—and should not—be stopped. Therefore, it's your responsibility to load the appropriate UI for the "loading state", which may be a blank view controller with a loading spinner. You can do this however you want, including loading the full UI with .isHidden = true set for all view objects. The idea is to do as much pre-loading of the UI as possible while the database is working in the background so that when the data is ready, you can display the full UI with as little work as possible.
What I'd suggest is after you've loaded the UI in its "loading" configuration, download the data as the final step in your flow and use a completion handler to finish the task:
override func viewDidLoad() {
super.viewDidLoad()
loadData { (result) in
// load full UI
}
}
Your data method may look something like this:
private func loadData(completion: #escaping (_ result: Result) -> Void) {
...
}
EDIT
Consider creating a data manager that operates along the following lines. Because the data manager is a class (a reference type), when you pass it forward to other view controllers, they all point to the same instance of the manager. Therefore, changes that any of the view controllers make to it are seen by the other view controllers. That means when you push a new view controller and it's time to update a label, access it from the data property. And if it's not ready, wait for the data manager to notify the view controller when it is ready.
class GameDataManager {
// stores game properties
// updates game properties
// does all thing game data
var score = 0
var word: String?
}
class MainViewController: UIViewController {
let data = GameDataManager()
override func viewDidLoad() {
super.viewDidLoad()
// when you push to another view controller, point it to the data manager
let someVC = SomeOtherViewController()
someVC.data = data
}
}
class SomeOtherViewController: UIViewController {
var data: GameDataManager?
override func viewDidLoad() {
super.viewDidLoad()
if let word = data?.word {
print(word)
}
}
}
class AnyViewController: UIViewController {
var data: GameDataManager?
}

UIImage returns nil on segue push

I have an image URL that needs to be parsed and displayed. The URL exists, but returns nil.
It successfully parses in the cellForRowAt function by calling cell.recipeImage.downloadImage(from: (self.tableViewDataSource[indexPath.item].image))
With this line the image displays. However, it doesn't exist when calling it in didSelectRowAt
RecipeTableViewController.swift
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let Storyboard = UIStoryboard(name: "Main", bundle: nil)
let resultsVC = Storyboard.instantiateViewController(withIdentifier: "ResultsViewController") as! ResultsViewController
// Information to be passed to ResultsViewController
if (tableViewDataSource[indexPath.item] as? Recipe) != nil {
if isSearching {
resultsVC.getTitle = filteredData[indexPath.row].title
//resultsVC.imageDisplay.downloadImage(from: (self.filteredData[indexPath.row].image))
} else {
resultsVC.getTitle = tableViewDataSource[indexPath.row].title
// Parse images
resultsVC.imageDisplay.downloadImage(from: (self.tableViewDataSource[indexPath.row].image))
}
}
// Push to next view
self.navigationController?.pushViewController(resultsVC, animated: true)
}
extension UIImageView {
func downloadImage(from url: String) {
let urlRequest = URLRequest(url: URL(string: url)!)
let task = URLSession.shared.dataTask(with: urlRequest) { (data,response,error) in
if error != nil {
print(error!)
return
}
DispatchQueue.main.sync {
self.image = UIImage(data: data!)
}
}
task.resume()
}
}
ResultsViewController.swift
class ResultsViewController: UIViewController {
var getTitle = String()
var getImage = String()
#IBOutlet weak var recipeDisplay: UILabel!
#IBOutlet weak var imageDisplay: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
recipeDisplay.text! = getTitle
}
...
}
Returns the error
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
From my understanding, the app is getting crashed at this line:
recipeDisplay.text! = getTitle
If it is, obviously this is not the proper way to do it. Just remove the force unwrapping because the text on the label here is nil by default. Force referencing a nil value will crash the app.
recipeDisplay.text = getTitle
UPDATED:
- Let's make sure that you wired the label and the outlets properly. Connect ti to the VC, not the File Owner.
You're calling view-related code on views that haven't been initialized yet. Remember, IBOutlets are implicitly unwrapped properties, so if you try to access them before they're initialized they'll force-unwrap and crash. So it's not that the UIImage is coming up nil, it's that recipeDisplay is nil and is getting force unwrapped.
The idiomatic iOS thing to do is to hand a view model of some sort (an object or a struct) to the view controller, and then let it do the work with that item once it has finished loading.
So, in you didSelect method, you could create your view model (which you'd need to define) and hand it off like this:
let title = filteredData[indexPath.row].title
let imageURL = self.tableViewDataSource[indexPath.row].image
let viewModel = ViewModel(title: title, imageURL: imageURL)
resultsVC.viewModel = viewModel
And then in your resultsVC, you'd do something like this:
override func viewDidLoad() {
super.viewDidLoad()
if let vm = viewModel {
recipeDisplay.text = vm.title
downloadImage(from: vm.imageURL)
}
}
So in your case all you'd need to do is hand those strings to your VC (you can wrap them up in a view model or hand them off individually) and then in that VC's viewDidLoad() that's where you'd call downloadImage(from:). That way there's no danger of calling a subview before that subview has been loaded.
One last note: Your download method should be a little safer with its use of the data and error variables, and its references to self. Remember, avoid using ! whenever you don't absolutely have to use it (use optional chaining instead), and unless you have a really good reason to do otherwise, always use [weak self] in closures.
I'd recommend doing it like this:
func downloadImage(from url: String) {
let urlRequest = URLRequest(url: URL(string: url)!)
let task = URLSession.shared.dataTask(with: urlRequest) { [weak self] (data,response,error) in
if let error = error {
print(error)
return
}
if let data = data {
DispatchQueue.main.sync {
self?.image = UIImage(data: data)
}
}
}
task.resume()
}
Update: Because the 'view model' concept was a little too much at once, let me explain.
A view model is just an object or struct that represents the presentation data a screen needs to be in a displayable state. It's not the name of a type defined by Apple and isn't defined anywhere in the iOS SDK. It's something you'd need to define yourself. So, in this case, I'd recommend defining it in the same fine where you're going to use it, namely in the same file as ResultsViewController.
You'd do something like this:
struct ResultsViewModel {
let title: String
let imageURL: String
}
and then on the ResultsViewController, you'd create a property like:
var viewModel: ResultsViewModel?
or if you don't like dealing with optionals, you can do:
var viewModel = ResultsViewModel(title: "", imageURL: "")
OR, you can do what you're already doing, but I'd highly recommend renaming those properties. getTitle sounds like it's doing something more besides just holding onto a value. title would be a better name. Same criticism goes for getImage, with the additional criticism that it's also misleading because it sounds like it's storing an image, but it's not. It's storing an image url. imageURL is a better name.

Getting data out of a firebase function in Swift [duplicate]

In my iOS app, I have two Firebase-related functions that I want to call within viewDidLoad(). The first picks a random child with .queryOrderedByKey() and outputs the child's key as a string. The second uses that key and observeEventType to retrieve child values and store it in a dict. When I trigger these functions with a button in my UI, they work as expected.
However, when I put both functions inside viewDidLoad(), I get this error:
Terminating app due to uncaught exception 'InvalidPathValidation', reason: '(child:) Must be a non-empty string and not contain '.' '#' '$' '[' or ']''
The offending line of code is in my AppDelegate.swift, highlighted in red:
class AppDelegate: UIResponder, UIApplicationDelegate, UITextFieldDelegate
When I comment out the second function and leave the first inside viewDidLoad, the app loads fine, and subsequent calls of both functions (triggered by the button action) work as expected.
I added a line at the end of the first function to print out the URL string, and it doesn't have any offending characters: https://mydomain.firebaseio.com/myStuff/-KO_iaQNa-bIZpqe5xlg
I also added a line between the functions in viewDidLoad to hard-code the string, and I ran into the same InvalidPathException issue.
Here is my viewDidLoad() func:
override func viewDidLoad() {
super.viewDidLoad()
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
pickRandomChild()
getChildValues()
}
Here is the first function:
func pickRandomChild () -> String {
var movieCount = 0
movieRef.queryOrderedByKey().observeEventType(.Value, withBlock: { (snapshot) in
for movie in snapshot.children {
let movies = movie as! FIRDataSnapshot
movieCount = Int(movies.childrenCount)
movieIDArray.append(movies.key)
}
repeat {
randomIndex = Int(arc4random_uniform(UInt32(movieCount)))
} while excludeIndex.contains(randomIndex)
movieToGuess = movieIDArray[randomIndex]
excludeIndex.append(randomIndex)
if excludeIndex.count == movieIDArray.count {
excludeIndex = [Int]()
}
let arrayLength = movieIDArray.count
})
return movieToGuess
}
Here is the second function:
func getChildValues() -> [String : AnyObject] {
let movieToGuessRef = movieRef.ref.child(movieToGuess)
movieToGuessRef.observeEventType(.Value, withBlock: { (snapshot) in
movieDict = snapshot.value as! [String : AnyObject]
var plot = movieDict["plot"] as! String
self.moviePlot.text = plot
movieValue = movieDict["points"] as! Int
})
return movieDict
)
And for good measure, here's the relevant portion of my AppDelegate.swift:
import UIKit
import Firebase
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UITextFieldDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
FIRApp.configure()
return true
}
I'm guessing Swift is executing the code not in the order I expect. Does Swift not automatically wait for the first function to finish before running the second? If that's the case, why does this pairing work elsewhere in the app but not in viewDidLoad?
Edit: The issue is that closures are not called in order.
I'm not sure what your pickRandomChild() and getChildValues() methods are, so please post them as well, but the way I fixed this type issue was by sending the data through a closure that can be called in your ViewController.
For example when I wanted to grab data for a Full Name and Industry I used this. This method takes a Firebase User, and contains a closure that will be called upon completion. This was defined in a class specifically for pulling data.
func grabDataDict(fromUser user: FIRUser, completion: (data: [String: String]) -> ()) {
var myData = [String: String]()
let uid = user.uid
let ref = Constants.References.users.child(uid)
ref.observeEventType(.Value) { (snapshot, error) in
if error != nil {
ErrorHandling.defaultErrorHandler(NSError.init(coder: NSCoder())!)
return
}
let fullName = snapshot.value!["fullName"] as! String
let industry = snapshot.value!["industry"] as! String
myData["fullName"] = fullName
myData["industry"] = industry
completion(data: myData)
}
}
Then I defined an empty array of strings in the Viewcontroller and called the method, setting the variable to my data inside the closure.
messages.grabRecentSenderIds(fromUser: currentUser!) { (userIds) in
self.userIds = userIds
print(self.userIds)
}
If you post your methods, however I can help you with those specifically.
Edit: Fixed Methods
1.
func pickRandomChild (completion: (movieToGuess: String) -> ()) {
var movieCount = 0
movieRef.queryOrderedByKey().observeEventType(.Value, withBlock: { (snapshot) in
for movie in snapshot.children {
let movies = movie as! FIRDataSnapshot
movieCount = Int(movies.childrenCount)
movieIDArray.append(movies.key)
}
repeat {
randomIndex = Int(arc4random_uniform(UInt32(movieCount)))
} while excludeIndex.contains(randomIndex)
movieToGuess = movieIDArray[randomIndex]
excludeIndex.append(randomIndex)
if excludeIndex.count == movieIDArray.count {
excludeIndex = [Int]()
}
let arrayLength = movieIDArray.count
// Put whatever you want to return here.
completion(movieToGuess)
})
}
2.
func getChildValues(completion: (movieDict: [String: AnyObject]) -> ()) {
let movieToGuessRef = movieRef.ref.child(movieToGuess)
movieToGuessRef.observeEventType(.Value, withBlock: { (snapshot) in
movieDict = snapshot.value as! [String : AnyObject]
var plot = movieDict["plot"] as! String
self.moviePlot.text = plot
movieValue = movieDict["points"] as! Int
// Put whatever you want to return here.
completion(movieDict)
})
}
Define these methods in some model class, and when you call them in your viewcontroller, you should be able to set your View Controller variables to movieDict and movieToGuess inside each closure. I made these in playground, so let me know if you get any errors.
Your functions pickRandomChild() and getChildValues() are asynchronous, therefore they only get executed at a later stage, so if getChildValues() needs the result of pickRandomChild(), it should be called in pickRandomChild()'s completion handler / delegate callback instead, because when one of those are called it is guaranteed that the function has finished.
It works when you comment out the second function and only trigger it with a button press because there has been enough time between the app loading and you pushing the button for the asynchronous pickRandomChild() to perform it action entirely, allowing getChildValues() to use its returned value for its request.

using variables outside of completion block

Im currently retrieving data from firebase the data is put inside an NSObject and then a completion block. The item inside of the completion block is store as a variable userBinfos. Variable userBinfos only work inside of the completion block i want to use this outside of the completion
var userBinfos = userObject()
override func viewDidLoad() {
super.viewDidLoad()
userBinfo { (user) in
self.userBinfos = user
}
//I want to use to variable here but it does not work
print(self.userBinfos.email)
}
func userBinfo(completion: (userObject) -> ()) {
let dbFir = FIRDatabase.database().reference()
let firRef = dbFir.child("frontEnd/users/\(userId)")
firRef.observeEventType(.Value, withBlock: { snapshot in
let userDict = snapshot.value as! [String: AnyObject]
self.name.text = userDict["firstname"] as? String
self.userBio.text = userDict["userBio"] as! String
var user = userObject()
user.firstName = userDict["firstname"]
user.lastName = userDict["lastname"]
user.email = userDict["email"]
user.profileImageUrl = userDict["profileImageUrl"]
user.userBio = userDict["firstname"]
user.userId = userDict["firstname"]
dispatch_async(dispatch_get_main_queue(), {
completion(user)
})
}) { (error) in
print(error)
}
}
The entire purpose of the completion parameter of userBinfo is to provide a mechanism for being informed when the asynchronous observeEventType is called. So put code contingent upon the completion of that asynchronous method inside the userBinfo { user in ... } closure.
And if part of the UI doesn't make sense until that asynchronous completion closure is called, then have viewDidLoad configure the UI to make that explicit (perhaps show a UIActivityIndicatorView or whatever) and then remove that stuff inside the completion handler.
override func viewDidLoad() {
super.viewDidLoad()
// do whatever you want to let the user know that something asynchronous
// is happening, e.g. add a spinning `UIActivityIndicatorView` or whatever
userBinfo { user in
self.userBinfos = user
// Update the UI here, including removing anything we presented to let
// the user know that the asynchronous process was underway. If you were
// dealing with UITableView`, you'd call `tableView.reloadData()` here.
}
// but not here
}

String Returning Multiple values in Swift

I have a CoreData entity named 'Studio' with an attribute named 'name' with an NSManagedObject subclass created.
My app designed for a simple process, enter a name into a text box, and press 'save' and the name is saved into Studio.name - Press 'Update' and a text label is refreshed to show the value of Studio.name
However, it is not functioning as expected, if I, for example, enter the name 'Stack' and save the update I see 'Stack' in the text label, if i then enter 'Overflow' save/update the label reads 'Overflow', If i update it a third time to 'Swift' save/update the label again reads 'Stack'.
From there updates will give one of the three values seemingly at random.
Force quitting the app and relaunching it shows that the data is being saved to Core Data as pressing the update button will return a random previous value.
My question is, how does this happen with a string? (Shouldn't it only hold one value at a time?)
How can I correct this so it will only hold a single value and any subsequent values simply overwrite the previous value?
My code follows.
import UIKit
import CoreData
class ViewController: UIViewController {
#IBOutlet weak var studioBox: UITextField!
#IBOutlet weak var nameLabel: UILabel!
#IBAction func saveData(sender: AnyObject) {
var studio = writeStudioData()
studio.name = studioBox.text
}
#IBAction func Update(sender: AnyObject) {
var studio = getStudioData()
nameLabel.text = studio.name
}
func getStudioData() -> Studio {
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let managedContext = appDelegate.managedObjectContext!
let request = NSFetchRequest(entityName: "Studio")
request.returnsObjectsAsFaults = false
let result = managedContext.executeFetchRequest(request, error: nil) as [Studio]
return result[0]
}
func writeStudioData () -> Studio {
let appDelegate =
UIApplication.sharedApplication().delegate as AppDelegate
let managedContext = appDelegate.managedObjectContext!
let entityDescription = NSEntityDescription.entityForName("Studio", inManagedObjectContext: managedContext)
let result = Studio(entity: entityDescription!, insertIntoManagedObjectContext: managedContext)
return result
}
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.
}
}
You don't have one object, you are creating a new object every time. As you aren't including a sort descriptor with your fetch request, the order you get them, and thus the corresponding name, is unspecified, meaning it could be any of them.
You could either perform a fetch first in writeStudioData to see if there's already an object, only creating one if there isn't, or you could create one object and keep it around in a property.