What is the time delay between getting data and loading to UITableView - ios

I'm loading my UITableView from an Api call but although the data is retrieved fairly quickly, there is a significant time delay before it is loaded into the table. The code used is below
import UIKit
class TrackingInfoController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var table : UITableView?
#IBOutlet var indicator : UIActivityIndicatorView?
#IBOutlet var spinnerView : UIView?
var tableArrayList = Array<TableData>()
struct TableData
{
var dateStr:String = ""
var nameStr:String = ""
var codeStr:String = ""
var regionStr:String = ""
init(){}
}
override func viewDidLoad() {
super.viewDidLoad()
table!.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
spinnerView?.hidden = false
indicator?.bringSubviewToFront(spinnerView!)
indicator!.startAnimating()
downloadIncidents()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
#IBAction func BackToMain() {
performSegueWithIdentifier("SearchToMainSegue", sender: nil)
}
//#pragma mark - Table view data source
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1 //BreakPoint 2
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableArrayList.count;
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CustomCell") as! CustomTableViewCell
cell.incidentDate.text = tableArrayList[indexPath.row].dateStr
cell.incidentText.text = tableArrayList[indexPath.row].nameStr
cell.incidentCode.text = tableArrayList[indexPath.row].codeStr
cell.incidentLoctn.text = tableArrayList[indexPath.row].regionStr
return cell //BreakPoint 4
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
AppDelegate.myGlobalVars.gIncName = tableArrayList[indexPath.row].nameStr
AppDelegate.myGlobalVars.gIncDMA = tableArrayList[indexPath.row].codeStr
performSegueWithIdentifier("SearchResultsToDetailSegue", sender: nil)
}
func alertView(msg: String) {
let dialog = UIAlertController(title: "Warning",
message: msg,
preferredStyle: UIAlertControllerStyle.Alert)
dialog.addAction(UIAlertAction(title: "Ok", style: .Default, handler: nil))
presentViewController(dialog,
animated: false,
completion: nil)
}
func downloadIncidents()
{
var event = AppDelegate.myGlobalVars.gIncName
var DMA = AppDelegate.myGlobalVars.gIncDMA
if event == "Enter Event Name" {
event = ""
}
if DMA == "Enter DMA" {
DMA = ""
}
let request = NSMutableURLRequest(URL: NSURL(string: "http://incident-tracker-api-uat.herokuapp.com/mobile/events?name=" + event)!,
cachePolicy: .UseProtocolCachePolicy,
timeoutInterval: 10.0)
request.HTTPMethod = "GET"
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
if error != nil {
self.alertView("Error - " + error!.localizedDescription)
}
else {
do {
var incidentList: TableData
if let json = try NSJSONSerialization.JSONObjectWithData(data!, options:.AllowFragments) as? Array<Dictionary<String, AnyObject>> {
for item in json {
if let dict = item as? Dictionary<String, AnyObject> {
incidentList = TableData()
if let nameStr = dict["name"] as? String {
incidentList.nameStr = nameStr
}
if let codeStr = dict["dma"] as? String {
incidentList.codeStr = codeStr
}
if let dateStr = dict["supplyOutageStart"] as? String {
let tmpStr = dateStr
let index = tmpStr.startIndex.advancedBy(10)
incidentList.dateStr = tmpStr.substringToIndex(index)
}
if let regionStr = dict["region"] as? String {
incidentList.regionStr = regionStr
}
self.tableArrayList.append(incidentList)
}
}
self.spinnerView?.hidden = true
self.indicator?.stopAnimating()
self.table?.reloadData() //BreakPoint 3
}
}catch let err as NSError
{
self.alertView("Error - " + err.localizedDescription)
}
}
})
task.resume() //BreakPoint 1
}
When the class is run, it hits BreakPoint 1 first and then hits BreakPoint 2 and then quickly goes to BreakPoint 3, it then goes to BreakPoint 2 once more. Then there is a delay of about 20 to 30 seconds before it hits Breakpoint 4 in cellForRowAtIndexPath() and the data is loaded into the UITableView. The view is displayed quickly afterwards.
The data is retrieved quite quickly from the Web Service so why is there a significant delay before the data is then loaded into the tableView? Is there a need to thread the Web Service method?

