Make instance wait until called functions are complete - Swift - ios

I apologise in advance if this has already been answered but as you can probably tell from the title I wasn't really sure how to describe the issue and a answer to a similar question I found wasn't helpful.
I'm attempting to make an instance of "Coupon" that has its properties loaded from an SQL database after passing an id to the database in the init method.
My issue is when I call then init method from a different viewController class it will return the instance with the default string values of "" as the data from the NSURLConnection hasn't been/decoded before returning to the viewContoller.
Im looking for a solution for to some how make the init method wait until the fields are loaded.
Coupon class relevant properties:
var webData: NSMutableData?
var id: Int
var name: String = ""
var provider: String = ""
var details: String = ""
Coupon class relevant methods:
convenience init(id: Int) {
self.init()
self.id = id
self.selectSQL(id) //passes id to server and returns all other varibles
}
func selectSQL(id: Int) {
let url = NSURL(string: "http://wwww.website.php?id=\(id)") // acess php page
let urlRequest = NSURLRequest(URL: url!)
let connection = NSURLConnection(request: urlRequest, delegate: self)
}
func connection(connection: NSURLConnection, didReceiveResponse response: NSURLResponse) {
webData = NSMutableData()
}
func connection(connection: NSURLConnection, didReceiveData data: NSData) {
webData?.appendData(data)
}
func connectionDidFinishLoading(connection: NSURLConnection) {
let result = NSJSONSerialization.JSONObjectWithData(webData!, options: .AllowFragments, error: nil) as? NSArray
let resultDict = result?[0] as? NSDictionary
if let dict = resultDict {
name = dict.objectForKey("name") as! String
provider = dict.objectForKey("provider") as! String
details = dict.objectForKey("details") as! String
}

It is not possible to "wait for your SQL finishing" and then return from init without blocking your thread (synchronize), which would not be what you want.
I suggest to use a factory method with a callback to get a workaround for it. Like this:
class Coupon {
private var handler: ((coupon: Coupon) -> ())?
class func createCoupon(id: Int, completionHandler: ((coupon: Coupon) -> ())?) {
let coupon = Coupon(id: id)
// Store the handler in coupon
coupon.handler = completionHandler
}
//...
func connectionDidFinishLoading(connection: NSURLConnection) {
//...Setup coupon properties
handler?(coupon: self)
handler = nil
}
}
Then you can create and use your coupon like this:
Coupon.createCoupon(1, completionHandler: { (coupon) -> () in
// Do your thing with fully "inited" coupon
})
Of course, you also need to consider the situation of connection failed to your server, and maybe call the handler with an error, which does not present in your current code.

Related

Where to store Decoded JSON array from server and how to access it globally in viewControllers?

Currently im creating application which parses JSON from my server. From server I can receive array with JSON models.
Data from this array must be populated in table View.
My question Is simple: where to store decoded array from server, if I want to access it from many viewControllers in my application?
Here is my JSON model, which coming from server.
import Foundation
struct MyModel: Codable {
var settings: Test?
var provider: [Provider]
}
extension MyModel {
struct setting: Codable {
var name: String
var time: Int
}
}
here is how I am decoding it
import Foundation
enum GetResourcesRequest<ResourceType> {
case success([ResourceType])
case failure
}
struct ResourceRequest<ResourceType> where ResourceType: Codable {
var startURL = "https://myurl/api/"
var resourceURL: URL
init(resourcePath: String) {
guard let resourceURL = URL(string: startURL) else {
fatalError()
}
self.resourceURL = resourceURL.appendingPathComponent(resourcePath)
}
func fetchData(completion: #escaping
(GetResourcesRequest<ResourceType>) -> Void ) {
URLSession.shared.dataTask(with: resourceURL) { data, _ , _ in
guard let data = data else { completion(.failure)
return }
let decoder = JSONDecoder()
if let jsonData = try? decoder.decode([ResourceType].self, from: data) {
completion(.success(jsonData))
} else {
completion(.failure)
}
}.resume()
}
}
This is an example of CategoriesProvider. It just stores categories in-memory and you can use them across the app. It is not the best way to do it and not the best architecture, but it is simple to get started.
class CategoriesProvider {
static let shared = CategoriesProvider()
private(set) var categories: [Category]?
private let categoryRequest = ResourceRequest<Category>(resourcePath: "categories")
private let dataTask: URLSessionDataTask?
private init() {}
func fetchData(completion: #escaping (([Category]?) -> Void)) {
guard categories == nil else {
completion(categories)
return
}
dataTask?.cancel()
dataTask = categoryRequest.fetchData { [weak self] categoryResult in
var fetchedCategories: [Category]?
switch categoryResult {
case .failure:
print("error")
case .success(let categories):
fetchedCategories = categories
}
DispatchQueue.main.async {
self?.categories = fetchedCategories
completion(fetchedCategories)
}
}
}
}
I suggest using URLSessionDataTask in order to cancel a previous task. It could happen when you call fetchData several times one after another. You have to modify your ResourceRequest and return value of URLSession.shared.dataTask(...)
Here more details about data task https://www.raywenderlich.com/3244963-urlsession-tutorial-getting-started#toc-anchor-004 (DataTask and DownloadTask)
Now you can fetch categories in CategoriesViewController in this way:
private func loadTableViewData() {
CategoriesProvider.shared.fetchData { [weak self] categories in
guard let self = self, let categories = categories else { return }
self.categories = categories
self.tableView.reloadData()
}
}
In the other view controllers, you can do the same but can check for the 'categories' before making a fetch.
if let categories = CategoriesProvider.shared.categories {
// do something
} else {
CategoriesProvider.shared.fetchData { [weak self] categories in
// do something
}
}
If you really want to avoid duplicate load data() calls, your simplest option would be to cache the data on disk (CoreData, Realm, File, etc.) after parsing it the first time.
Then every ViewController that needs the data, can just query your storage system.
Of course the downside of this approach is the extra code you'll have to write to manage the coherency of your data to make sure it's properly managed across your app.
make a global dictionary array outside any class to access it on every viewcontroller.

Swift completion handler for Alamofire seemingly not executing

I have the following function in a class in my program:
func getXMLForTrips(atStop: String, forRoute: Int, completionHandler: #escaping (String) -> Void) {
let params = [api key, forRoute, atStop]
Alamofire.request(apiURL, parameters: params).responseString { response in
if let xmlData = response.result.value {
completionHandler(xmlData)
} else {
completionHandler("Error")
}
}
}
In the init() for the class, I attempt to call it like this:
getXMLForTrips(atStop: stop, forRoute: route) { xmlData in
self.XMLString = xmlData
}
This compiles without errors, but after init() is executed, my class's self.XMLString is still nil (shown both by the Xcode debugger and by my program crashing due to the nil value later on). I see no reason why this shouldn't work. Can anyone see what I am missing?
You shouldn't be making internet calls in the initializer of a class. You will reach the return of the init method before you go into the completion of your internet call, which means it is possible that the class will be initialized with a nil value for the variable you are trying to set.
Preferably, you would have another class such as an API Client or Data Source or View Controller with those methods in it. I am not sure what your class with the init() method is called, but lets say it is called Trips.
class Trips: NSObject {
var xmlString: String
init(withString xml: String) {
xmlString = xml
}
}
Then one option is to put the other code in whatever class you are referencing this object in.
I'm gonna use a view controller as an example because I don't really know what you are working with since you only showed two methods.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//setting some fake variables as an example
let stop = "Stop"
let route = 3
//just going to put that method call here for now
getXMLForTrips(atStop: stop, forRoute: route) { xmlData in
//initialize Trip object with our response string
let trip = Trip(withString: xmlData)
}
}
func getXMLForTrips(atStop: String, forRoute: Int, completionHandler: #escaping (String) -> Void) {
let params = [api key, forRoute, atStop]
Alamofire.request(apiURL, parameters: params).responseString { response in
if let xmlData = response.result.value {
completionHandler(xmlData)
} else {
completionHandler("Error")
}
}
}
}
If you want to be able to initialize the class without requiring setting the xmlString variable, you can still do the same thing.
Change the Trips class init() method to whatever you need it to be and set var xmlString = "" or make it optional: var xmlString: String?.
Initialize the class wherever you need it initialized, then in the completion of getXMLForTrips, do trip.xmlString = xmlData.

