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.
Related
I'm new to Swift and generally lack of experience in programming. Currently I'm working on a project trying to display a list of star war characters on view controller, but I'm having some issues in passing data through networking Manager. When I ran the program, I couldn't get the name label displayed on the screen.
I have checked the tableView cell and the label is connected with viewController. I feel that the problem is somewhere related with networking manager but couldn't figure out by myself.
var charactersArray: [Characters] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
getCharacters(){
DispatchQueue.main.async {
self.starWarTableViewController.reloadData()
}
}
self.title = "Star War Characters"
// print(charactersArray), returns an empty array
}
private func getURL() -> String {
return "https://swapi.dev/api/people/"
}
func getCharacters(completion: #escaping () -> Void) {
self.starWarTableViewController.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
self.starWarTableViewController.dataSource = self
self.starWarTableViewController.delegate = self
DispatchQueue.global(qos: .background).async {
for i in 1...20 {
NetworkingManager.shared.getDecodedObject(from: self.getURL() + "\(i)"){
(characters: Characters?, error) in
guard let characters = characters else{ return }
self.charactersArray.append(characters)
print(self.charactersArray) //this will return an array list of characters names
}
}
print(self.charactersArray) // here the characterArray is empty
}
completion()
}
For what I found, the issue seems in my networking manager or the table view cell
enum NetworkError: Error {
case invalidURLString
}
final class NetworkingManager{
static let shared = NetworkingManager()
private init(){
}
func getDecodedObject <T: Decodable> (from urlString: String, completion: #escaping (T?, Error?) -> Void){
guard let url = URL(string: urlString) else {
completion(nil, NetworkError.invalidURLString)
return
}
URLSession.shared.dataTask(with: url){
(data, response, error) in
guard let data = data else{
completion(nil, error)
return }
guard let characters = try? JSONDecoder().decode(T.self, from: data) else{ return }
completion(characters, nil)
}.resume()
}
}
class TableViewCell: UITableViewCell {
#IBOutlet weak var nameLabel: UILabel!
func configure (with characters: Characters) {
self.nameLabel.text = characters.name
}
}
Here is Characters
struct Characters: Decodable {
let name: String
enum CodingKeys: String, CodingKey {
case name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
// print(name), this will return a list of character names
}
}
This is the tableview extension
extension ViewController: UITableViewDataSource, UITableViewDelegate{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.charactersArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
cell.configure(with: self.charactersArray[indexPath.row])
// cell.nameLabel.text = "Hi"
return cell
}
}
Right now, you are initiating a series of asynchronous network requests, but calling the completion handler after the requests are initiated, rather than waiting for when them to finish. Thus, when you call completion, the results have not been received.
When you start a bunch of asynchronous tasks and you want to know when they are done, a common pattern is to use DispatchGroup, calling enter before the asynchronous call, calling leave in the completion handler, and then specifying what you want to do when they are all done in a closure supplied to notify. If you put your call to your own completion handler in notify, then it will not get called until the requests are done and you have data to present in the UI.
A few other observations:
getDecodedObject is already asynchronous method, so there's no need to dispatch it to a background queue.
You will want to update both the model and the UI from the main queue. (URLSession calls its completion handlers on a serial background queue.) To accomplish this can basically tell the aforementioned notify to run its closure on the .main queue. I would also not update the model object until you are ready to update the UI.
When performing a series of asynchronous network requests in parallel, you have no assurances of the order in which they may complete. So, you would generally want to use some structure that is independent of the order that the tasks finish, such as a dictionary. Then, when they are done, if you want to build a sorted array of results, then build the array of results from that.
Thus:
func getCharacters(completion: #escaping ([Characters]) -> Void) {
// to keep track of when the 20 requests finish
let group = DispatchGroup()
// temporary structure to keep track of the results
var charactersDictionary: [Int: Characters] = [:]
// perform the requests
for i in 1...20 {
group.enter()
NetworkingManager.shared.getDecodedObject(from: getURL() + "\(i)") { (characters: Characters?, error) in
defer { group.leave() }
guard let characters = characters else { return }
charactersDictionary[i] = characters
}
}
// when the group tells us that all 20 are done, let's build array of the
// results and pass it back to the main queue via the completion handler parameter.
group.notify(queue: .main) {
let results = (1...20).compactMap { charactersDictionary[$0] }
print(results)
completion(results)
}
}
Note, by the way, I have pulled the table configuration code out of this routine (as you really don't want to intermingle network code with UI code):
func configureTableView() {
starWarTableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
starWarTableView.dataSource = self
starWarTableView.delegate = self
}
I've also renamed that outlet to be starWarTableView, because it is not a view controller, but a table view:
Anyway, you would then call it like so:
override func viewDidLoad() {
super.viewDidLoad()
configureTableView()
getCharacters { characters in
self.charactersArray = characters
self.starWarTableView.reloadData()
}
}
I'm stuck on something I don't quite understand as from a few tests, it looks like the generation of table cell is happening before but not as well after a page load and Alamofire request.
If you see below I'm trying to get it to where our museum's outbound shipments are viewed after referencing the pro:
import Alamofire
import SwiftyJSON
class ShipmentProSearchResultsTableViewController: UITableViewController {
var pronumber:String = ""
var shipments = [Shipment]()
typealias JSONStandard = [String: AnyObject]
override func viewDidLoad() {
super.viewDidLoad()
fetchShipments()
}
func fetchShipments() {
let parameters: Parameters = ["pro_number": pronumber]
let todoEndpoint: String = "OURHOST/shipments/api/details/pro"
Alamofire.request(todoEndpoint, method: .get, parameters: parameters)
.responseJSON { response in
if response.result.isSuccess{
let shipmentJSON : JSON = JSON(response.result.value!)
for (index, subJson):(String, JSON) in shipmentJSON{
let proNumber = subJson["proNumber"].int
let consigneeName = subJson["consignee"]["name"].string
let shipment = Shipment(proNumber: proNumber!, consigneeName: consigneeName!)
self.shipments.append(shipment)
}
print(self.shipments)
}else{
print("Could not get results")
}
}
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return shipments.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ShipmentCell", for: indexPath)
let shipment = shipments[indexPath.row]
cell.textLabel?.text = "Hello"
cell.detailTextLabel?.text = "\(shipment.proNumber)"
return cell
}
}
Now where I printed the self.shipments, I get the following results:
[OakRidgeArchaeologicalRepositoryDispatcher.Shipment(proNumber: 471008276, consigneeName: "A1 CHICAGO INSTITUTE OF THE ARTS")]
So I know the data is appropriately being passed to the model. I will also note that the Table View Cell Identifier in the storyboard is correctly set to ShipmentCell. But after the query, nothing pops up in my table.
I'm using Swift 4.
You should reload the tableView after updating source field.
func fetchShipments() {
let parameters: Parameters = ["pro_number": pronumber]
let todoEndpoint: String = "OURHOST/shipments/api/details/pro"
Alamofire.request(todoEndpoint, method: .get, parameters: parameters)
.responseJSON { response in
if response.result.isSuccess{
let shipmentJSON : JSON = JSON(response.result.value!)
for (index, subJson):(String, JSON) in shipmentJSON{
let proNumber = subJson["proNumber"].int
let consigneeName = subJson["consignee"]["name"].string
let shipment = Shipment(proNumber: proNumber!, consigneeName: consigneeName!)
self.shipments.append(shipment)
}
tableView.reloadData() //<-------add this.
print(self.shipments)
}else{
print("Could not get results")
}
}
}
I'm currently in the process of creating an app to display the latest football scores. I've connected to an API through a URL and pulled back the team names for the english premier league into an array of strings.
The problem seems to come from populating the iOS table view that I intend to display the list of teams with. The data appears to be pulled from the API fine, but for some reason the TableView method which creates a cell and returns it doesn't seem to be called. The only time I can get the method to be called is when I actually hard code a value into the array of team names.
Here is my code:
class Main: UIViewController {
var names = [String]()
override func viewDidLoad() {
super.viewDidLoad()
let URL_String = "https://football-api.com/api/?Action=standings&APIKey=[API_KEY_REMOVED]&comp_id=1204"
let url = NSURL(string: URL_String)
let urlRequest = NSURLRequest(URL: url!)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(urlRequest, completionHandler: {
(data, response, error) in
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments)
if let teams = json["teams"] as? [[String : AnyObject]] {
for team in teams {
if let name = team["stand_team_name"] as? String {
self.names.append(name)
}
}
}
} catch {
print("error serializing JSON: \(error)")
}
})
task.resume()
}
// Number of Sections In Table
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
// Number of Rows in each Section
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return names.count
}
// Sets the content of each cell
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
cell.textLabel?.text = names[indexPath.row]
return cell
}
}
Just wondering if anyone can point me in the right direction here. This code doesn't crash or throw any errors, it just refuses to load a table view. The only reason I can possibly think of is that the array of team names is empty after completing a request to the API. However I've set breakpoints throughout and checked the values of local variables and the desired information is being pulled from the API as intended...
you are in the correct way , just refresh the table using reloadData once you got the new data from API
if let teams = json["teams"] as? [[String : AnyObject]] {
for team in teams {
if let name = team["stand_team_name"] as? String {
self.names.append(name)
}
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.yourtableViewname.reloadData()
})
}
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.
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!