I am attempting to take a string from JSON data and set it to a variable. My problem is that the variable shows as empty. I am using JSONDecoder to retrieve the JSON data and setting the string to a variable outside of the function. I then want to use that variable inside of another function
When I print the variable it still shows up as blank even after the function has loaded. Within the function the string appears correctly.
Code:
var filmTitle = ""
override func viewDidLoad() {
super.viewDidLoad()
loadFilms()
print(self.filmTitle) //Prints as an empty string
}
func loadFilms() {
let id = filmId
let apiKey = "97a0d64910120cbeae9df9cb675ad235"
let url = URL(string: "https://api.themoviedb.org/3/movie/\(id)?api_key=\(apiKey)&language=en-US")
let request = URLRequest(
url: url! as URL,
cachePolicy: URLRequest.CachePolicy.reloadIgnoringLocalCacheData,
timeoutInterval: 10 )
let session = URLSession (
configuration: URLSessionConfiguration.default,
delegate: nil,
delegateQueue: OperationQueue.main
)
let task = session.dataTask(with: request, completionHandler: { (dataOrNil, response, error) in
if let data = dataOrNil {
do { let details = try! JSONDecoder().decode(Details.self, from: data)
self.filmTitle = details.title
print(self.filmTitle) //string prints correctly
}
}
})
task.resume()
}
What am I missing to correctly set the string to the variable?
Loading data from the internet is an asynchronous method. The print statement is being called before loadFilms() has completed.
Use a callback to get the data after it has completed.
func loadFilms(completion: #escaping (Details?, Error?) -> Void) {
//...
let task = session.dataTask(with: request, completionHandler: { (dataOrNil, response, error) in
if let data = dataOrNil {
do { let details = try JSONDecoder().decode(Details.self, from: data)
completion(details, nil)
} catch {
completion(nil, error)
}
})
}
At the call site:
override func viewDidLoad() {
loadFilms { details, error in
if error { //* Handle Error */ }
self.filmTitle = details.title
print(filmTitle)
}
}
Web request are asynchronous and from the CP's perspective, take a long time to complete. When you call this:
override func viewDidLoad() {
super.viewDidLoad()
loadFilms()
print(self.filmTitle) // loadFilms() hasn't finished so `filmTitle` is empty
}
It's better to set a property observer on filmTitle:
var filmTitle: String? = nil {
didSet {
print(filmTitle)
Dispatch.main.async {
// update your GUI
}
}
}
The solution to this problem was to reload the collection view that the array was being sent to within the decoder function after the data was set to the array.
Related
I'm downloading remote JSON data and want my loading screen to stay up until the download is complete. Once my parse method finishes running, a segue should be called to move to the next view automatically.
I've verified that my data is properly downloading and parsing. My performSegue function is even being called when I throw up a breakpoint. But the application is still not moving to the next view.
Here's where I'm calling my parse method and then immediately calling the desired segue:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
downloadSources(atURL: "https://newsapi.org/v1/sources?language=en")
performSegue(withIdentifier: "loadingFinished", sender: self)
}
For reference, if you need it, here is my parse method in its entirety:
func downloadSources(atURL urlString: String) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
if let validURL = URL(string: urlString) {
var request = URLRequest(url: validURL)
request.setValue("49fcb8e0fa604e7aa461ee4f22124177", forHTTPHeaderField: "X-Api-Key")
request.httpMethod = "GET"
let task = session.dataTask(with: request) { (data, response, error) in
if error != nil {
assertionFailure()
return
}
guard let response = response as? HTTPURLResponse,
response.statusCode == 200,
let data = data
else {
assertionFailure()
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] {
guard let sources = json["sources"] as? [[String: Any]]
else {
assertionFailure()
return
}
for source in sources {
guard let id = source["id"] as? String,
let name = source["name"] as? String,
let description = source["description"] as? String
else {
assertionFailure()
return
}
self.sources.append(Source(id: id, name: name, description: description))
}
}
}
catch {
print(error.localizedDescription)
assertionFailure()
}
}
task.resume()
}
}
Thanks in advance.
Sounds like a closure callback is what you want.
typealias CompletionHandler = ((_ success:Bool) -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
downloadSources(atURL: "www.example.com", completion: {
if success {
performSegue(withIdentifier: "loadingFinished", sender: self)
return
}
// otherwise deal with failure
})
}
func downloadSources(atURL urlString: String, completion: CompletionHandler) {
if error != nil {
completion?(false)
return
}
// finish downlaod
completion?(true)
}
I have a server for storing username and password data in my application. When testing the app I had everything save to the device locally using NSDefaults, but now that the app is close to being fully launched, I am trying to save them to the server instead, as it is safer that way for the user's information.
When I had it save to NSDefaults, it was easy and short work. Now however, I am trying to POST the data to the server and keep getting build errors. What do I need to change for this to work? Am I not fully understanding how POST and GET works? Thanks. Using Swift 2 as of right now, not my choice, I prefer 3, but my boss isn't letting us update it yet.
The current error is coming from the POST USER DATA TO SERVER section, where xcode claims that userNmeTxt cannot be converted into NSData. Thank you in advance.
EDIT: Error is on line 87: "Cannot convert value of type UITextField! to expected argument type NSData!"
import UIKit
class UserNameViewController: AuthorizationViewController {
#IBOutlet weak var userNameTxt: UITextField!
#IBOutlet weak var continueBtn: UIButton!
var userModel: ProfileModel!
//MARK: - SYSTEMS METHODS
override func viewDidLoad() {
super.viewDidLoad()
userNameTxt.delegate = self
userNameTxt.autocapitalizationType = .Sentences
setEnabledButton()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBarHidden = false
self.navigationItem.leftBarButtonItem = getBackButton()
self.title = ""
}
override func viewWillDisappear(animated: Bool) {
self.navigationController?.navigationBarHidden = true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
continueBtn.layer.cornerRadius = 10
}
override func popToRoot(sender:UIBarButtonItem){
self.navigationController!.popViewControllerAnimated(true)
}
//MARK: - CHECK FOR AVALABILITY
func setEnabledButton(){
if userNameTxt.text == "" {
continueBtn.backgroundColor = UIColor.lightGrayColor()
} else {
continueBtn.backgroundColor = UIColor(colorLiteralRed: 63.0/255.0, green: 220.0/255.0, blue: 236.0/255.0, alpha: 1.0)
}
continueBtn.userInteractionEnabled = userNameTxt.text != ""
}
//MARK: - POST USER DATA TO SERVER
func postData(url: String, params: Dictionary<String, String>, completionHandler: (data: NSData?, response: NSURLResponse?, error: NSError?) -> ()) {
// Indicate download
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
let url = NSURL(string: "myPlaceholderURLgoesHere")!
// print("URL: \(url)")
let request = NSMutableURLRequest(URL: url)
let session = NSURLSession.sharedSession()
request.HTTPMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
// Verify downloading data is allowed
do {
request.HTTPBody = try NSJSONSerialization.dataWithJSONObject(params, options: [])
} catch let error as NSError {
print("Error in request post: \(error)")
request.HTTPBody = nil
} catch {
print("Catch all error: \(error)")
}
// Post the data
let task = session.dataTaskWithRequest(request) { data, response, error in
completionHandler(data: userNameTxt, response: userModel, error: error)
// Stop download indication
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
// Stop download indication
}
task.resume()
}
//MARK: - SEGUE
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "toPassword"{
let controller = segue.destinationViewController as! PasswordViewController
controller.userModel = userModel
}
}
//MARK: - IB ACTIONS
#IBAction func continuePressed(sender: AnyObject) {
userModel.userNickName = userNameTxt.text!
performSegueWithIdentifier("toPassword", sender: self)
}
}
extension UserNameViewController: UITextFieldDelegate{
func textFieldDidEndEditing(textField: UITextField) {
self.setEnabledButton()
}
}
There are a couple of things you need to change.
userNameTxt is not the username, it's the UITextField containing the username. The text you need is userNameTxt.text?
If the function is expecting Data, you have to convert your text to Data first
let task = session.dataTaskWithRequest(request) { data, response, error in
completionHandler(data: userNameTxt.text?.data(using: .utf8), response: userModel, error: error)
I assume you have to send the data to the server.
If you don't have too you can save the data in keychain access, see: SO: Keychain Access
In order to resolve the error please edit the question with the error message and line of code (if possible).
I would suggest that you use Alamofire for POST/GET (REST). To use Alamofire you need basic knowledge of Cocoapods. It's better in the long term.
NOTE: There can be two possible error outcomes when you make the request.
1) Incorrect data format or bug from your side
2) Server error due backend bug from server side.
The data can be sent from your device with POST where the data is in the BODY or HEADER of the request. Usually it is in the body (parameters in the alamofire methods).
Here is an example:
import Alamofire
...
// MARK:- Login Feature - Universal Met for login
internal static func loginWith(serverUrl: String, parameters: [String: AnyObject]?, headers: [String: String]?, notificationName: String, serviceType: LoginService)
{
Alamofire.request(.POST, serverUrl, parameters: parameters, headers: headers).responseJSON
{ (response) in
print("\n Login feature - \n")
print(" Login url - \(serverUrl)\n")
print(" Login parameters - \(parameters)\n")
print(" Login notificationName - \(notificationName)\n")
print(" Login response - \(response)\n")
EXCDataParserH.parseResponseFrom(ServiceType.Login(type: serviceType),
operation: nil,
data: response.data,
notification: notificationName)
}
}
Instead of writing the whole thing every time you make a server request, try to do as follows:
import Foundation
import SystemConfiguration
class HTTPHelper{
class func httpPostDataDic(postURL:NSURL,postString:NSString,completionHandler:#escaping (NSDictionary?, NSError?) -> Void ) -> URLSessionTask{
var responseResultData: NSDictionary = NSDictionary()
let request = NSMutableURLRequest(url:postURL as URL);
request.httpMethod = "POST";// Compose a query string
request.httpBody = postString.data(using: String.Encoding.utf8.rawValue);
print(request)
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if error != nil
{
print("error=\(error)")
completionHandler(nil, error as NSError?)
return
}
let responseString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
if let responseString = responseString {
print("responseString = \(responseString)")
}
do {
let myJSON = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary
responseResultData=myJSON!
completionHandler(responseResultData, nil)
} catch {
print(error)
}
}
task.resume()
return task
}
}
Now whenever you need to make a server POST request,in your ViewController class, do as follows:
//Requesting server
func requestServer() -> Void{
let postvariable = "Value"
let url = URL(string: "your url")!
let postString = "postkey=\(postvariable)"
HTTPHelper.httpPostDataDic(postURL: url as NSURL, postString: postString) {
(responseResult, error) -> Void in
DispatchQueue.main.async {
if error != nil{
print(error ?? "unknown")
}
else{
print(responseResult ?? "unknown result")
//Parse your response
self.parseResult(result: responseResult!);
}
}
}
}
May I ask you one thing that I didn't understand in your question.
How exactly would you save the login credentials in a server? I mean, if you save the login credentials in the server, how would you authenticate user access to these saved credentials?
i'm new on programming and of course swift. and i'm wondering how can i send back my request response to my viewDidLoad to to use there.
for requesting, i'm using alamofire and for json , swiftyJSON.
what i'm trying to do is to Get data(JSON) from server which contains titles and image urls. after that i'm trying to Get images from image urls.
in viewDidLad, i call a function which is a Get method.
override func viewDidLoad() {
super.viewDidLoad()
getData()
//use myStruct
in getData() i do this:
func getData()
{
Alamofire.request(.GET,"heregoesurl")
.responseJSON { response in
if let Json = response.result.value {
let json = JSON(Json)
for (_,subJson):(String, JSON) in json {
//get image url
//pass image url to imageGet() method
//add image to an array of myStruct
}
}
}
}
and in imageGet() method:
func getImages(url:String,type:String)
{
Alamofire.request(.GET, url)
.responseImage { response in
if let image = response.result.value {
//add image to myStruct
}
}
}
the problem is because of async nature of request, myStruct isn't ready in viewDidLoad. i used completionHandler for getData() method and it works fine. but still i don't have images and i just have image urls there. i don't know what should i do then.
any help on how to do things like this would be appreciated.
Try this:
public func getImages(url:String,type:String){
Alamofire.request(.GET, url).response { (request, response, data, error) in
//For sure that you will have the main queue and you will see the view
dispatch_sync(dispatch_get_main_queue(), {
self.myImageView.image = UIImage(data: data, scale:1) // or store it to your `myStruct`
})
}
}
call it inside of getData() function
use this:
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)
}
DispatchQueue.main.async {
self.image = UIImage(data:data!)
}
}
task.resume()
}
}
I have an iOS application that sync data from a JSON restful web service. This method is called from an external class (not UI controller). This class has a sync method and It sends and retrieves data without any problem. My problem is how do I pause the UI till I get my result.
The following would provide the idea about the code.
UIController Class
let C : Customer = Customer(UserName: UserName!, Password: Password!)
let S : Syncronization = Syncronization()
S.Sync(C)
Syncronization class
Class Syncronization : NSObject, NSURLSessionDataDelegate
func Sync(C : Customer){
var datastr = ""
datastr = "http://192.168.248.134:8008/MobileWeb.svc/GetFirstTimeSync/" + C.UserName + "/" + C.Password
let url:NSURL = NSURL(string: datastr)!
self.buffer = NSMutableData()
let defaultConfigObject:NSURLSessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration()
let session:NSURLSession = NSURLSession(configuration: defaultConfigObject, delegate: self, delegateQueue: NSOperationQueue.mainQueue())
let req:NSMutableURLRequest = NSMutableURLRequest(URL: url)
req.HTTPMethod = "POST"
session.dataTaskWithURL(url).resume()
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
print("Recieved with data")
buffer.appendData(data)
}
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
if error == nil {
print("Download Successful")
print("Done with Bytes " + String(buffer.length))
self.parseJSONA(self.buffer)
}
else {
print("Error %#",error!.userInfo);
print("Error description %#", error!.localizedDescription);
print("Error domain %#", error!.domain);
}
}
func parseJSONA(data:NSMutableData) {
do {
let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as! Array<AnyObject>
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
}
}
I have tried dispacther methods, but I believe I do not know how to use that so far cause most of the examples have down the services & data exchange on UI Controllers.
Any kind of help is appreciated. Thanks
You can give the Sync class a completion handler closure.
In your UIViewController:
S.completion = {
information in
UIElement.updateWith(information)
}
and of course you'll need to add a member to your Syncronization class:
var completion:((information:String)->())!
and you can call completion("here's some info!") from inside parseJSON() or URLSession() of the Syncronization class
Here's some reading on closures in Swift
If I understand you correctly, you want to be able to do something in your UI based on the outcome of your call to S.Sync(C)
One way of doing that is to include a closure as a parameter to your Sync function.
Here's how I would do that (Disclaimer...I haven't checked everything in a compiler, so there might be errors along the way. See how far you get, and if there are problems, just write again :-)):
enum SynchronizationResult {
case Success(Array<AnyObject>)
case Failure(NSError)
}
class Syncronization : NSObject, NSURLSessionDataDelegate {
var functionToExecuteWhenDone: ((SynchronizationResult) -> Void)?
func Sync(C : Customer, callback: (SynchronizationResult) -> Void){
functionToExecuteWhenDone = callback
var datastr = ""
datastr = "http://192.168.248.134:8008/MobileWeb.svc/GetFirstTimeSync/" + C.UserName + "/" + C.Password
let url:NSURL = NSURL(string: datastr)!
self.buffer = NSMutableData()
let defaultConfigObject:NSURLSessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration()
let session:NSURLSession = NSURLSession(configuration: defaultConfigObject, delegate: self, delegateQueue: NSOperationQueue.mainQueue())
let req:NSMutableURLRequest = NSMutableURLRequest(URL: url)
req.HTTPMethod = "POST"
session.dataTaskWithURL(url).resume()
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
print("Recieved with data")
buffer.appendData(data)
}
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
if error == nil {
print("Download Successful")
print("Done with Bytes " + String(buffer.length))
self.parseJSONA(self.buffer)
} else {
print("Error %#",error!.userInfo);
print("Error description %#", error!.localizedDescription);
print("Error domain %#", error!.domain);
let result = SynchronizationResult.Failure(error!)
if let functionToExecuteWhenDone = functionToExecuteWhenDone {
functionToExecuteWhenDone(result)
}
}
}
func parseJSONA(data:NSMutableData) {
do {
let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as! Array<AnyObject>
let result = SynchronizationResult.Success(json)
if let functionToExecuteWhenDone = functionToExecuteWhenDone {
functionToExecuteWhenDone(result)
}
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
let result = SynchronizationResult.Failure(error)
if let functionToExecuteWhenDone = functionToExecuteWhenDone {
functionToExecuteWhenDone(result)
}
}
}
}
So...we introduce an Enum called SynchronizationResult to handle the outcome of fetching data.
We then add a function to be called when done, as a parameter to the Sync function:
func Sync(C : Customer, callback: (SynchronizationResult) -> Void)
This method will be called with a SynchronizationResult as a parameter and returns void.
We store that callback in functionToExecuteWhenDone for later usage.
Depending on whether you see any errors along the way or everything is sunshine, we generate different SynchronizationResult values along the way and call your functionToExecuteWhenDone with the current SynchronizationResult when we are ready (when parsing is done or we have failed)
And in your ViewController you'd do something along the lines of
let C : Customer = Customer(UserName: UserName!, Password: Password!)
let S : Syncronization = Syncronization()
S.Sync(C) { (result) in
switch result {
case .Success(let json):
//Your code to update UI based on json goes here
case .Failure(let error):
//Your code to handle error goes here
}
}
I hope this makes sense and is what you needed.
this might help you
https://thatthinginswift.com/background-threads/
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
// do downloading or sync task here
dispatch_async(dispatch_get_main_queue()) {
// update some UI with downloaded data/sync data
}
}
let qualityOfServiceClass = QOS_CLASS_BACKGROUND
let backgroundQueue = dispatch_get_global_queue(qualityOfServiceClass, 0)
dispatch_async(backgroundQueue, {
print("This is run on the background queue")
dispatch_async(dispatch_get_main_queue(), { () -> Void in
print("This is run on the main queue, after the previous code in outer block")
})
})
Found here
I have a problem trying to load a UITableViewController.
I have an async task that is called in the loadView method. This async task works well and results are returned as I expect.
The problem is, the app fail when trying to populate the TableView.
I suspect that it's due to the fact that my data are not completely loaded when the method are called.
Is there anyway to force the TableView to wait on my async task to be finished ?
The function that loads my data:
func loadNetInformations(){
var postString: NSString = "info=all"
HTTPGet("myurl", "POST", postString){
(data: NSArray, error:String?)-> Void in
if error != nil{
println(error)
}
else{
for value in data{
var sService: NSString = "some value"
var aContent: NSArray = value["Content"] as NSArray
var sNumberCount: NSNumber = aContent.count
self.aListeService.addObject(sService)
self.aSizeOfDpt.addObject(sNumberCount)
self.aContenuListes.addObject(aContent)
self.bFinishLoading = true
} // End for in HTTPGet
} // End else in HTTPGet
} // End HTTPGet
} // End LoadNet Information
My HTTPGet method is as following:
func HTTPSendRequest(request: NSMutableURLRequest,
callback: (NSArray, String?) -> Void){
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if error != nil {
callback (NSArray(), error.localizedDescription)
println("error=\(error)")
return
}
else{
callback(NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil)! as NSArray, nil)
}
}
task.resume()
}
func HTTPGet(url: String, requestMethod: String, postString: String, callback: (NSArray, String?) -> Void){
var request = NSMutableURLRequest(URL: NSURL(string: url)!)
request.HTTPMethod = requestMethod
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding)
HTTPSendRequest(request, callback)
}
EDIT: Ok now I don't have the error anymore (I used a NSString as an NSInteger...) but it doesn't show anything
EDIT2:
here's my JSON format in case it can help:
[
{
"CodeAbsence" : "6000",
"Content" :
[
{
"Init":"DAS",
"Nom":"Name",
"Prenom":"Firstname",
"IdAbsence":"619",
"TimbreusePresent":"O",
"TimbreuseHeure":"14:44",
"TimbreuseRaison":"0",
"TimbreuseDate":"",
"CodeAbsence":"6000",
"Telephone":"248"
},
....
]
},
.......
]
You need to reload the tableView to trigger a table update and you need to trigger it on the main UI thread.
for value in data{
var sService: NSString = "some value"
var aContent: NSArray = value["Content"] as NSArray
var sNumberCount: NSNumber = aContent.count
self.aListeService.addObject(sService)
self.aSizeOfDpt.addObject(sNumberCount)
self.aContenuListes.addObject(aContent)
self.bFinishLoading = true
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
When you perform an asynchronous operation on a background thread your callback is still going to run on the background thread, so always be sure to switch back to the main thread before performing any UI task.
I finally found what was the problem. It was coming from the use of viewDidLoad() instead of viewDidAppear().