closures in swift are doing whatever. My code execution isn't acting as is supposed

In app delegate, after to get the coordinates of the user with core location, I want to make two api calls. One is to my server, to get a slug of the city name in which we are. The call is async so I want to load all the content into a global variable array before make the second call to google maps api, to get the city name from google, also with an async call. And finally after I have loaded all the google data, I want to compare the two arrays, with the city names to find a coincidence. To do that, I need the first two operation to have ended. For this I'm using closures, to ensure all the data is loaded before the next operation start. But when I launch my program, it doesn't find any coincidence between the two arrays and when I set breakpoints, I see the second array (google) is loaded after the comparison is made, which is very frustrating because I've set a lot of closures, and at this stage I'm not able to find the source of my issue. Any help would be appreciated.
this is app delegate:
let locationManager = CLLocationManager()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
//Language detection
let pre = NSLocale.preferredLanguages()[0]
print("language= \(pre)")
//Core Location
// Ask for Authorisation from the User.
self.locationManager.requestAlwaysAuthorization()
//Clore Location
// For use in foreground
self.locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
//Load cities slug via api call
let apiCall : webApi = webApi()
apiCall.loadCitySlugs(){(success) in
//Slug loaded in background
//Call google api to compare the slug
apiCall.loadGoogleContent(){(success) in //this function is called after compareGoogleAndApiSlugs()
apiCall.compareGoogleAndApiSlugs() //this one called before
}
}
}
return true
}
This is my global variables swift file:
import Foundation
import CoreLocation
let weatherApiKey : String = "" //weather api key
var globWeatherTemp : String = ""
var globWeatherIcon : String = ""
var globCity : String = ""
var globCountry : String = ""
let googleMapsApiKey : String = ""
let googlePlacesApiKey : String = ""
var TableData:Array< String > = Array < String >()
var nsDict = []
var locValue : CLLocationCoordinate2D = CLLocationCoordinate2D()
typealias SuccessClosure = (data: String?) -> (Void)
typealias FinishedDownload = () -> ()
typealias complHandlerAsyncCall = (success : Bool) -> Void
typealias complHandlerCitySlug = (success:Bool) -> Void
typealias complHandlerAllShops = (success:Bool) -> Void
typealias googleCompareSlugs = (success:Bool) -> Void
var flagCitySlug : Bool?
var flagAsyncCall : Bool?
var flagAllShops : Bool?
var values : [JsonArrayValues] = []
var citySlug : [SlugArrayValues] = []
var asyncJson : NSMutableArray = []
let googleJson : GoogleApiJson = GoogleApiJson()
this is the first function called in app delegate, which make a call to my server to load the city slug:
func loadCitySlugs(completed: complHandlerCitySlug){
//Configure Url
self.setApiUrlToGetAllSlugs()
//Do Async call
asyncCall(userApiCallUrl){(success)in
//Reset Url Async call and Params
self.resetUrlApi()
//parse json
self.parseSlugJson(asyncJson)
flagCitySlug = true
completed(success: flagCitySlug!)
}
}
This is the second function, it load google content but it's called after compareGoogleAndApiSlugs() and it's supposed to be called before...
/*
Parse a returned Json value from an Async call with google maps api Url
*/
func loadGoogleContent(completed : complHandlerAsyncCall){
//Url api
setGoogleApiUrl()
//Load google content
googleAsyncCall(userApiCallUrl){(success) in
//Reset API URL
self.resetUrlApi()
}
flagAsyncCall = true // true if download succeed,false otherwise
completed(success: flagAsyncCall!)
}
And finally the async calls, there are two but they are almost the same code:
/**
Simple async call.
*/
func asyncCall(url : String, completed : complHandlerAsyncCall)/* -> AnyObject*/{
//Set async call params
let request = NSMutableURLRequest(URL: NSURL(string: url)!)
request.HTTPMethod = "POST"
request.HTTPBody = postParam.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
guard error == nil && data != nil else {
// check for fundamental networking error
print("error=\(error)")
return
}
if let httpStatus = response as? NSHTTPURLResponse where httpStatus.statusCode != 200 {
// check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(response)")
}
let responseString = NSString(data: data!, encoding: NSUTF8StringEncoding)
asyncJson = responseString!.parseJSONString! as! NSMutableArray
flagAsyncCall = true // true if download succeed,false otherwise
completed(success: flagAsyncCall!)
}
task.resume()
}
If anyone can see the issue or throw some light it would be very appreciated.
The problem is this function:
func loadGoogleContent(completed : complHandlerAsyncCall){
setGoogleApiUrl()
googleAsyncCall(userApiCallUrl){(success) in
self.resetUrlApi()
}
flagAsyncCall = true
completed(success: flagAsyncCall!) //THIS LINE IS CALLED OUTSIDE THE googleAsyncCall..
}
The above completion block is called outside of the googleAsyncCall block.
The code should be:
func loadGoogleContent(completed : complHandlerAsyncCall){
setGoogleApiUrl()
googleAsyncCall(userApiCallUrl){(success) in
self.resetUrlApi()
flagAsyncCall = true
completed(success: flagAsyncCall!)
}
}
Btw.. your global variables are NOT atomic.. so be careful.