You are getting server response in a background thread so you need to call the reloadData() function on the UI thread. I am suspecting that the wait time can vary depending on whether you interact with the app, which effectively calls the UI thread, and that's when the table actually displays the new data.
In a nutshell, you need to wrap the self.table?.reloadData() //BreakPoint 3 with
dispatch_async(dispatch_get_main_queue()) {
// update some UI
}
The final result would be
Pre Swift 3.0
dispatch_async(dispatch_get_main_queue()) {
self.table?.reloadData()
}
Post Swift 3.0
DispatchQueue.main.async {
print("This is run on the main queue, after the previous code in outer block")
}

The table view should begin to reload in a fraction of a second after you call tableView.reloadData().
If you make UI calls from a background thread, however, the results are "undefined". In practice, a common effect I've seen is for the UI changes to take an absurdly long time to actually take effect. The second most likely side-effect is a crash, but other, strange side-effects are also possible.
The completion handler for NSURLSession calls is run on a background thread by default. You therefore need to wrap all your UI calls in a call to dispatch_async(dispatch_get_main_queue()) (which is now DispatchQueue.main.async() in Swift 3.)
(If you are doing compute-intensive work like JSON parsing in your closure it's best to do that from the background so you don't block the main thread. Then make just the UI calls from the main thread.)
In your case you'd want to wrap the 3 lines of code marked with "breakpoint 3" (all UI calls) as well as the other calls to self.alertView()
Note that if you're sure the code in your completion closure is quick you can simply wrap the whole body of the closure in a call to dispatch_async(dispatch_get_main_queue()).

Just make sure you reload your tableview in inside the Dispatch main async, just immediately you get the data

Related

Couldn't figure out where my data was passed in networking Swift

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()
}
}

UICollectionView displays wrong images in cells

