Populate array of custom classes from two different network requests - ios

In my Swift iOS project, I am trying to populate an array of custom class objects using JSON data retrieved with Alamofire and parsed with SwiftyJSON. My problem, though, is combining the results of two different network request and then populating a UITableView with the resulting array.
My custom class is implemented:
class teamItem: Printable {
var name: String?
var number: String?
init(sqljson: JSON, nallenjson: JSON, numinjson: Int) {
if let n = sqljson[numinjson, "team_num"].string! as String! {
self.number = n
}
if let name = nallenjson["result",0,"team_name"].string! as String! {
self.name = name
}
}
var description: String {
return "Number: \(number) Name: \(name)"
}
}
Here is my viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
refresh() {
() -> Void in
self.tableView(self.tableView, numberOfRowsInSection: self.teamsArr.count)
self.tableView.reloadData()
for item in self.teamsArr {
println(item)
}
return
}
self.tableView.reloadData()
}
which goes to the refresh() method:
func refresh(completionHandler: (() -> Void)) {
populateArray(completionHandler)
}
and finally, populateArray():
func populateArray(completionHandler: (() -> Void)) {
SqlHelper.getData("http://cnidarian1.net16.net/select_team.php", params: ["team_num":"ALL"]) {
(result: NSData) in
let jsonObject : AnyObject! = NSJSONSerialization.JSONObjectWithData(result, options: NSJSONReadingOptions.MutableContainers, error: nil)
let json = JSON(jsonObject)
self.json1 = json
println(json.count)
for var i = 0; i < json.count; ++i {
var teamnum = json[i,"team_num"].string!
NSLog(teamnum)
Alamofire.request(.GET, "http://api.vex.us.nallen.me/get_teams", parameters: ["team": teamnum])
.responseJSON { (req, res, json, err) in
let json = JSON(json!)
self.json2 = json
self.teamsArr.append(teamItem(sqljson: self.json1, nallenjson: self.json2, numinjson: i))
}
}
completionHandler()
}
}
the first problem I had was that i in the for loop reached 3 and caused errors when I thought it really shouldn't because that JSON array only contains 3 entries. My other main problem was that the table view would be empty until I manually triggered reloadData() with a reload button in my UI, and even then there were problems with the data in the tables.
really appreciate any assistance, as I am very new to iOS and Swift and dealing with Alamofire's asynchronous calls really confused me. The code I have been writing has grown so large and generated so many little errors, I thought there would probably be a better way of achieving my goal. Sorry for the long-winded question, and thanks in advance for any responses!

The Alamofire request returns immediately and in parallel executes the closure, which will take some time to complete. Your completion handler is called right after the Alamofire returns, but the data aren't yet available. You need to call it from within the Alamofire closure - this ensures that it is called after the data became available.

Related

Wait for JSON parsing in a different class file in swift 3

