I am a TOTAL NOOB when it comes to iOS coding.
Im trying to learn how to make an API call to "http://de-coding-test.s3.amazonaws.com/books.json"
however, since I'm a total noob, all tutorials i find make no sense as to how to do it.
All i want to learn is how I can get the JSON data from the web and input it into a UITableViewCell
I have looked through about 3 dozen tutorials, and nothing makes sense.
Any help is appreciated.
Let's going by step:
1) The framework that you're going to use to make api call is NSURLSession (or some library like Alomofire, etc).
An example to make an api call:
func getBooksData(){
let url = "http://de-coding-test.s3.amazonaws.com/books.json"
(NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: url)!) { (data:NSData?, response:NSURLResponse?, error:NSError?) -> Void in
//Here we're converting the JSON to an NSArray
if let jsonData = (try? NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableLeaves)) as? NSArray{
//Create a local variable to save the data that we receive
var results:[Book] = []
for bookDict in jsonData where bookDict is NSDictionary{
//Create book objects and add to the array of results
let book = Book.objectWithDictionary(bookDict as! NSDictionary)
results.append(book)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
//Make a call to the UI on the main queue
self.books = results
self.tblBooks.reloadData()
})
}
}).resume()
}
Book Entity:
class Book{
var title:String
init(title:String){
self.title = title
}
class func objectWithDictionary(dict:NSDictionary)->Book{
var title = ""
if let titleTmp = dict.objectForKey("title") as? String{
title = titleTmp
}
return Book(title: title)
}
}
Note: In practice, you will check error and status code of response, and also you can extract the code of making api call to another class (or service layer).One option, using the pattern of DataMapper, you can create a class Manager by Entities (In this example for book like BookManager) and you can make something like this (You can abstract even more, creating a general api, that receive a url and return an AnyObject from the transformation of the JSON, and from there process inside your manager):
class BookManager{
let sharedInstance:BookManager = BookManager()
private init(){}
func getBookData(success:([Book])->Void,failure:(String)->Void){
let url = "http://de-coding-test.s3.amazonaws.com/books.json"
(NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: url)!) { (data:NSData?, response:NSURLResponse?, error:NSError?) -> Void in
if error != nil{
failure(error!.localizedDescription)
}else{
if let jsonData = (try? NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableLeaves)) as? NSArray{
var results:[Book] = []
for bookDict in jsonData where bookDict is NSDictionary{
let book = Book.objectWithDictionary(bookDict as! NSDictionary)
results.append(book)
}
success(results)
}else{
failure("Error Format")
}
}
}).resume()
}
}
Related
I have a JSON request that gets data from the Darksky API, I get the data properly and it is showing on the screen. However, When i'm trying to set the data from the array I get from the JSON call in another array, it stays empty.
This is my code:
just declaring the array:
var mForecastArray = [Weather]()
this is the function that calls the API:
func getForecast(){
Weather.forecast(withLocation: "37.8267,-122.4233") { (arr) in
DispatchQueue.main.async {
self.mForecastArray = arr
self.mTodayWeather = arr[0]
self.mCollectionView.reloadData()
}
}
}
The weird part is that it does work, and the data do shows on screen, but still, mForecastArray seems null.
This is the API call itself:
static func forecast(withLocation location: String, completion: #escaping ([Weather]) -> ()){
let url = basePath + location
let request = URLRequest(url: URL(string: url)!)
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
var forecastArray: [Weather] = []
if let data = data{
do{
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String:Any]{
if let dailyForecast = json["daily"] as? [String:Any]{
if let dailyData = dailyForecast["data"] as? [[String:Any]]{
for dataPoint in dailyData{
if let weatherObject = try? Weather(json: dataPoint){
forecastArray.append(weatherObject)
}
}
}
}
}
}catch{
print(error.localizedDescription)
}
completion(forecastArray)
}
}
task.resume()
}
It's a visual asynchronous illusion.
The static method forecast works asynchronously.
Most likely your code looks like
getForecast()
print(self.mForecastArray)
This cannot work because the array is populated much later.
Move the print line into the completion handler of the static method
func getForecast(){
Weather.forecast(withLocation: "37.8267,-122.4233") { (arr) in
DispatchQueue.main.async {
self.mForecastArray = arr
print(self.mForecastArray)
self.mTodayWeather = arr[0]
self.mCollectionView.reloadData()
}
}
}
I am trying to extract an array from closure in swift 3 and its not working for me. I have my JSON parser in the class WeatherGetter and I am calling it in the view did load file in the viewcontroller.swift how to assign the weather_data array to some outside variable?
class WeatherGetter {
func getWeather(_ zip: String, startdate: String, enddate: String, completion: #escaping (([[Double]]) -> Void)) {
// This is a pretty simple networking task, so the shared session will do.
let session = URLSession.shared
let string = "Insert API address"
let url = URL(string: string)
var weatherRequestURL = URLRequest(url:url! as URL)
weatherRequestURL.httpMethod = "GET"
// The data task retrieves the data.
let dataTask = session.dataTask(with: weatherRequestURL) {
(data, response, error) -> Void in
if let error = error {
// Case 1: Error
// We got some kind of error while trying to get data from the server.
print("Error:\n\(error)")
}
else {
// Case 2: Success
// We got a response from the server!
do {
var temps = [Double]()
var winds = [Double]()
let weather = try JSON(data: data!)
//print(weather)
let conditions1 = weather["data"]
let conditions2 = conditions1["weather"]
let count = conditions2.count
for i in 0...count-1 {
let conditions3 = conditions2[i]
let conditions4 = conditions3["hourly"]
let count2 = conditions4.count
for j in 0...count2-1 {
let conditions5 = conditions4[j]
let tempF = conditions5["tempF"].doubleValue
let windspeed = conditions5["windspeedKmph"].doubleValue
//temps.updateValue(tempF, forKey: "\(date)//\(j)")
temps.append(tempF)
winds.append(windspeed)
}
}
//print(temps)
//print(winds)
completion([temps, winds])
}
catch let jsonError as NSError {
// An error occurred while trying to convert the data into a Swift dictionary.
print("JSON error description: \(jsonError.description)")
}
}
}
// The data task is set up...launch it!
dataTask.resume()
}
}
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30"){(weather_data) -> Void in
print(weather_data[1])
}
//Do your stuff with isResponse variable.
}
You can assign it to a class property like this:
var weatherData: [[Double]]()
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30"){(weather_data) -> Void in
self.weatherData = weather_data
// reload or display data
}
}
You need to remember that the network request takes some time, so this is why you would call something like reloadData once you know you have received the response.
Say for example, the network response takes 100 milliseconds to respond. By the time the data has responded, all of the code in viewDidLoad will very likely be completely finished. So you need to respond to the data being received, when you receive it. If you have a bad mobile signal, it may take longer.
This is why you use callbacks/closures. They are called when the operation completes
UPDATE:
The code inside getWeather shows multiple errors for me and won't let me run it as is.
I managed to get a response from the weather API by modifying the code slightly and commenting alot out. Your main issue here is that you are not casting your JSON data to specific types.
// The data task retrieves the data.
let dataTask = session.dataTask(with: weatherRequestURL) {
(data, response, error) -> Void in
guard error == nil, let data = data else {
print("ERROR")
return
}
// Case 2: Success
// We got a response from the server!
do {
var temps = [Double]()
var winds = [Double]()
if let weather = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String:AnyObject] {
if let conditions1 = weather["data"] as? [String:AnyObject] {
print(conditions1)
}
}
} catch let jsonError {
// An error occurred while trying to convert the data into a Swift dictionary.
print("JSON error description: \(jsonError)")
}
}
dataTask.resume()
See in the code above how I am optionally unwrapping the values whilst casting their types. This is what you need to do throughout your code and check you get the right data at each step along the way. Unfortunately the API response is too large for me to do it here.
Unwrapping JSON Swift
Swift Closures
iOS Networking with Swift - This is a free course which I highly recommend. This is how I learnt iOS networking.
As mentioned by #Scriptable, it takes a while for the response to be processed since it's asynchronous. What you can do is to add the OperationQueue.main.addOperation to assign the current process to the main queue. This will prioritize the processing of your network response. You can also put your reloadData in this part.
var weatherData: [Double]()
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
OperationQueue.main.addOperation {
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30"){(weather_data) -> Void in
print(weather_data[1])
// reloadData()
}
}
}
I am retrieving posts in a data handler from a DB for a news feed and I run a php script and echo the JSON encoded data back to my application, at which point the data is parsed and stored in a model of a "post", there is a protocol that is used in the view controller to get the data once it has been downloaded. My problem is that I am getting the notorious "Unexpectedly found nil when unwrapping optional value" error when I pass the NSMutableArray of "post" objects to the function "itemsDownloaded" which is function of the protocol. I checked all the values being parsed and they exist, and I also checked the count of the array to make sure it has values. The exception is occurring on the line self.delegate.itemsDownloaded(posts)
The code to handle the data is this :
import Foundation
protocol PostDataHandlerProtocol: class {
func itemsDownloaded(items: NSArray)
}
class PostDataHandler: NSObject, NSURLSessionDataDelegate {
weak var delegate: PostDataHandlerProtocol!
var data : NSMutableData = NSMutableData()
//The path to the php script to be executed
let urlPath: String = "www.something.com/myphpscript.php"
func downloadItems() {
let url: NSURL = NSURL(string: urlPath)!
var session: NSURLSession!
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
session = NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil)
let task = session.dataTaskWithURL(url)
task.resume()
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
self.data.appendData(data);
}
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
if error != nil {
print("Failed to download data")
}else {
print("Data downloaded")
self.parseJSON()
}
}
func parseJSON() {
var jsonResult: NSMutableArray = NSMutableArray()
do{
jsonResult = try NSJSONSerialization.JSONObjectWithData(self.data, options:NSJSONReadingOptions.AllowFragments) as! NSMutableArray
} catch let error as NSError {
print(error)
}
var jsonElement: NSDictionary = NSDictionary()
let posts: NSMutableArray = NSMutableArray()
for(var i = 0; i < jsonResult.count; i++)
{
jsonElement = jsonResult[i] as! NSDictionary
let post = PostModel()
//The following insures none of the JsonElement values are nil through optional binding
let username = jsonElement["username"] as? String
let imagePath = jsonElement["user_imagePath"] as? String
let postID = (jsonElement["post_id"] as! NSString).integerValue
let postRep = (jsonElement["post_rep"] as! NSString).integerValue
let postType = jsonElement["post_type"] as? String
let postDate = jsonElement["post_date"] as? String
let comment = jsonElement["comment"] as? String
post.username = username
post.imagePath = imagePath
post.postID = postID
post.postRep = postRep
post.postType = postType
post.postDate = postDate
post.comment = comment
posts.addObject(post)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.delegate.itemsDownloaded(posts)
})
}
}
In my view controller, I create a new data handler let postHandler = PostDataHandler() and then once the view has loaded I call postHandler.downloadItems() and in the view controller declaration I conformed to the protocol and implemented itemsDownloaded:
func itemsDownloaded(items: NSArray) {
allPosts = items as! [PostModel]
indicator.stopAnimating()
self.tableVIew.reloadData()
}
Does anyone know why this is happening? I tried to look at the numerous postings regarding this error as I'm aware it's quite common, but couldn't find anything that helped me. Many of the postings say there should be a check to ensure it's not nil, the problem is I didn't think NSMutableArray can be optional, and also I checked all the values and they don't appear to be nil, so those answers did not help me. In the thread exceptions it says something related to a closure, which I think could be causing the issue, I'm just not exactly sure what or how.
None of the code that you showed ever set your PostDataHandler's delegate property to anything, so it is reasonable to suppose that it is still nil, which perfectly explains why you crash at runtime when you try to access it as if it were an actual object.
I'm saving images into Parse Data as an array of NSURL's. Once I have them back into my app I would like to convert them to [String] so my app can temporarily store them. Any ideas?
Here is my code....
// Saving like This....
vc.videoImageArry = defaults.setObjectForKey("vidImages)
//Retrieving like This....
vc.vidImageArray = defaults.objectForKey("vidImages") as! [NSURL]
Using NSData
You can convert each NSURL to NSData in order to save it
func save(urls: [NSURL]) {
let urlsData = urls.map { $0.dataRepresentation }
NSUserDefaults.standardUserDefaults().setObject(urlsData, forKey: "urlsData")
}
Later on you can retrieve the NSData array and convert it back to [NSURL]
func load() -> [NSURL]? {
let retrievedData = NSUserDefaults.standardUserDefaults().arrayForKey("urlsData") as? [NSData]
return retrievedData?.map { NSURL(dataRepresentation: $0, relativeToURL: nil) }
}
Using String
Alternatively you can save the urls as String(s)
func save(urls: [NSURL]) {
let urlsData = urls.map { $0.absoluteString }
NSUserDefaults.standardUserDefaults().setObject(urlsData, forKey: "urlsData")
}
func load() -> [NSURL?]? {
let retrievedData = NSUserDefaults.standardUserDefaults().arrayForKey("urlsData") as? [String]
return retrievedData?.map { NSURL(string: $0) }
}
As discussed in the comments below, if data is written to NSUserDefaults exclusively with the save function, we know that every element of the array is a String representing a valid NSURL.
So we can change the return type of load from [NSURL?]? to [NSURL]? using this alternate version of load.
func load() -> [NSURL]? {
let retrievedData = NSUserDefaults.standardUserDefaults().arrayForKey("urlsData") as? [String]
return retrievedData?.flatMap { NSURL(string: $0) }
}
To convert from NSURL to String:
String(url)
To convert from String to NSURL:
NSURL(string: string)
Here's a fully working example that converts an array both ways:
import Cocoa
let urls = [NSURL(string: "http://www.swift.org/")!, NSURL(string: "http://www.apple.com/")!]
let strings = urls.map { String($0) }
let backToUrls = strings.map { NSURL(string: $0)! }
I believe that the above answers your specific question.
Having said that, the line for saving doesn't look right to me. You may want to look further into NSUserDefaults or ask a separate question if you're having difficulty with that line. You would need to paste some more context like lines above and below and exact error messages you're getting if any.
I'm trying to retrieve an image and text from Parse. I'm able to retrieve the saved text but not the image. What am I doing wrong? Below is the code for the function that I want to retrieve the image. Thanks in advance.
func showImage() {
var query = PFQuery(className: "Description")
query.orderByDescending("ceatedAt")
query.findObjectsInBackgroundWithBlock { (objects: [AnyObject]?, error: NSError?) -> Void in
self.imageArray = [UIImage]()
if let objects = objects {
for imageObject in objects {
let userImage: UIImage? = (imageObject as! PFObject)["UserPhoto"] as? UIImage
if userImage != nil {
self.imageArray.append(userImage!)
}
}
}
self.tableView.reloadData()
}
}
It's tricky at first, but it gets a lot easier. Here's the code to make it work:
let userImage = (imageObject as! PFObject)["UserPhoto"] as! PFFile
userImage.getDataInBackgroundWithBlock {
(imageData, error) -> Void in
if error == nil {
let image = UIImage(data: imageData!)
self.imageArray.append(userImage!)
} else {}
}}
the issue is that parse stores images as PFFiles, they're not directly images yet, think of it more as a URL than anything. You have to download the image, and then do something with it to make it work. You can't just directly cast it as a UIImage.
One thing to note (because this gave me trouble a while ago) is that the .getDataInBackgroundWithBlock method is asynchronous, so it'll run on it's own, and your code will continue before it's completed. Another thing to get used to.
Best of luck!