I am building a UITableView that is going to have cells with different layouts in them. The cell I am having issues with has a UICollectionView embedded in it that is generated from an API.
The category name and id populate in the cell correctly, but the images in the UICollectionView do not. The images load, but they are not the right ones for that category. Screen capture of how the collection is loading currently
Some of the things I've tried:
Hard-coding the ids for each one of the categories instead of dynamically generating them. When I do this, the correct images load (sometimes but not always) ... and if they do load correctly, when I scroll the images change to wrong ones
The prepareForReuse() function ... I'm not exactly sure where I would put it and what I would reset in it (I have code I believe already kind of nils the image out [code included below])
I have spent a few hours trying to figure this out, but I am stuck ... any suggestions would be appreciated.
My View Controller:
class EcardsViewController: BaseViewController {
#IBOutlet weak var categoryTable: UITableView!
var categories = [CategoryItem]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.categoryTable.dataSource! = self
self.categoryTable.delegate! = self
DispatchQueue.main.async {
let jsonUrlString = "https://*********/******/category"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
if err == nil {
do {
let decoder = JSONDecoder()
let ecardcategory = try decoder.decode(Category.self, from: data)
self.categories = ecardcategory.category
self.categories.sort(by: {$0.title < $1.title})
self.categories = self.categories.filter{$0.isFeatured}
} catch let err {
print("Err", err)
}
DispatchQueue.main.async {
self.categoryTable.reloadData()
}
}
}.resume()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
extension EcardsViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return categories.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "EcardsCategoriesTableViewCell", for: indexPath) as! EcardsCategoriesTableViewCell
cell.categoryName.text = ("\(categories[indexPath.row].title)**\(categories[indexPath.row].id)")
cell.ecardCatId = String(categories[indexPath.row].id)
return cell
}
}
My Table Cell:
class EcardsCategoriesTableViewCell: UITableViewCell {
#IBOutlet weak var categoryName: UILabel!
#IBOutlet weak var thisEcardCollection: UICollectionView!
var ecardCatId = ""
var theseEcards = [Content]()
let imageCache = NSCache<NSString,AnyObject>()
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.thisEcardCollection.dataSource! = self
self.thisEcardCollection.delegate! = self
DispatchQueue.main.async {
let jsonUrlString = "https://**********/*******/content?category=\(self.ecardCatId)"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
if err == nil {
do {
let decoder = JSONDecoder()
let ecards = try decoder.decode(Ecards.self, from: data)
self.theseEcards = ecards.content
self.theseEcards = self.theseEcards.filter{$0.isActive}
} catch let err {
print("Err", err)
}
DispatchQueue.main.async {
self.thisEcardCollection.reloadData()
}
}
}.resume()
}
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
extension EcardsCategoriesTableViewCell: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return theseEcards.count > 7 ? 7 : theseEcards.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "EcardCategoriesCollectionViewCell", for: indexPath) as! EcardCategoriesCollectionViewCell
cell.ecardImage.contentMode = .scaleAspectFill
let ecardImageLink = theseEcards[indexPath.row].thumbSSL
cell.ecardImage.downloadedFrom(link: ecardImageLink)
return cell
}
}
Collection View Cell:
class EcardCategoriesCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var ecardImage: UIImageView!
}
Extension to "download" image:
extension UIImageView {
func downloadedFromReset(url: URL, contentMode mode: UIViewContentMode = .scaleAspectFit, thisurl: String) {
contentMode = mode
self.image = nil
// check cache
if let cachedImage = ImageCache.shared.image(forKey: thisurl) {
self.image = cachedImage
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let image = UIImage(data: data)
else { return }
ImageCache.shared.save(image: image, forKey: thisurl)
DispatchQueue.main.async() {
self.image = image
}
}.resume()
}
func downloadedFrom(link: String, contentMode mode: UIViewContentMode = .scaleAspectFit) {
guard let url = URL(string: link) else { return }
downloadedFromReset(url: url, contentMode: mode, thisurl: link)
}
}
Both UICollectionViewCell and UITableViewCell are reused. As one scrolls off the top of the screen, it is reinserted below the visible cells as the next cell that will appear on screen. The cells retain any data that they have during this dequeuing/requeuing process. prepareForReuse exists to give you a point to reset the view to default values and to clear any data from the last time it was displayed. This is especially important when working with asynchronous processes, such as network calls, as they can outlive the amount of time that a cell is displayed. Additionally, you're doing a lot of non-setup work in awakeFromNib. This method is not called every time a cell is displayed, it is only called the FIRST time a cell is displayed. If that cell goes off screen and is reused, awakeFromNib is not called. This is likely a big reason that your collection views have the wrong data, they're never making their network request when they appear on screen.
EcardsCategoriesTableViewCell:
prepareForReuse should be implemented. A few things need to occur in this method:
theseEcards should be nilled. When a table view scrolls off screen, you want to get rid of the collection view data or else the next time that cell is displayed, it will show the collection view data potentially for the wrong cell.
You should keep a reference to the dataTask that runs in awakeFromNib and then call cancel on this dataTask in prepareForReuse. Without doing this, the cell can display, disappear, then get reused before the dataTask completes. If that is the case, it may replace the intended values with the values from the previous dataTask (the one that was supposed to run on the cell that was scrolled off screen).
Additionally, the network call needs to be moved out of awakeFromNib:
You are only ever making the network call in awakeFromNib. This method only gets called the first time a cell is created. When you reuse a cell, it is not called. This method should be used to do any additional setup of views from the nib, but is not your main entry point in adding data to a cell. I would add a method on your cell that lets you set the category id. This will make the network request. It will look something like this:
func setCategoryId(_ categoryId: String) {
DispatchQueue.main.async {
let jsonUrlString = "https://**********/*******/content?category=\(categoryId)"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
if err == nil {
do {
let decoder = JSONDecoder()
let ecards = try decoder.decode(Ecards.self, from: data)
self.theseEcards = ecards.content
self.theseEcards = self.theseEcards.filter{$0.isActive}
} catch let err {
print("Err", err)
}
DispatchQueue.main.async {
self.thisEcardCollection.reloadData()
}
}
}.resume()
}
}
This will be called in the cellForRowAt dataSource method in EcardsViewController.
EcardCategoriesCollectionViewCell:
This cell has similar issues. You are setting images asynchronously, but are not clearing the images and cancelling the network requests when the cell is going to be reused. prepareForReuse should be implemented and the following should occur within it:
The image on the image view should be cleared or set to a default image.
The image request should be cancelled. This is going to take some refactoring to accomplish. You need to hold a reference to the dataTask in the collection view cell so that you can cancel it when appropriate.
After implementing these changes in the cells, you'll likely notice that the tableview and collection view feel slow. Data isn't instantly available. You'll want to cache the data or preload it some way. That is a bigger discussion than is right for this thread, but it will be your next step.