I created a class as shown in the code below, and as u can see I am parsing a JSON file in the class outside the viewController.
When I create the AllCards object in the view controller obviously return 0 at the beginning but after a while it returns the correct number of cards.
here my questions:
1) How can I wait the object creation before the viewDidLoad so at the view did load the AllCard object will return the correct number of cards?
2) If I add a button in the viewController updating the number of cards it freezes until all the cards have been created. I think because in my code everything is in the main queue. How can I resolve that?
3) Is it a good practice parsing JSON in a separate class like I did?
AllCards class:
import Foundation
import Alamofire
import SwiftyJSON
class AllCards {
var allCard = [Card]()
let dispatchGroup = DispatchGroup()
//gzt the JSON with Alamofire request
let allCardsHTTP: String = "https://omgvamp-hearthstone-v1.p.mashape.com/cards?mashape"
init() {
dispatchGroup.enter()
Alamofire.request(allCardsHTTP, method: .get).responseJSON { (response) in
if response.result.isSuccess {
let jsonCards : JSON = JSON(response.value!)
print("success")
//create the cards
if jsonCards["messagge"].stringValue != "" {
print(jsonCards["message"].stringValue)
}
else {
for (set, value) in jsonCards {
if jsonCards[set].count != 0 {
for i in 0...jsonCards[set].count - 1 {
let card = Card(id: jsonCards[set][i]["cardId"].stringValue, name: jsonCards[set][i]["name"].stringValue, cardSet: set, type: jsonCards[set][i]["type"].stringValue, faction: jsonCards[set][i]["faction"].stringValue, rarity: jsonCards[set][i]["rarity"].stringValue, cost: jsonCards[set][i]["cost"].intValue, attack: jsonCards[set][i]["attack"].intValue, durability: jsonCards[set][i]["durability"].intValue, text: jsonCards[set][i]["text"].stringValue, flavor: jsonCards[set][i]["flavor"].stringValue, artist: jsonCards[set][i]["artist"].stringValue, health: jsonCards[set][i]["health"].intValue, collectible: jsonCards[set][i]["collectible"].boolValue, playerClass: jsonCards[set][i]["playerClass"].stringValue, howToGet: jsonCards[set][i]["howToGet"].stringValue, howToGetGold: jsonCards[set][i]["howToGetGold"].stringValue, mechanics: [""], img: jsonCards[set][i]["img"].stringValue, imgGold: jsonCards[set][i]["imgGold"].stringValue, race: jsonCards[set][i]["race"].stringValue, elite: jsonCards[set][i]["elite"].boolValue, locale: jsonCards[set][i]["locale"].stringValue)
if jsonCards[set][i]["mechanics"].count > 0 {
for n in 0...jsonCards[set][i]["mechanics"].count - 1 {
card.mechanics.append(jsonCards[set][i]["mechanics"][n]["name"].stringValue)
}
}
else {
card.mechanics.append("")
}
self.allCard.append(card)
}
}
else {
print("The set \(set) has no cards")
}
}
print(self.allCard.count)
}
}
else {
print("No network")
}
self.dispatchGroup.leave()
}
}
}
View Controller:
import UIKit
class ViewController: UIViewController {
let allcards = AllCards()
let mygroup = DispatchGroup()
#IBAction func updateBtn(_ sender: Any) {
print(allcards.allCard.count) //Button is frozen until all the cards have been created then it shows the correct number of cards
}
override func viewDidLoad() {
super.viewDidLoad()
print(allcards.allCard.count) / This returns 0
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Here is an example of completion handler.
First you have to write a function in a single class ex: APICall
func getDataFromJson(allCardsHTTP: String, completion: #escaping (_ success: Any) -> Void) {
Alamofire.request(allCardsHTTP, method: .get).responseJSON { response in
if response.result.isSuccess {
completion(response)
}
}
}
and call this method from any class.
let callApi = APICall()
callApi.getDataFromJson(allCardsHTTP: "https://omgvamp-hearthstone-v1.p.mashape.com/cards?mashape",completion: { response in
print(response)
})
1) if you pass an object via a UIStoryboard segue, it's set before viewDidLoad() is called. However if you want to wait for UI elements to be ready, I generally go for a didSet on the UI element, you could add a guardstatement checking your object in there if you want.
2) first of all you'll probably want a closure so maybe read 3) first. you're using dispatchGroup.enter() here, DispatchQueue.global.async { } is the usual way to accomplish what you're doing. Add in a DispatchQueue.main.async { } when done if you want, or dip into the main thread in the view controller, up to you really. Look into the differences between [unowned self]
and [weak self] when you have the time.
3) Give your Card object an init(from: JSON) initialiser where it parses its properties from the JSON object you're passing it.
Let the function responsible for the download (Alamofire in your case) live in a different class (like for example APIClient) and give it a download function with a closure in the argument list like completion: ((JSON?) -> ())? to return the JSON object. Let that class download a JSON object and then initialise your Cardobject with the init(from: JSON) initializer you wrote earlier. Note that this isn't an approach fit for use with Core Data NSManagedObjects so if you'll need local storage maybe keep that in mind.
In the end you should be able to build an array of Cards something like this:
APIClient.shared.fetchCards(completion: { cardJSONs in
let cards = [Card]()
for cardJSON: JSON in cardJSONs {
let card = Card(from; JSON)
cards.append(card)
}
}

Parse large amount of JSON data with swift

I've got to fetch a large amount of data from a remote web server. I decided to use Alamofire for the HTTP request and Swifty JSON to parse JSON response. When the process ends I return back data to a UITableViewController and I feed my table view. Due to the size of JSON (1.5 MB), bind process to object model requires additional time.
Here an example of how I handle the process in my code:
Example of object model:
import Foundation
import SwiftyJSON
class Course {
var id: String!
var students: [JSON]
var hours: [JSON]
var related: [JSON]
init?(_ key: String, json: JSON) {
self.id = key
self.students = json["students"].arrayValue
self.calendar = json["calendar"].arrayValue
self.relatedCourses = json["related_courses"].arrayValue
}
}
Example of Alamofire request:
func fetchAllData(data: Data, completion: #escaping ([String:[String:Any]], [JSON]) -> ()) {) {
let url = "http://myapi/data" // includes courses
Alamofire.request(url, parameters: params)
.validate()
.responseJSON(queue: utilityQueue) {
response in
switch response.result {
case .success(let responseData):
let (results, order) = self.parseResults(json: JSON(responseData))
completion(results, order)
break
case .failure(let error):
AppDelegate.print(value: error)
break
}
}
}
Example of JSON parsing:
func parseResults(json: JSON) -> ([String:[String:Any]], [JSON]){
var results = [String:[String:Any]]()
results["courses"] = getCourses(from: json)
results["students"] = getStudents(from: json)
... I parse other elements here
return (results, order)
}
func getCourses(from json: JSON) -> [String:Course] {
var courses = [String:Course]()
for (key, data) : (String, JSON) in json["courses"] {
courses[key] = Course(key, json: data)!
}
return places
}
I used dictionaries in order to reduce time complexity to access elements. I've got lots of informations to join.
Here TableViewController methods:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
// Call here fetch method
ResultsService().fetchResults(data: data, completion: {
(results, order) in
DispatchQueue.main.async {
self.courses = results["courses"]!
self.students = results["students"]!
...
self.spinner.stopAnimating()
self.tableView.reloadData()
}
})
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CourseCell", for: indexPath) as! CourseCell
let course = courses[orderedCourses[indexPath.row].stringValue] as! Course
let bestStudent = students[course.students[0].stringValue] as! Student
cell.courseName.text = course.name
cell.bestStudentScore.text = bestStudent.score
...
return cell
}
Everything is working fine, but I have to wait 3 or 4 seconds in order to get data displayed in table view. With a little bit of debugging I've pointed out that my bottleneck is binding JSON to objects. Do you have any suggestion useful to improve performances? I was thinking about something like: starting to parse JSON, return small chunks of data and display it in table view but I don't know exactly how to achieve this goal. I can't use pagination so I have to fetch whole data from web server and try to optimise everything from a client point of view. Thank you.

