I am trying to make an API call in my Swift project. I just started implementing it and i am trying to return a Swift Dictionary from the call.
But I think i am doing something wrong with the completion handler!
I am not able to get the returning values out of my API call.
import UIKit
import WebKit
import SafariServices
import Foundation
var backendURLs = [String : String]()
class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
#IBOutlet var containerView : UIView! = nil
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
}
print(backendURLs)
}
func getBackendURLs(completion: #escaping (NSArray) -> ()) {
let backend = URL(string: "http://example.com")
var json: NSArray!
let task = URLSession.shared.dataTask(with: backend! as URL) { data, response, error in
guard let data = data, error == nil else { return }
do {
json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? NSArray
completion(json)
} catch {
#if DEBUG
print("Backend API call failed")
#endif
}
}
task.resume()
}
func extractJSON(JSON : NSArray) -> [String : String] {
var URLs = [String : String]()
for i in (0...JSON.count-1) {
if let item = JSON[i] as? [String: String] {
URLs[item["Name"]! ] = item["URL"]!
}
}
return URLs
}
}
The first print() statements gives me the correct value, but the second is "nil".
Does anyone have a suggestion on what i am doing wrong?
Technically #lubilis has answered but I couldn't fit this inside a comment so please bear with me.
Here's your viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
}
print(backendURLs)
}
What will happen is the following:
viewDidLoad is called, backendURLs is nil
you call getBackendURLs, which starts on another thread in the background somewhere.
immediately after that your code continues to the outer print(backendURLs), which prints nil as backendURLs is still nil because your callback has not been called yet as getBackendURLs is still working on another thread.
At some later point your getBackendURLs finishes retrieving data and parsing and executes this line completion(json)
now your callback is executed with the array and your inner print(backendURLs) is called...and backendURLs now has a value.
To solve your problem you need to refresh your data inside your callback method.
If it is a UITableView you could do a reloadData() call, or maybe write a method that handles updating the UI for you. The important part is that you update the UI inside your callback, because you don't have valid values until then.
Update
In your comments to this answer you say:
i need to access the variable backendURLs right after the completionHandler
To do that you could make a new method:
func performWhateverYouNeedToDoAfterCallbackHasCompleted() {
//Now you know that backendURLs has been updated and can work with them
print(backendURLs)
//do what you must
}
In the callback you then send to your self.getBackendURLs, you invoke that method, and if you want to be sure that it happens on the main thread you do as you have figured out already:
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
DispatchQueue.main.async {
self.performWhateverYouNeedToDoAfterCallbackHasCompleted()
}
}
Now your method is called after the callback has completed.
As your getBackendURLs is an asynchronous method you can not know when it has completed and therefore you cannot expect values you get from getBackedURLs to be ready straight after calling getBackendURLs, they are not ready until getBackendURLs has actually finished and is ready to call its callback method.
Hope that makes sense.
Related
I try to decode weather api
this is my struct class weatherModal :
import Foundation
struct WeatherModel:Decodable{
var main:Main?
}
struct Main:Decodable {
var temp : Double?
var feels_like : Double?
var temp_min:Double?
var temp_max:Double?
var pressure , humidity: Int?
}
I am trying to learn protocols. So this is where a make api call manager class :
protocol WeatherManagerProtocol:AnyObject {
func weatherData(weatherData:WeatherModel)
}
class WeatherManager{
var weather : WeatherModel?
weak var delegate :WeatherManagerProtocol?
public func callWeather(city:String) {
let url = "http://api.openweathermap.org/data/2.5/weather?q=\(city)&appid=1234"
URLSession.shared.dataTask(with:URL(string: url)!) { (data, response, err) in
if err != nil {
print(err!.localizedDescription)
} else {
do {
self.weather = try JSONDecoder().decode(WeatherModel.self, from: data!)
self.delegate?.weatherData(weatherData: self.weather!)
} catch {
print(error.localizedDescription)
}
}
}.resume()
}
}
In my ViewController what I want to do is user write city name on textfield and If user clicked the process button print the information about weather.
class ViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
var weatherManager = WeatherManager()
var data : WeatherModel?
override func viewDidLoad() {
super.viewDidLoad()
weatherManager.delegate = self
// Do any additional setup after loading the view.
}
#IBAction func processButtonClicked(_ sender: Any) {
if textField.text != "" {
weatherManager.callWeather(city: textField.text ?? "nil")
print(data?.main?.humidity) // it print nil
} else{
print("empty")
}
}
extension ViewController: WeatherManagerProtocol{
func weatherData(weatherData: WeatherModel) {
self.data = weatherData
print(self.data.main)
// in here I can show my data
}
}
When I clicked process button it always print nil. Why ? What am I doing wrong?
You seem not to understand how your own code is supposed to work. The whole idea of the protocol-and-delegate pattern you've set up is that the "signal" round-trips thru the weather manager on a path like this:
You (the ViewController) say weatherManager.callWeather
The weather manager does some networking.
The weather manager calls its own delegate's weatherData.
You (the ViewController) are that delegate, so your weatherData is called and that is where you can print.
So that is the signal path:
#IBAction func processButtonClicked(_ sender: Any) {
weatherManager.callWeather(city: textField.text ?? "nil") // < TO wm
}
func weatherData(weatherData: WeatherModel) { // < FROM wm
// can print `weatherData` here
}
You cannot short circuit this path by trying to print the weather data anywhere else. Stay on the path. You cannot turn this into a "linear" simple path; it is asynchronous.
If you do want it to look more like a "linear" simple path, use a completion handler instead of a delegate callback. That's what I do in my version of this same experiment, so my view controller code looks like this:
self.jsonTalker.fetchJSON(zip:self.currentZip) { result in
DispatchQueue.main.async {
// and now `result` contains the weather data, or an error
Even better, use the Combine framework (or wait until Swift 6 implements async/await).
You call to weatherManager.callWeather that start URLSession.shared.dataTask. the task is executed asynchronously, but callWeather return immediately. So, when you print data?.main?.humidity, the task did not finish yet, and data is still nil. After the task finish, you call weatherData and assign the response to data.
I'm working on an app in Swift 2.2.
I created a tabbed application from the initial template. I then added some code to my FirstViewController class, in order to download, parse, and display some JSON-formatted data.
This is the contents of my FirstViewController.swift file:
import UIKit
class FirstViewController: UIViewController {
#IBOutlet weak var siteNameLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var siteName = ""
let requestURL: NSURL = NSURL(string: "http://wheretogo.com.mx/backends/service.php")!
let urlRequest: NSMutableURLRequest = NSMutableURLRequest(URL: requestURL)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(urlRequest) {
(data, response, error) -> Void in
let httpResponse = response as! NSHTTPURLResponse
let statusCode = httpResponse.statusCode
if (statusCode == 200) {
//print("Everyone is fine, file downloaded successfully.")
do{
let json = try NSJSONSerialization.JSONObjectWithData(data!, options:.AllowFragments)
if let appInfo = json["appInfo"] as? [[String: AnyObject]] {
siteName = appInfo[0]["siteName"] as! String
self.siteNameLabel.text = siteName
}
}catch {
print("Error with Json: \(error)")
}
}
}
task.resume()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Fetching and parsing the data actually works. The problem is that the data is only displayed when I select the second tab and then go back to the first.
When it loads the label, it has the default text and it changes when I go to another tab and come back to the first, then the self.siteNameLabel.text = siteName.
I think an init can solve it, but I don't know how, or maybe a delay?
Or how can I handle the downloading data before my first view controller load.
Thanks in advance.
This may have something to do with the thread you use to update your UI. Note that the completion block utilised by dataTaskWithRequest is performed on a background thread.
Since Apple only allows UI updates to be done on the main thread, you can solve this issue by using a dispatch block like so (as mentioned here):
dispatch_async(dispatch_get_main_queue(), {
self.siteNameLabel.text = siteName
})
Also, note that if you want the data that your UI depends on to update each time FirstViewController is shown, you should have your code in viewDidAppear rather than viewDidLoad.
Hope this helps!
I am trying to call a function reloadTable in my HomeViewController from my Task class. But I keep being thrown an
Use of instance member 'reloadTable' on type 'HomeViewController'; did you mean to use a value of type 'HomeViewController' instead?
This is my HomeViewController code:
import UIKit
import Alamofire
import SwiftyJSON
class HomeViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
func reloadTable() {
self.displayTask.reloadData()
}
}
This is my Tasks class:
import Foundation
import Alamofire
import SwiftyJSON
class Tasks {
static let sharedInstance = Tasks()
var datas: [JSON] = []
func getTaskDetails(){
Alamofire.request(.GET, Data.weeklyEndpoint).validate().responseJSON { response in
switch response.result {
case .Success(let data):
let json = JSON(data)
if let buildings = json.array {
for building in buildings {
if let startDate = building["start_date"].string{
print(startDate)
}
if let tasks = building["tasks"].array{
Tasks.sharedInstance.datas = tasks
HomeViewController.reloadTable()
for task in tasks {
if let taskName = task["task_name"].string {
print(taskName)
}
}
}
}
}
case .Failure(let error):
print("Request failed with error: \(error)")
}
}
}
// for prevent from creating this class object
private init() { }
}
In this case reloadTable() is an instance method. You can't call it by class name, you have to create object for HomeViewController and then you have to call that method by using that object.
But in this situation no need to call the method directly by using HomeViewController object. You can do this in another way by using NSNotification
Using NSNotification :
Add a notification observer for your HomeViewController
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(HomeViewController.reloadTable), name:"ReloadHomeTable", object: nil)
Add the above lines in your viewDidLoad method of HomeViewController
Now replace this line HomeViewController.reloadTable() by NSNotificationCenter.defaultCenter().postNotificationName("ReloadHomeTable", object: nil) in your Task class
As many of the other answers states, you are trying to access a non-static function on a static reference.
I would suggest the use of closures, consider the following;
Create a closure for .Success and the .Failure, this enables you to act accordingly to the data request result. The onError closure is defined as an optional, this means you don't have to implement the onError in the ´getTaskDetails` function call, implement it where you feel you need the error information.
func getTaskDetails(onCompletion: ([Task]) -> (), onError: ((NSError) -> ())? = nil) {
Alamofire.request(.GET, Data.weeklyEndpoint).validate().responseJSON {
response in
switch response.result {
case .Success(let data):
let json = JSON(data)
if let buildings = json.array {
for building in buildings {
if let startDate = building["start_date"].string{
print(startDate)
}
if let tasks = building["tasks"].array{
Tasks.sharedInstance.datas = tasks
onCompletion(tasks)
}
}
}
case .Failure(let error):
print("Request failed with error: \(error)")
onError?(error)
}
}
}
From your HomeViewController:
// Call from wherever you want to reload data.
func loadTasks(){
// With error handling.
// Fetch the tasks and reload data.
Tasks.sharedInstance.getTaskDetails({
tasks in
self.displayTask.reloadData()
}, onError: {
error in
// Handle error, display a message or something.
print(error)
})
// Without error handling.
// Fetch the tasks and reload data.
Tasks.sharedInstance.getTaskDetails({
tasks in
self.displayTask.reloadData()
})
}
Basics
Closures
Make class Task as Struct, make function getTaskDetails as static and try to call function getTaskDetails() in HomeViewController. In result this of this function use realoadTable()
In HomeViewController you have defined reloadTable() as instance method not the class function.You should call reloadTable() by making instance of HomeViewController as HomeViewController(). Replace
HomeViewController.showLeaderboard()
HomeViewController().showLeaderboard()
Hope it helps. Happy Coding.
i requested data from webserver by alamofire. i want to pass data to viewdidload but data in viewdidload is empty, please help me explain. thanks and sory for my english. this my code
class LiveScoreViewController: UIViewController
{
var matchData : JSON! = []
func loadLiveScore(section: String){
DNService.getLiveScore(section) { (JSON) -> () in
self.matchData = JSON[ ]
self.matchData = self.matchData["match"]
//print(self.matchData) -> is ok
}
}
override func viewDidLoad() {
super.viewDidLoad()
loadLiveScore("LiveScore")
//print(self.matchData) -> is empty
}}
If DNService.getLiveScore is a webservice call, then you wont be able to get the matchData inside viewDidLoad, since the webservice call will take some time to complete, whatever you are trying to do with the matchData should be done in the completion block of DNService.getLiveScore most likely
If you want, you can put a print statement right after loadLiveScore in viewDidLoad and also in the completion block, and you will see the order of execution of the print statements is not what you are expecting
getLiveScore is asynchronous method. So you have to use completion handler to get the response. Make an handler for loadLiveScore
func loadLiveScore(section: String), handler: (JSON) -> ()) {
DNService.getLiveScore(section) { (JSON) -> () in
handler(JSON)
}
}
Call the method from your viewDidLoad as like:
override func viewDidLoad() {
super.viewDidLoad()
loadLiveScore("LiveScore") { json in
print(json) // parse JSON as you need
self.matchData = json["match"]
}
}}
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.