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!
Related
I'm trying to make a request (GET) to a REST API. This API returns airport weather reports in JSON format.
What I am trying to achieve is the following: The user inputs the identification code of an airport, which gets amended to the url. When he/she presses the 'Request' button, a GET request is made to the API which will then show the result in a UILabel.
I've come so far as being able to make the call using a button, and the response from the server is being printed in the console, but I can't get it printed in the UILabel.
This is my code so far:
//
// ViewController.swift
// urltest
//
// Created by Stefan Oomen on 06/05/2020.
// Copyright © 2020 FlyTechSoft. All rights reserved.
//
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var metarResponse: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
#IBAction func onButtonClick(_ sender: UIButton) {
let url = URL(string: "hhttps://website.rest/api/metar/location?options=&airport=true&reporting=true&format=json&onfail=cache")!
var request = URLRequest(url: url)
request.addValue("My_API_KEY", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let response = response {
print(response)
if let data = data, let body = String(data: data, encoding: .utf8) {
print(body)
}
} else {
print(error ?? "Unknown error")
}
}
task.resume()
}
}
In the GET URL I only need to change the 'Location?' portion, using the input from the user from the 'icaoTextField' UITextField:
https://website.rest/api/metar/location?options=&airport=true&reporting=true&format=json&onfail=cache
I'm quite new to this, so all help is very welcome.
Thank you!
PS. Small subquestion: is it possible to select and use only specific portions of a REST API call's result?
The function metarResponse which is setting the UILabel's text value never gets called anywhere.
Try replacing -
func metarResponse() {
self.metarresponse.text = response as? String
}
with -
self.metarresponse.text = response ?? "Something went wrong"
is it possible to select and use only specific portions of a REST API call's result?
Certainly. You need to parse the result and extract the desired key value pairs. API responses are usually in the JSON fromat and you could pretty much extract any value out of it, if you know how to loop through and access elements of an array/dictionary.
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.
Problem: When using Alamofire and SwiftyJSON to populate a UITableView, the table view loads but there is a 1 second pause before the data is displayed. I am not sure where I should be calling reloadData() to fix this.
There are various questions about when to call reloadData() using Alamofire and SwiftyJSON to populate a UITableView, but I have yet to find an answer that solves my problem.
A bit of background:
I am using the Google Places Web API to populate a UITableView with Google Place names, addresses and icons. Originally I was only using SwiftyJSON to accomplish this, but it was taking quite some time for the UITableView to load. Here is some of that code:
GooglePlacesRequest.swift:
...
var placesNearbyArray: [GooglePlaceNearby]?
if let url = NSURL(string: urlString) {
if let data = NSData(contentsOfURL: url, options: .allZeros, error: nil) {
let json = JSON(data: data)
placesNearbyArray = parseNearbyJSON(json)
completion(placesNearbyArray)
} else {
completion(placesNearbyArray)
}
} else {
completion(placesNearbyArray)
}
}
func parseNearbyJSON(json: JSON) -> [GooglePlaceNearby] {
var placesNearbyArray = [GooglePlaceNearby]()
for result in json["results"].arrayValue {
let name = result["name"].stringValue
let address = result["vicinity"].stringValue
...
placesNearbyArray.append(place)
}
return placesNearbyArray
}
And the code from viewWillAppear in the UITableView:
override func viewWillAppear(animated: Bool) {
...
placesRequest.getPlacesNearUserLocation(location, completion: { (googlePlaces) -> Void in
if let googlePlaces = googlePlaces {
self.venues = googlePlaces
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
})
}
As I said above, this approach was working, but the UITableView would often take 3 - 4 seconds to load. I then decided to use Alamofire along with SwiftyJSON and (posssibly) a cache.
Here are some snippets of the current code:
GooglePlacesRequest.swift:
...
var placesNearbyArray: [GooglePlaceNearby]?
request(.GET, urlString).responseSwiftyJSON({ (_, _, json, error) in
var innerPlacesArray = [GooglePlaceNearby]()
for result in json["results"].arrayValue {
let name = result["name"].stringValue
let address = result["vicinity"].stringValue
...
innerPlacesArray.append(place)
}
placesNearbyArray = innerPlacesArray
completion(placesNearbyArray)
})
completion(placesNearbyArray)
}
After adding this, I tried to use the same code from viewWillAppear in the UITableView, but this is what happens:
The UITableView loads much faster
There is then a 1 second pause before the table view cells are populated.
I have tried to place the new request function in various places in the UITableView, such as in the viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
...
let placesRequest = PlacesRequest()
placesRequest.fetchNearbyPlaces(location, completion: { (googlePlaces) -> Void in
if let googlePlaces = googlePlaces {
self.venues = googlePlaces
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}
)
And I have also tried to call reloadData() on the main thread in both viewWillLoad and viewWillAppear.
The result is always the same...there is a 1 second pause before the UITableView loads the data.
Before I even implement my cache, I need to figure out how (and where) to properly make my request. Can anyone assist me with this?
I am attempting to load a lunch menu PDF into a web view for a high school app that I am updating. Currently, it can load a PDF into the web view and display it just fine, but I want to speed up the monthly update process by having my app receive the link through Parse (Which I can update much quicker than updating the link in the app itself with Apple's 7 day review period), and then load the PDF. Currently, with what I have put together, my app will not load the PDF. Here's the entire view:
import UIKit
class AlaCarte_ViewController: UIViewController {
#IBOutlet weak var webviewAlaCarte: UIWebView!
var urlpath = String()
func loadAddressUrl(){
let requestURL = NSURL (string:urlpath)
let request = NSURLRequest(URL: requestURL!)
webviewAlaCarte.loadRequest(request)
alaCarteUpdate()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
clearPDFBackground(self.webviewAlaCarte)
}
func clearPDFBackground(webView: UIWebView) {
var view :UIView?
view = webView as UIView
while view != nil {
if NSStringFromClass(view?.dynamicType) == "UIWebPDFView" {
view?.backgroundColor = UIColor.clearColor()
}
view = view?.subviews.first as! UIView?
}
}
func alaCarteUpdate() {
var query = PFQuery(className: "AlaCarte")
query.getObjectInBackgroundWithId("rT7MpEFySU") {(AlaCarte: PFObject!, error: NSError!)-> Void in
if error == nil && AlaCarte != nil {
println(AlaCarte)
} else {
println(error)
}
let AlaCarteLink = AlaCarte["webaddress"] as! String
self.urlpath = AlaCarteLink
}
}
override func viewDidLoad() {
super.viewDidLoad()
loadAddressUrl()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
The link is stored in my Parse app as "webaddress" and does not contain end quotations. Adding them does not help. Any ideas?
It looks to me like you're not telling the web view to load the URL once it's retrieved from Parse.
Try adding the following lines after self.urlpath = AlaCarteLink in alaCarteUpdate().
let requestURL = NSURL (string:self.urlpath)
let request = NSURLRequest(URL: requestURL!)
self.webviewAlaCarte.loadRequest(request)
I think it would also be a good idea to add a function that specifically loads a url string into your web view, so you can call it from both inside alaCarteUpdate(), and loadAddressUrl(), and avoid the duplicate 3x lines. I've assumed that you're loading the URL in loadAddressURL() so that you can show a local/cached document while retrieving the latest from Parse.
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.