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!
Related
I believe that I misunderstood some conception in Swift and can assign received array to my instance variable. Can somebody explain why overall my announcementsList array has 0 elements?
UIViewController.swift
var announcementsList: [Announcement] = []
override func viewDidLoad() {
super.viewDidLoad()
api.getAnnouncements(){ announcements in //<- announcements is array which has 12 elements
for ann in announcements{
self.announcementsList.append(ann)
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return announcementsList.count //<- have 0 here
}
API.swift
func getAnnouncements(completion: #escaping ([Announcement]) -> ()){
var announcements: [Announcement] = []
let url = URL(string: "https://api.ca/announcements")!
let task = self.session.dataTask(with: url) {
data, response, error in
if let data = data,
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
guard let announcements_json = json!["announcements"] as? [[String: Any]]
else { return }
for announcement in announcements_json{
let title = announcement["title"] as! String
let desc = announcement["description"] as! String
announcements.append(Announcement(title: title,desc: desc))
}
}
completion(announcements)
}
task.resume()
}
P.S.: In my defence, I should say code works in Java pretty well
UPD
In UIViewController.swift if glance inside my announcementsList in viewWillDisappear() I will get my objects there. So I assume that tableView() started count elements earlier then they became reassigned in viewDidLoad().
The question now how to assign objects inide viewDidLoad() to a new array faster than tableView() start count them.
var announcementsList: [Announcement] = [] {
didSet {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
api.getAnnouncements { announcements in
self.announcementsList = announcements
}
}
The operation is asynchronous , so the tableView reloads when the VC appears and at that moment the response isn't yet return
for ann in announcements{
self.announcementsList.append(ann)
}
self.tableView.reloadData()
BTW why not
self.announcementsList = announcements
self.tableView.reloadData()
Also don't know current thread of callback , so do
DispatchQueue.main.async {
self.announcementsList = announcements
self.tableView.reloadData()
}
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.
I have an issue with code order in a ItemsViewController.swift
When I run my code it starts the for items loop before my api returns the values for the items. This is done in the line: self.viewModel/getItemsTwo... Therefore it thinks that items is nil by the time the loop starts, so it errors with:
fatal error: unexpectedly found nil while unwrapping an Optional value
How can I start the loop only after items has been filled by the api call/function call?
class ItemsViewController: UIViewController {
private let viewModel : ItemsViewModel = ItemsViewModel()
override func viewDidLoad() {
super.viewDidLoad()
self.viewModel.getItemsTwo(self.viewModel.getCurrentUser())
var items = self.viewModel.items
for item in items! {
print(item)
}
}
...
The getItemsTwo function in the viewModel sets the viewModel.items variable when it is called
EDIT 1
ItemsViewModel.swift
...
var items : JSON?
...
func getItemsTwo(user: MYUser) {
let user_id = user.getUserId()
let url = String(format:"users/%#/items", user_id)
self.get(url).responseJSON { (response) -> Void in
let dataExample = response.data
var newdata = JSON(data: dataExample!)
self.items = newdata
}
}
...
EDIT 2
I am trying to do this:
just change it in the ViewController to:
var items = self.viewModel.getItemsTwo(self.viewModel.getCurrentUser())
and the ViewModel to:
func getItemsTwo(user: MYUser) -> JSON {
let user_id = user.getUserId()
let url = String(format:"users/%#/items", user_id)
self.get(url).responseJSON { (response) -> Void in
let dataExample = response.data
var newdata = JSON(data: dataExample!)
self.items = newdata
}
return self.items
}
But the return statement still errors as if self.items in nil.
Maybe you could expand your getItemsTwo method to take a callback closure, something like:
func getItemsTwo(user: MYUser, callback: (items: [JSON])-> Void)
Meaning that you have a parameter called callback which is a closure function that returns Void and takes an array of JSON items as an input parameter.
Once you have added newdata to self.items you could call your callback closure like so:
func getItemsTwo(user: MYUser, callback: (items: [JSON])-> Void) {
let user_id = user.getUserId()
let url = String(format:"users/%#/items", user_id)
self.get(url).responseJSON { (response) -> Void in
let dataExample = response.data
var newdata = JSON(data: dataExample!)
self.items = new data
//Items are now populated, call callback
callback(items: self.items)
}
}
And then, in your ItemsViewController you could say:
override func viewDidLoad() {
super.viewDidLoad()
self.viewModel.getItemsTwo(self.viewModel.getCurrentUser()) { items in
for item in items {
print(item)
}
}
}
Notice that if you add a closure as the last parameter you can use a so called "Trailing Closure" and place it "outside" or "after" your function as described in this chapter of "The Swift Programming Language".
Hope that helps you (I haven't checked in a compiler so you might get some errors, but then well look at them OK :)).
I call a function to get data from REST api and fill array of objects with result data, I use (inout parameter) as mentioned in apple documentation But the array does not change
My code
static func getData(tab : String, inout listItems : [Item], table: UITableView) -> (){
listItems.removeAll();
Alamofire.request(.GET, baseURL + currencyAPI, parameters: ["tab":tab]).responseArray { (response: Response<[Item], NSError>) in
let items = response.result.value
if let items = items {
listItems.appendContentsOf(items)
table.reloadData()
}
}
}
and function call
APIManager.getData("syp", listItems: &listItems, table: sypTable)
Update: when I put API request inside viewDidLoad function it works correctly
override func viewDidLoad() {
super.viewDidLoad()
//APIManager.getData("syp", listItems: &listItems, table: sypTable)
Alamofire.request(.GET, baseURL + currencyAPI, parameters: ["tab":"syp"]).responseArray { (response: Response<[Item], NSError>) in
let items = response.result.value
if let items = items {
self.listItems = items
self.sypTable.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.