Wait for multiple Alamofire request

I'm trying to add data to my data model so to test it I'm printing the info fetched via Alamofire but my problem is since some data needs to call the api again it becomes null when I print it. Here's my code
Code for getting the person's data
func printAPI(){
swApiHandler.requestSWPApi("http://swapi.co/api/people", completionHandler: {(response, error) in
let json = JSON(response!)
let jsonResult = json["results"]
for (index,person):(String, JSON) in jsonResult{
let name = person["name"].stringValue
let height = person["height"].intValue
let mass = person["mass"].intValue
let hairColor = person["hair_color"].stringValue
let skinColor = person["skin_color"].stringValue
let eyeColor = person["eye_color"].stringValue
let birthYear = person["birth_year"].stringValue
let gender = person["gender"].stringValue
let homeWorldUrl = person["homeworld"].stringValue
let homeWorldNameKey = "name"
let homeWorld = self.getSWApiSpecificValue(homeWorldUrl, strKey: homeWorldNameKey)
print("Name: \(name)")
print("Height: \(height)")
print("Mass: \(mass)")
print("Hair Color: \(hairColor)")
print("Skin Color: \(skinColor)")
print("Eye Color: \(eyeColor)")
print("Birth Year: \(birthYear)")
print("Gender: \(gender)")
print("Home World: \(homeWorld)")
print("------------------------------")
}
})
}
Code for getting the specific value
func getSWApiSpecificValue(strUrl: String, strKey: String) -> String{
var name = ""
swApiHandler.requestSWPApi(strUrl, completionHandler: {(response,error) in
let json = JSON(response!)
print(json[strKey].stringValue)
name = json[strKey].stringValue
})
return name
}
If you want to know the JSON Model here it is
And for running the code here's the output
You should make your api call in background and after it's finished populate your data on main queue.
Just change your code to get specific value to this one:
func getSWApiSpecificValue(strUrl: String, strKey: String) -> String{
var name = ""
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) { () -> Void in
swApiHandler.requestSWPApi(strUrl, completionHandler: {(response,error) in
dispatch_async(dispatch_get_main_queue()) {
let json = JSON(response!)
print(json[strKey].stringValue)
name = json[strKey].stringValue
return name
}
})
}
}
In code above first you get make a request to the server in background and if you get response in main queue will populate you variable name.
Also it's better to change your api call function to something like that:
func getDataFromServer(ulr: String, success: (([AnyObject]) -> Void)?, failure: (error: ErrorType) -> Void){
}
In this way you can handle your errors and if success get your data.

