I have a function that returns parsed information out of a JSON string.
var info = [AppModel]()
func getEarthquakeInfo(completion: (results : NSArray?) ->Void ){
DataManager.getEarthquakeDataFromFileWithSuccess {
(data) -> Void in
let json = JSON(data: data)
if let JsonArray = json.array {
for appDict in JsonArray {
var ids: String? = appDict["id"].stringValue
var title: String? = appDict["title"].stringValue
var time: String? = appDict["time"].stringValue
var information = AppModel(idEarth: ids, title: title, time: time)
self.info.append(information)
completion(results: self.info)
}
}
}
}
This function uses SwiftyJSON and calls the web service using my DataManager class. When I print it out outside the function I get all the information I need. Now I want to use the title information to populate my TableView. Inside my cellForRowAtIndexPath I've tried filtering out my Earthinformation so that I could get just the title and put that into an array, and populate my tableView with that array. So far I've been unsuccessful, and I've been looking everywhere on how to do this and nothing I've tried or found worked. Can someone point me in the right direction on how to do this?
What I've done so far:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath) as UITableViewCell
getEarthquakeInfo( { (info) -> Void in
var Earthinformation = self.info as NSArray
let titleArray = Earthinformation["TITLE"] as NSArray // error: type int does not conform to protocol "StringLiteralConvertible"
cell.textLabel!.text = titleArray[indexPath.row]
})
return cell
}
3 records I get when I print out Earthinformation I get: ID: 146323, TITLE: M 1.6 - 27km E of Coso Junction, California, TIME: 2015-04-15 14:08:20 UTC,
, ID: 146346, TITLE: M 1.8 - 26km E of Coso Junction, California, TIME: 2015-04-15 14:08:20 UTC,
, ID: 146324, TITLE: M 2.4 - 26km NW of Anchor Point, Alaska, TIME: 2015-04-15 13:33:36 UTC,
Edit:
Sorry I should have included this before:
My AppModel.swift:
class AppModel: NSObject, Printable {
let idEarth: String
let title: String
let time: String
override var description: String {
return "ID: \(idEarth), TITLE: \(title), TIME: \(time), \n"
}
init(idEarth: String?, title: String?, time: String?) {
self.idEarth = idEarth ?? ""
self.title = title ?? ""
self.time = time ?? ""
}
}
And my DataManager.swift file:
let earthquakeURL = "http://www.kuakes.com/json/"
class DataManager {
class func getEarthquakeDataFromFileWithSuccess(success: ((websiteData: NSData) -> Void)) {
//1
loadDataFromURL(NSURL(string: earthquakeURL)!, completion:{(data, error) -> Void in
//2
if let urlData = data {
//3
success(websiteData: urlData)
}
})
}
class func loadDataFromURL(url: NSURL, completion:(data: NSData?, error: NSError?) -> Void) {
var session = NSURLSession.sharedSession()
// Use NSURLSession to get data from an NSURL
let loadDataTask = session.dataTaskWithURL(url, completionHandler: { (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void in
if let responseError = error {
completion(data: nil, error: responseError)
} else if let httpResponse = response as? NSHTTPURLResponse {
if httpResponse.statusCode != 200 {
var statusError = NSError(domain:"com.kuakes", code:httpResponse.statusCode, userInfo:[NSLocalizedDescriptionKey : "HTTP status code has unexpected value."])
completion(data: nil, error: statusError)
} else {
completion(data: data, error: nil)
}
}
})
loadDataTask.resume()
}
}
You are calling your getEarthquakeInfo function every time a cell is to be created. This is very unnecessary and may cause unintentional behavior (seeing as the getEarthquakeInfo function is asynchronous). I recommend utilizing the fact that you have a model array info already to use to populate your tableview. In viewDidLoad call your asynchronous function to retrieve the data for the model array. Example:
override func viewDidLoad() {
super.viewDidLoad()
getEarthquakeInfo { (info) in
// Your model array is now populated so we should reload our data
self.tableView.reloadData()
}
}
Now, adjust your UITableViewDataSource functions to properly handle the model array. Note that I am assuming that your info array is a property of the UITableViewController that is populating the tableView. Example:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier",
forIndexPath: indexPath) as UITableViewCell
// Note that I have no idea how you access the title of your AppModel
// object so you may have to adjust the below code
cell.textLabel!.text = self.info[indexPath.row].title
return cell
}
override func numberOfSectionsInTableView() {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) {
return info.count
}
Related
I try to display my data in a tableView using no framework to parse my data, but when I add my data to my table and debug it, it is nil at the output while my data I retrieve are well parses, have I forgotten something to do?
I use a structure for my parameters as this :
enum Types {
case School
case Hospital
case Station_Essence
case Restaurant
}
struct Adresse {
public var title: String
public var details: String?
public var type: Types
public var coordinate: [String: Any]
}
and in my ViewController, i proced as this :
class ListMapViewController: UIViewController {
#IBOutlet var TitleTableView: UITableView!
#IBOutlet var MapView: MKMapView!
var adresse: [Adresse]?
override func viewDidLoad() {
super.viewDidLoad()
self.TitleTableView.register(UINib(nibName: "ListMapTableViewCell", bundle: nil), forCellReuseIdentifier: "Adresse")
self.TitleTableView.delegate = self
self.TitleTableView.dataSource = self
guard let POI = URL(string: "https://moc4a-poi.herokuapp.com/") else {
return
}
let task = URLSession.shared.dataTask(with: POI) { (data, response, error) in
guard let dataResponse = data else { return }
if let json = try! JSONSerialization.jsonObject(with: dataResponse, options:[]) as? [[String: Any]] {
for data in json {
let title = data["title"] as! String
let details = data["details"] as? String
guard let type = data["type"] as? Int else { return }
let valueType = self.valueType(dataType: type)
guard let coordinates = data["coordinates"] as? [String: Any] else { return }
self.adresse?.append(Adresse(title: title, details: details, type: valueType, coordinate: coordinates))
}
}
print(self.adresse)
}
self.TitleTableView.reloadData()
task.resume()
}
private func valueType(dataType: Int) -> Types {
if(dataType == 1) {
return Types.School
} else if (dataType == 2) {
return Types.Hospital
} else if (dataType == 3) {
return Types.Station_Essence
} else {
return Types.Restaurant
}
}
}
extension ListMapViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.adresse?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Adresse", for: indexPath) as! ListMapTableViewCell
if let adresse = self.adresse?[indexPath.row] {
cell.draw(adresse: adresse)
}
return cell
}
}
extension ListMapViewController: UITableViewDelegate {
}
You have two big problems.
self.adresse is nil. You never assign it a value. So all of the self.adresse?... do nothing.
You call reloadData too soon. It needs to be done inside the completion block, after you update the data. And it needs to be on the main queue.
To fix #1, change var adresse: [Adresse]? to var adresse = [Adresse](). Then you can get rid of all the ? after uses of adresse.
To fix #2, add:
DispatchQueue.main.async {
self.TitleTableView.reloadData()
}
just after the print at the end of the completion block. Don't forget to remove the current call to reloadData.
Edit: Please ignore the old answers. I can't create a new question my accunt was blocked. So I edited this one with a new question in the title.
How can i have my data result as soon as i viewDidLoad my tableView ?
Here is a snippet of my code. Can anyone help me to understand the asynchronous part that make my code fetch data and bring me back the result late.
`
let apiUrl = "https://shopicruit.myshopify.com/admin/products.json?
page=1&access_token=c32313df0d0ef512ca64d5b336a0d7c6"
var myRowProduct : String = ""
var productArrayName : [String] = []
var productsPassedOver : [String] = []
override func viewDidLoad() {
super.viewDidLoad()
getFullProductsJson(url: apiUrl, tag1: myRowProduct){array, err in
if err != nil {
print("ERROR 2")
}else{
self.productsPassedOver = array
self.tableView.reloadData()
print("productsPassedOver \(self.productsPassedOver)")
}
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return productsPassedOver.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell", for: indexPath)
cell.selectionStyle = .none
cell.textLabel?.text = "\(indexPath.row + 1). " + productsPassedOver[indexPath.row] + "\n" //label are in every single cell. The current text that is displayed by the label
cell.textLabel?.numberOfLines = 0; //removes any limitations on the number of lines displayed
return cell
}
//Networking JSON
func getFullProductsJson(url: String, tag1: String, completion: #escaping ([String], Error?) -> ()) {
Alamofire.request(url, method: .get).responseJSON {
response in
//response.result.value comes back as an optional so use ! to use it. Also value have datatype of Any? thats why we JSON casting
if response.result.isSuccess{
let fullJSON : JSON = JSON(response.result.value!)
let finalArrayOfProductS = self.updateTagProductData(json: fullJSON, tag2: tag1)
completion(finalArrayOfProductS, nil)
}
else{
print("Error JSON")
}
}
}
//JSON parsing, deal with formatting here
func updateTagProductData(json : JSON, tag2: String) -> [String]{
let tagClicked = tag2
var nbOfProducts : Int = 0
nbOfProducts = json["products"].count
var inventoryAvailableTotal : Int = 0
var nbOfVariants : Int = 0
for i in 0..<nbOfProducts {
let tagsGroup : String = json["products"][i]["tags"].stringValue
let productName : String = json["products"][i]["product_type"].stringValue
nbOfVariants = json["products"][i]["variants"].count
if tagsGroup.contains(tagClicked){
for j in 0..<nbOfVariants{
inventoryAvailableTotal += json["products"][i]["variants"][j]["inventory_quantity"].int!
}
print("\(tagClicked) tag exist in the product \(i): \(productName)")
print("inventorytotal \(inventoryAvailableTotal)")
productArrayName.append("\(productName): \(inventoryAvailableTotal) available")
}
}
return productArrayName
}
}
`
This is a function that does something in the background and return immediately, and when it finishes, it calls your completion handler.
So it is natural for this function to go directly to the return finaleNbOfRowsInProductsListPage.
Your solution is that the function shouldn't return the value, but it should
accept a completion handler, and call it when ended. This is the continuation passing style.
func getFullProductsJson(url: String, tag1: String, completion: #escaping (Int?, Error?) -> ()) {
Alamofire.request(url, method: .get).responseJSON {
response in
...
// When you are done
completion(finaleNbOfRowsInProductsListPage, nil)
}
}
Also please note to try avoiding setting a lot of variables, try to make everything a parameter or a return value from a function, this is easier to debug. For example, try to make the list as a parameter that is passed to the completion handler, not as a member variable of your view controller, make this member variable only for the list that is ready to be displayed.
func getFullProductsJson(url: String, tag1: String, completion: #escaping (Products?, Error?) -> ()) {
Alamofire.request(url, method: .get).responseJSON {
response in
...
// When you are done
completion(products, nil)
}
}
var products: [Product] = []
func refreshData() {
getFullProductsJson(url: "YOUR_URL_HERE", tag1: "TAG") {
// Try to use [weak self], also, read about Automatic Reference Counting
productsNullable, error in
if let products = productsNullable {
// Alamofire calls your callback on a background thread
// So you must return to the main thread first when you want
// to pass the result to any UI component.
DispatchQueue.main.async {
self?.products = products
self?.tableView.reloadData()
}
}
}
}
I have some JSON data that looks like this
{
"fullName": "John Doe",
"imageUrl": "https://www.example.com/images/about/team/john.jpg",
"titles": [
"Founder & President",
"Advisor"
]
},
{
"fullName": "Jane Doe",
"imageUrl": "https://www.example.com/images/about/team/jane.jpg",
"titles": [
"Executive Vice President",
"Director of Advisor Services and Marketing"
]
},
The data gets loaded and parses correctly, but when I run the code I get an error. I think it has to do with the titles section having multiple titles and it doesn't know how to display the titles correctly.
Here is my code.
This is the initial structure.
import UIKit
class Managers: Codable {
let managers: [Manager]
init (managers: [Manager]) {
self.managers = managers
}
}
class Manager: Codable {
let imageUrl: String?
let fullName: String?
let titles: [titles]
init(imageUrl: String?, fullName: String?, titles: [titles]) {
self.imageUrl = imageUrl
self.fullName = fullName
self.titles = titles
}
struct titles: Codable {
let title: String
}
}
This is the Management cell that displays the layout of the image, name and title.
import UIKit
class ManagementCell: UITableViewCell {
#IBOutlet weak var imageUrl: UIImageView!
#IBOutlet weak var fullNameLbl: UILabel!
#IBOutlet weak var titlesLbl: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
This is the view controller
import UIKit
class ManagementViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
final let url = URL(string: "Data from above goes here")
private var managers = [Manager]()
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
downloadJson()
tableView.tableFooterView = UIView()
}
func downloadJson() {
guard let downloadURL = url else { return }
URLSession.shared.dataTask(with: downloadURL) { data, urlResponse, error in
guard let data = data, error == nil, urlResponse != nil else {
print("Something went wrong")
return
}
print("downloaded")
do
{
let decoder = JSONDecoder()
let downloadedManagers = try decoder.decode([Manager].self, from: data)
self.managers = downloadedManagers
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch {
print("Something went wrong after download")
}
}.resume()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return managers.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "ManagementCell") as? ManagementCell else { return UITableViewCell() }
cell.fullNameLbl?.text = managers[indexPath.row].fullName
cell.titlesLbl?.text = managers[indexPath.row].titles
if let imageURL = URL(string: managers[indexPath.row].imageUrl!) {
DispatchQueue.global().async {
let data = try? Data(contentsOf: imageURL)
if let data = data {
let image = UIImage(data: data)
DispatchQueue.main.async {
cell.imageUrl.image = image
cell.imageUrl.layer.cornerRadius = 60
cell.imageUrl.clipsToBounds = true
cell.imageUrl.contentMode = .scaleAspectFit
cell.imageUrl.backgroundColor = UIColor.lightGray
cell.imageUrl.layer.borderWidth = 5
cell.imageUrl.layer.borderColor = UIColor.clear.cgColor
}
}
}
}
return cell
}
This line of code is throwing an error:
cell.titlesLbl?.text = managers[indexPath.row].titles
error is Cannot assign value of type '[Manager.titles]' to type 'String?'
Does anyone have a workaround for this. I believe that I have to create a separate loop for the titles since it also has an array of options. Any suggestions will be very much appreciated.
On a separate note for the image if an image is missing is causes the app to crash. How do I set a generic image if an image in the JSON data isn't present?
Because managers[indexPath.row].titles returns an array of Strings. Peeking into your data, it looks like the first title in that array is the most recent title the manager holds. You can append an index or call .first to get that title:
cell.titlesLbl?.text = managers[indexPath.row].titles.first?.title
// or
cell.titlesLbl?.text = managers[indexPath.row].titles[0].title
// or to show all past titles, comma separated
cell.titlesLbl?.text = managers[indexPath.row].titles
.map({ $0.title })
.joined(separator: ", ")
The difference is that .first won't throw an error if the titles array is empty. You get a blank label instead. [0] will throw a index out of bounds error if your array is empty.
I have already parsed JSON and showing in tableView which is working fine. Now my question is how will i save data offline and show when internet is not available offline using Core Data. I am working in Swift 3. If anyone can help me with screenshot it will be great help.
Below is my Code for fetching json and showing on tableView :
import UIKit
import SystemConfiguration
struct CellData {
var name:String
var address:String
public init(name:String,address:String){
self.name = name
self.address = address
}
}
///ViewController
class ViewController: UIViewController,UITableViewDataSource,UITableViewDelegate {
#IBOutlet weak var tableViewData: UITableView!
var arrayData = [CellData]()
override func viewDidLoad() {
super.viewDidLoad()
if Reachability.isConnectedToNetwork(){
print("Internet Connection Available!")
fetchServerData()
}else{
let alert = UIAlertController(title: "No Internet connection", message: "Please ensure you are connected to the Internet", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
print("Internet Connection not Available!")
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrayData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! MyCellData
cell.lblTop.text = "😀\(arrayData[indexPath.row].name)"
cell.lblBottom.text = arrayData[indexPath.row].address
return cell
}
func fetchServerData(){
let prs = [
"author_id": "1780",
"get_deals_author": "1" as String
]
Service.StartWithoutLoading(prs as [String : AnyObject]?, onCompletion: { result in
let json = result as? NSDictionary
if let data = json as? [String:Any]{
if let err = data["status"] as? String, err == "success"{
if let data = data["result"] as? [Any]{
var arrayData = [CellData]()
for sectionObj in data{
if let sectionObjVal = sectionObj as? [String:Any]{
if let name_deal = sectionObjVal["name"] as? String{
if let address_deal = sectionObjVal["address"] as? String{
let dataValue = CellData.init(name: name_deal, address: address_deal)
arrayData.append(dataValue)
}
}
}
}
DispatchQueue.main.async { () -> Void in
self.arrayData.removeAll()
self.arrayData = arrayData
self.tableViewData.reloadData()
}
}
}
}
})
}
}
For Core Data, you need to create the entities you need in CoreData model .xcdatamodeld. Click on Add Entity and name your entity. Then add attributes which you require to save.
You can see this link on how to create the entities and attributes. After creating everything, we can write a CoreDataStack and a manager class or we can directly use the code pre-written in AppDelegate when we check on Core Data when creating a project. I'll here use the CoreDataStack class.
Here is the class
import Foundation
import CoreData
class CoreDataStack: NSObject {
static let moduleName = "YourProject"
static let shared = CoreDataStack()
private override init() {
super.init()
_ = self.persistentContainer
}
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: CoreDataStack.moduleName)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
print("Coordinator URL - \(storeDescription)")
})
return container
}()
}
Now we can make a manager class to insert the data. Let's say your entity is Person and its attributes are name and address
Here is the CoreDataManager class to insert, update, fetch data.
import UIKit
import CoreData
class CoreDataManager: NSObject {
class func addRecord(object:[String:Any]) {
let person = NSEntityDescription.insertNewObject(forEntityName: "Person", into: CoreDataStack.shared.persistentContainer.viewContext) as! Person
person.name = object["name"] as? String
person.address = object["address"] as? String
CoreDataStack.shared.saveContext()
}
class func getRecords() -> [Person]? {
let request:NSFetchRequest<Person> = Person.fetchRequest()
do {
let results = try CoreDataStack.shared.persistentContainer.viewContext.fetch(request)
return results
} catch {
print(error.localizedDescription)
}
return nil
}
}
You can call addRecord method in your ViewController class and it will save your data. I recommend that you pass the complete array and then add in core data and finally call saveContext().
Finally you can use getRecords to get all records.
I am trying to parse JSON using URLSession and without using Alamofire or anything else.
I just want to take the JSON and put it into a UITableView.
I am trying to piece together what I learned from learning how to Parse JSON using Alamofire with what I can find on google. Many of the answers on youtube or Stack etc use NS for everything..NSURL, NSDictionary, etc etc..Or are just typing code without explaining what/why.
I THINK I am almost there, but I need help understanding what I have left to do.
SO.
I Allowed arbitrary loads in the plst
In a Swift File I have the following
class Potter {
private var _title: String!
private var _author: String!
private var _imageURL: String!
let POTTER_URL = "http://de-coding-test.s3.amazonaws.com/books.json"
var title: String {
if _title == nil {
_title = ""
}
return _title
}
var author: String {
if _author == nil {
_author = ""
}
return _author
}
var imageURL: String {
if _imageURL == nil {
_imageURL = ""
}
return _imageURL
}
func downloadJSON() {
let url = URL(string: POTTER_URL)
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print("Error")
} else {
if let content = data {
do {
if let jDict = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as? Dictionary<String, AnyObject> {
if let title = jDict["title"] as? String {
self._title = title.capitalized
}
if let author = jDict["author"] as? String {
self._author = author.capitalized
}
if let imgURL = jDict["imageURL"] as? String {
self._imageURL = imgURL
}
}
}
catch {
}
}
}
}
task.resume()
}
}
In my Main.Storyboard I added the tableview and set up all the UI, and in my ViewController I have set up the tableview delegates.
I created a property of
var potters = [Potter]()
I am stuck now on how to I populate this array, and how do I set up the proper threading
First of all your model is insane pretty weird.
In Swift never use backed private variables to get read/only properties. And never declare properties as implicit unwrapped optional because you are too lazy to write an initializer.
The entire model can be reduced to
class Potter {
let title, author, imageURL: String
init(title: String, author: String, imageURL : String) {
self.title = title
self.author = author
self.imageURL = imageURL
}
}
If you would use a struct, it's even
struct Potter {
let title, author, imageURL: String
}
because you get the memberwise initializer for free.
Secondly, put the method downloadJSON() out of the model and put it in the controller and call it in viewDidLoad().
In the controller declare the download URL and the data source array
let POTTER_URL = "http://de-coding-test.s3.amazonaws.com/books.json"
var books = [Potter]()
Your method downloadJSON() cannot work because the JSON object is an array ([]), not a dictionary ({}). You need a loop to iterate thru the items, get the values, create a Potter item respectively and append it to the data source. If a value does not exist, an empty string is assigned. Finally reload the table view on the main thread.
func downloadJSON() {
let url = URL(string: POTTER_URL)
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print("DataTask error", error!)
} else {
do {
if let bookData = try JSONSerialization.jsonObject(with: data!) as? [[String:String]] {
books.removeAll() // clear data source array
for book in bookData {
let title = book["title"] ?? ""
let author = book["author"] ?? ""
let imgURL = book["imageURL"] ?? ""
books.append(Potter(title: title, author: author, imageURL: imgURL))
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
catch {
print("Serialization error", error)
}
}
}
task.resume()
}
Two notes:
The standard JSON dictionary in Swift 3 is [String:Any], in this particular case it's even [String:String].
.mutableContainers is useless if the containers are only read and useless in Swift anyway because the object cannot be casted to NSMutableArray / -Dictionary and you get mutability for free using a variable.
The web services returns an array of objects: [Dictionary<String, AnyObject>].
It will be easier if you create a init method with a dictionary as parameter.
The downloadJSON is an async task, using completionHandler is the best way. And if you want to place the downloadJSON in the Potter class, it should be a static function.
Final, you should handle the result like this:
Potter.downloadJSON { potters in
self.potters = potters
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
The final code:
class ViewController: UIViewController {
var potters = [Potter]()
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
Potter.downloadJSON { potters in
self.potters = potters
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return potters.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
let potter = potters[indexPath.row]
cell.textLabel?.text = potter.title
cell.detailTextLabel?.text = potter.author
return cell
}
}
class Potter {
private var _title: String!
private var _author: String!
private var _imageURL: String!
static let POTTER_URL = "http://de-coding-test.s3.amazonaws.com/books.json"
var title: String {
if _title == nil {
_title = ""
}
return _title
}
var author: String {
if _author == nil {
_author = ""
}
return _author
}
var imageURL: String {
if _imageURL == nil {
_imageURL = ""
}
return _imageURL
}
init(dict: Dictionary<String, AnyObject>) {
self._title = dict["title"] as? String
self._imageURL = dict["imageURL"] as? String
self._author = dict["author"] as? String
}
class func downloadJSON(completion: #escaping (_ potters: [Potter]) -> Void) {
let url = URL(string: POTTER_URL)
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print("Error")
} else {
if let content = data {
do {
if let jArray = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as? [Dictionary<String, AnyObject>] {
var potters = [Potter]()
for jDict in jArray {
let potter = Potter(dict: jDict)
potters.append(potter)
}
completion(potters)
}
}
catch {
}
}
}
}
task.resume()
}
}
The method downloadJSON() should be implemented in the ViewController since it is returning the array of Potter data. Then in the URLSession response you should create one array which will be act as the tableview datasource. (i.e self.arrTableData = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as? [[String : AnyObject]])
Then in for the tableView
func tableView(_ tableView: UITableView, numberOfRowsInSection sectionIndex: Int) -> Int {
return self.arrTableData.count
}
and in cell for row at index path
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//create `potters` object with the value and use it else you can direcly use the value of objects as below.
let dictPotters = self.arrTableData[indexPath.row]
let title = dictPotters["title"]
}
Thanks