Get value inside NSURLSession

My codes doesnt work, do you have an idea why? I want to display some data to my UITable from the requested HTTP
class contactView : UITableViewController{
var values: NSMutableArray!
#IBOutlet var tbview: UITableView!
override func viewDidLoad() {
if(signed != 1){
self.navigationItem.title = ""
}else{
let outbtn = UIBarButtonItem(title: "Sign out", style: .Plain, target: self, action: #selector(contactView.out_action))
navigationItem.leftBarButtonItem = outbtn
let reloadData = UIBarButtonItem(title: "Reload", style: .Plain, target: self, action: #selector(contactView.loadData))
navigationItem.rightBarButtonItem = reloadData
//Check Connection
if(reachability.isConnectedToNetwork() == true) {
loadData()
}else{
let alert = UIAlertController(title: "Error Connection", message: "Not Internet Connection", preferredStyle: .ActionSheet)
let alertAct = UIAlertAction(title: "I'll connect later !", style: .Destructive){
(actions) -> Void in
}
alert.addAction(alertAct)
self.presentViewController(alert, animated: true, completion: nil)
}
}
}
func loadData(){
let url = NSURL(string: url_friends)
let to_post = "user=iam&pin=101218"
let request = NSMutableURLRequest(URL: url!)
request.HTTPMethod = "POST"
request.HTTPBody = to_post.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request){
(let data,let response,let error) -> Void in
dispatch_async(dispatch_get_main_queue(),{
do{
self.values = try! NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as! NSMutableArray
print(self.values)
}catch{
print(error)
return
}
})
}
task.resume()
}
I want to display the variable "value" data in my table but error keep occuring, saying it is nil when call in my table function cellForRowAtIndexPath
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cellidentity") as UITableViewCell!
let mainData = values[indexpath.row] as! String
let x = cell.viewWithTag(2) as! UILabel
if(signed != 1){
print("No people")
}else{
let x = cell.viewWithTag(2) as! UILabel
x.text = mainData["name"]
}
return cell
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let x : Int
if(reachability.signed() != 1){
x = 1
}else{
x = values.count
}
return x
}
Yes, the first time you load the table, it would be nil. The dataTaskWithRequest completion block has to explicitly call self.tableview.reloadData(), to tell the table to update itself now that the network request has finished. Remember, dataTaskWithRequest runs asynchronously, meaning that it finishes after the table is already presented. So you have to tell the table to reload itself (and therefore call the UITableViewDataSource methods again).
So you probably want something like:
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
guard data != nil && error == nil else {
print(error)
return
}
do {
if let values = try NSJSONSerialization.JSONObjectWithData(data!, options: []) as? NSMutableArray {
dispatch_async(dispatch_get_main_queue()) {
self.value = values
print(values)
self.tableview.reloadData()
}
}
} catch let parseError {
print(parseError)
return
}
}
task.resume()
Note, before doing forced unwrapping of data with data!, I first guard to make sure it's not nil. Never use ! unless you've know it cannot possibly be nil.
In terms of why your UITableView methods are failing the first time they're called, it's because they're relying upon reachability.signed() or signed. But the real question is whether values is nil or not.
So, perhaps:
var values: NSMutableArray? // make this a standard optional, not an implicitly unwrapped one
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cellidentity") as UITableViewCell!
let x = cell.viewWithTag(2) as! UILabel // btw, using `UITableViewCell` subclasses are more elegant than using cryptic `tag` numbers
if let mainData = values?[indexPath.row] as? [String: String] {
x.text = mainData["name"]
} else {
print("No people")
x.text = "(retrieving data)" // perhaps you want to tell the user that the request is in progress
}
return cell
}
// if `values` is `nil`, return `1`, otherwise returns `values.count`
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return values?.count ?? 1
}
Your code was configured to return one cell if the data was not available, so I've repeated that here, but generally I return zero rows if there's no data. If you do that, it simplifies cellForRowAtIndexPath even more, as it doesn't have to even worry about the "no data" condition at all. But that's up to you.
Now, I've made some assumptions above (e.g. that mainData was really a dictionary given that you are subscripting it with a string "name"). But less important than these little details is the big picture, namely that one should scrupulously avoid using ! forced unwrapping or using implicitly unwrapped optionals unless you know with absolute certainty that the underlying optional can never be nil.