Populate array of custom classes from two different network requests

In my Swift iOS project, I am trying to populate an array of custom class objects using JSON data retrieved with Alamofire and parsed with SwiftyJSON. My problem, though, is combining the results of two different network request and then populating a UITableView with the resulting array.
My custom class is implemented:
class teamItem: Printable {
var name: String?
var number: String?
init(sqljson: JSON, nallenjson: JSON, numinjson: Int) {
if let n = sqljson[numinjson, "team_num"].string! as String! {
self.number = n
}
if let name = nallenjson["result",0,"team_name"].string! as String! {
self.name = name
}
}
var description: String {
return "Number: \(number) Name: \(name)"
}
}
Here is my viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
refresh() {
() -> Void in
self.tableView(self.tableView, numberOfRowsInSection: self.teamsArr.count)
self.tableView.reloadData()
for item in self.teamsArr {
println(item)
}
return
}
self.tableView.reloadData()
}
which goes to the refresh() method:
func refresh(completionHandler: (() -> Void)) {
populateArray(completionHandler)
}
and finally, populateArray():
func populateArray(completionHandler: (() -> Void)) {
SqlHelper.getData("http://cnidarian1.net16.net/select_team.php", params: ["team_num":"ALL"]) {
(result: NSData) in
let jsonObject : AnyObject! = NSJSONSerialization.JSONObjectWithData(result, options: NSJSONReadingOptions.MutableContainers, error: nil)
let json = JSON(jsonObject)
self.json1 = json
println(json.count)
for var i = 0; i < json.count; ++i {
var teamnum = json[i,"team_num"].string!
NSLog(teamnum)
Alamofire.request(.GET, "http://api.vex.us.nallen.me/get_teams", parameters: ["team": teamnum])
.responseJSON { (req, res, json, err) in
let json = JSON(json!)
self.json2 = json
self.teamsArr.append(teamItem(sqljson: self.json1, nallenjson: self.json2, numinjson: i))
}
}
completionHandler()
}
}
the first problem I had was that i in the for loop reached 3 and caused errors when I thought it really shouldn't because that JSON array only contains 3 entries. My other main problem was that the table view would be empty until I manually triggered reloadData() with a reload button in my UI, and even then there were problems with the data in the tables.
really appreciate any assistance, as I am very new to iOS and Swift and dealing with Alamofire's asynchronous calls really confused me. The code I have been writing has grown so large and generated so many little errors, I thought there would probably be a better way of achieving my goal. Sorry for the long-winded question, and thanks in advance for any responses!
The Alamofire request returns immediately and in parallel executes the closure, which will take some time to complete. Your completion handler is called right after the Alamofire returns, but the data aren't yet available. You need to call it from within the Alamofire closure - this ensures that it is called after the data became available.

Resources