Swift completion handler for Alamofire seemingly not executing

I have the following function in a class in my program:
func getXMLForTrips(atStop: String, forRoute: Int, completionHandler: #escaping (String) -> Void) {
let params = [api key, forRoute, atStop]
Alamofire.request(apiURL, parameters: params).responseString { response in
if let xmlData = response.result.value {
completionHandler(xmlData)
} else {
completionHandler("Error")
}
}
}
In the init() for the class, I attempt to call it like this:
getXMLForTrips(atStop: stop, forRoute: route) { xmlData in
self.XMLString = xmlData
}
This compiles without errors, but after init() is executed, my class's self.XMLString is still nil (shown both by the Xcode debugger and by my program crashing due to the nil value later on). I see no reason why this shouldn't work. Can anyone see what I am missing?
You shouldn't be making internet calls in the initializer of a class. You will reach the return of the init method before you go into the completion of your internet call, which means it is possible that the class will be initialized with a nil value for the variable you are trying to set.
Preferably, you would have another class such as an API Client or Data Source or View Controller with those methods in it. I am not sure what your class with the init() method is called, but lets say it is called Trips.
class Trips: NSObject {
var xmlString: String
init(withString xml: String) {
xmlString = xml
}
}
Then one option is to put the other code in whatever class you are referencing this object in.
I'm gonna use a view controller as an example because I don't really know what you are working with since you only showed two methods.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//setting some fake variables as an example
let stop = "Stop"
let route = 3
//just going to put that method call here for now
getXMLForTrips(atStop: stop, forRoute: route) { xmlData in
//initialize Trip object with our response string
let trip = Trip(withString: xmlData)
}
}
func getXMLForTrips(atStop: String, forRoute: Int, completionHandler: #escaping (String) -> Void) {
let params = [api key, forRoute, atStop]
Alamofire.request(apiURL, parameters: params).responseString { response in
if let xmlData = response.result.value {
completionHandler(xmlData)
} else {
completionHandler("Error")
}
}
}
}
If you want to be able to initialize the class without requiring setting the xmlString variable, you can still do the same thing.
Change the Trips class init() method to whatever you need it to be and set var xmlString = "" or make it optional: var xmlString: String?.
Initialize the class wherever you need it initialized, then in the completion of getXMLForTrips, do trip.xmlString = xmlData.

Swift: Lazy load variable vs MVC model?