Unable to append strings to array while parsing JSON data

I am having difficulties storing the results retrieved from a JSON source data. I have confirmed the ability to print the data retrieved but it was not able to store into my local array.
My end objective is to actually print in a UITableView the results.
Below is the code for my relevant table view controller :
import UIKit
class CommunityActivityTableViewController: UITableViewController {
var displayNameArr = [String]()
var postDateArr = [String]()
var postDetailArr = [String]()
var testArr = ["teaad"]
override func viewDidLoad() {
super.viewDidLoad()
parseJson()
print(self.displayNameArr.count) //returns 0
print(self.postDateArr.count) //returns 0
print(self.postDetailArr.count) //returns 0
print(self.testArr.count)
print("end")
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return self.displayNameArr.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
print("3")
let cell = tableView.dequeueReusableCellWithIdentifier("Cell_activity", forIndexPath: indexPath)
print("hi")
cell.textLabel?.text = "hi"
cell.detailTextLabel?.text = "test"
return cell
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func makeAttributedString(title title: String, subtitle: String) -> NSAttributedString {
let titleAttributes = [NSFontAttributeName: UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline), NSForegroundColorAttributeName: UIColor.purpleColor()]
let subtitleAttributes = [NSFontAttributeName: UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)]
let titleString = NSMutableAttributedString(string: "\(title)\n", attributes: titleAttributes)
let subtitleString = NSAttributedString(string: subtitle, attributes: subtitleAttributes)
titleString.appendAttributedString(subtitleString)
return titleString
}
func parseJson(){
//MARK: JSON parsing
let requestURL: NSURL = NSURL(string: "<sanitised>")!
let urlRequest: NSMutableURLRequest = NSMutableURLRequest(URL: requestURL)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(urlRequest) {
(data, response, error) -> Void in
let httpResponse = response as! NSHTTPURLResponse
let statusCode = httpResponse.statusCode
if (statusCode == 200) {
print("Everyone is fine, file downloaded successfully.")
do{
let json = try NSJSONSerialization.JSONObjectWithData(data!, options:.AllowFragments)
if let results = json["result"] as? [[String: AnyObject]] {
for result in results {
if let lastname = result["last_name"] as? String {
if let postdate = result["timestamp"] as? String {
if let firstname = result["first_name"] as? String {
if let postdetails = result["post_details"] as? String {
let displayname = firstname + " " + lastname
//print(displayname)
self.displayNameArr.append(displayname)
self.postDateArr.append(postdate)
self.postDetailArr.append(postdetails)
self.testArr.append("haha")
}
}
}
}
}
}
}catch {
print("Error with Json: \(error)")
}
}
}
task.resume()}
}
As per the code above the print results of displaynamearr.count and postDateArr.count and postDetailArr.count returned 0 when it should have returned more than 0 as a result of parseJson() method.
I have printed the display name, postgame and post details variables and they all contain data within so the problem does not lie with the extraction of data but the appending of data into the array.
Appreciate any help provided thanks ! Developed on Xcode 7 and Swift 2.2
Sanitised my JSON source due to sensitive nature of information (i have verified the retrieval of information is OK)
dataTaskWithRequest() is an asynchronous data loading. It loads on the background thread ensuring your UI won't freeze up. So your array will be empty when you this will be getting executed and hence your error. You need to a completion handler like so:
func parseJson(completion: (isDone: Bool) -> ()){
///code
for result in results {
if let lastname = result["last_name"] as? String {
if let postdate = result["timestamp"] as? String {
if let firstname = result["first_name"] as? String {
if let postdetails = result["post_details"] as? String {
let displayname = firstname + " " + lastname
//print(displayname)
self.displayNameArr.append(displayname)
self.postDateArr.append(postdate)
self.postDetailArr.append(postdetails)
self.testArr.append("haha")
}
completion(isDone: True)
}
}
Now in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
parseJson(){ success in
if success{
print(self.displayNameArr.count) //returns a value
print(self.postDateArr.count) //returns a value
print(self.postDetailArr.count) //returns a value
print(self.testArr.count) //This wont because I havent added it in the completion handler
print("end")
self.tableView.reloadData()
}
}
}
All of your UI updates run on the main thread. If you do something like
let task = session.dataTaskWithRequest(urlRequest) {
(data, response, error) -> Void in
// ...
}.resume()
you start a task asynchronously on another thread (not the main thread). Your iPhone is doing a network request and this takes some time. So I guess when your cellForRowAtIndexPath delegate method is called you haven't received any data yet. This is the reason you don't see anything.
The easiest solution to this would be to reload the table view once you have received the data. When you're done with all the parsing in your parseJson method (outside of all the loops) simply run:
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
This forces your table view to update. Remember that you have to run code that updates the UI on the main thread. This is what dispatch_async(dispatch_get_main_queue()) {} does.
EDIT: The answer above was to illustrate the problem to you. The more elegant solution would be to use a completion handler like so:
func parseJson(completionHandler: (Bool) -> Void) {
//do all your json parsing.
//....
dispatch_asyc(dispatch_get_main_queue()) {
//run this if you received the data
//implement some kind of if statement that checks if the parsing was successful
completionHandler(true)
//run this if it failed
completionHandler(false)
}
}
In your viewDidLoad you would do something like
override func viewDidLoad() {
super.viewDidLoad()
//...
parseJson() { success in
tableView.reloadData()
if(success) {
print("success")
}
}
}
If you want to display an activity indicator while data is loaded (which I would recommend) it is easier to use a callback as I've just described.

Swift 2 data is not reloading data when the array of rows is modified

I'm just getting to grips with iOS development and Xcode altogether, and I'm learning it with Swift 2. I'm trying to get some JSON data from a URL, split it up into a swift Array, and display it in a TableView. I have managed to split the JSON data into an Array, but I'm having trouble reloading the table's data to get it to display this. Here's the code I have:
//
// ViewController.swift
// Table JSON
//
// Created by James Allison on 06/11/2015.
// Copyright © 2015 James Allison. All rights reserved.
//
import UIKit
class ViewController: UIViewController {
var cellContent = ["this should be replaced","something","something else"]
#IBOutlet weak var table: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// construct url
let url = NSURL(string: "http://127.0.0.1:8888/json.php")!
let task = NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
// the following will happen when the task is complete
if let urlContent = data {
var webContent = NSString(data: urlContent, encoding: NSUTF8StringEncoding)
// remove []
webContent = webContent?.stringByReplacingOccurrencesOfString("[", withString: "")
webContent = webContent?.stringByReplacingOccurrencesOfString("]", withString: "")
// split by commas
var webContentArr = webContent?.componentsSeparatedByString(",")
var temp = ""
// remove quote marks
for var i = 0; i < webContentArr!.count; i++ {
temp = webContentArr![i]
temp = temp.stringByReplacingOccurrencesOfString("\"", withString: "")
webContentArr![i] = temp
}
print(webContentArr!)
self.cellContent = webContentArr! as! Array
self.table.reloadData()
}
else {
// something failed
print("Error: invalid URL or something.")
}
}
task.resume()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellContent.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
cell.textLabel?.text = cellContent[indexPath.row]
return cell
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
At the moment when I run it the table displays the original cellContent variable, but not the new one. No errors are produced, and the array is printed okay.
Edit: Thanks Joshua for your answer. I ended up using the following code to solve my issue:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.cellContent = webContentArr! as! Array
self.table.reloadData()
})
At a guess, your "will happen when the task is complete" code is being run on some thread/queue other than main, which does not play well with UI updates. Anything that touches the UI must be done on the main queue.
You should adjust your code so that both the replacement of cellContent and the call to your table view to reloadData() are scheduled on the main queue after you're finished processing everything. To do so, wrap both the above-mentioned calls in an async dispatch sent to the main queue:
dispatch_async(dispatch_get_main_queue(), ^{
self.cellContent = webContentArr! as! Array
self.table.reloadData()
});
This will ensure the cellContent array isn't being modified "behind the table view's back" while it's updating the UI on the main queue (bad!) and that the table view doesn't try updating again until it's done with any ongoing updates.
I hope this helps.

Resources