I'm building an app with MVC Model.
I use lazy load technical to fill up a variable. (Model)
And this variable is being by one UIViewController (Controller)
But i don't know how to reload or trigger the view controller when the model action is finished. Here is my code
Model (lazy load data)
class func allQuotes() -> [IndexQuotes]
{
var quotes = [IndexQuotes]()
Alamofire.request(.GET, api_indexquotes).responseJSON { response in
if response.result.isSuccess && response.result.value != nil {
for i in (response.result.value as! [AnyObject]) {
let photo = IndexQuotes(dictionary: i as! NSDictionary)
quotes.append(photo)
}
}
}
return quotes
}
And the part of view controller
class Index:
UIViewController,UICollectionViewDelegate,UICollectionViewDataSource {
var quotes = IndexQuotes.allQuotes()
var collectionView:UICollectionView!
override func viewDidLoad() {
This is really serious question, i'm confusing what technic will be used to full fill my purpose?
Since Alamofire works asynchronously you need a completion block to return the data after being received
class func allQuotes(completion: ([IndexQuotes]) -> Void)
{
var quotes = [IndexQuotes]()
Alamofire.request(.GET, api_indexquotes).responseJSON { response in
if response.result.isSuccess && response.result.value != nil {
for photoDict in (response.result.value as! [NSDictionary]) {
let photo = IndexQuotes(dictionary: photoDict)
quotes.append(photo)
}
}
completion(quotes)
}
}
Or a bit "Swiftier"
... {
let allPhotos = response.result.value as! [NSDictionary]
quotes = allPhotos.map {IndexQuotes(dictionary: $0)}
}
I'd recommend also to use native Swift collection types rather than NSArray and NSDictionary
In viewDidLoad in your view controller call allQuotes and reload the table view in the completion block on the main thread.
The indexQuotes property starting with a lowercase letter is assumed to be the data source array of the table view
var indexQuotes = [IndexQuotes]()
override func viewDidLoad() {
super.viewDidLoad()
IndexQuotes.allQuotes { (quotes) in
self.indexQuotes = quotes
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
}
First of all call the function from inside the viewdidLoad. Secondly use blocks or delegation to pass the control back to ViewController. I would prefer the blocks approch. You can have completion and failure blocks. In completions block you can reload the views and on failure you can use alertcontroller or do nothing.
You can see AFNetworking as an example for blocks.
It's async action, just use a callback here:
class func allQuotes(callback: () -> Void) -> [IndexQuotes]
{
var quotes = [IndexQuotes]()
Alamofire.request(.GET, api_indexquotes).responseJSON { response in
if response.result.isSuccess && response.result.value != nil {
for i in (response.result.value as! [AnyObject]) {
let photo = IndexQuotes(dictionary: i as! NSDictionary)
quotes.append(photo)
}
}
callback()
}
return quotes
}
In your UIViewController:
var quotes = IndexQuotes.allQuotes() {
self.update()
}
var collectionView:UICollectionView!
override func viewDidLoad() {
update()
}
private func update() {
// Update collection view or whatever.
}
Actually, I strongly don't recommend to use class functions in this case (and many other cases too), it's not scalable and difficult to maintain after some time.

Populating Table Cells With Alamofire Data

For the better part of the day, I've been attempting to play with Alamofire and use it to gather some API-based data to populate a table. I've successfully managed to get the data into my iOS app (I can println to see it), but I cannot for the life of me figure out the context to use my data to populate the correct number of table rows and set a label.
My data from the web is like so;
{
"members": [
"Bob Dole",
"Bill Clinton",
"George Bush",
"Richard Nixon",
]
}
My TableViewController has code like so;
...
var group: String?
var memberArr = [String]()
var member: [String] = []
...
override func viewDidLoad() {
super.viewDidLoad()
func getData(resultHandler: (data: AnyObject?) -> ()) -> () {
Alamofire.request(.GET, "http://testurl/api/", parameters: ["groupname": "\(group!)"])
.responseJSON { (_, _, JSON, _) in
let json = JSONValue(JSON!)
let data: AnyObject? = json
let memberArr:[JSONValue] = json["members"].array!
for obj in json["members"] {
let member = obj.string!
}
resultHandler(data: data)
}
}
...
...
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return memberArr.count
}
....
My return memberArr.count does not work
What I cannot figure out, however, is how to get my "member" variable to be accessible throughout the controller, as I'd like to use it to return the proper number of rows or use the list of members to dynamically set the title of each cell.
I know this is a novice question, but I've dug through StackOverflow and none of the questions seem to fit in to my situation.
Thank you in advance!
What is happening is that getData has a completion block that run in the background, you need to tell swift to update the table after you finish reading the returned data data, but you need to send this update back in the main thread:
func getData(resultHandler: (data: AnyObject?) -> ()) -> () {
Alamofire.request(.GET, "http://testurl/api/", parameters: ["groupname": "\(group!)"])
.responseJSON { (_, _, JSON, _) in
let json = JSONValue(JSON!)
let data: AnyObject? = json
let memberArr:[JSONValue] = json["members"].array!
for obj in json["members"] {
let member = obj.string!
}
resultHandler(data: data)
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
}
I hope that helps you!

Resources