I am loading some JSON data into a UITableView. Each cell has a delete button and when this button is pressed the object is successfully deleted from the server. However, the page doesn't update to reflect this change. I want the table view to reload but without the deleted object. I have tried calling viewDidLoad() and viewDidAppear() as well as several other tricks I've found online. Currently, I am using a workaround that sends the user back to the home page. From there when you click on the page with the table view, it has updated to reflect the changes. However, I cannot seem to make this happen without leaving the view controller and coming back to it. I know that the API calls are working correctly, the page just isn't "refreshing". What should I try to make this work?
Thank you very much! My code for the entire class is below (I've added a couple comments to point out the places I am having problems with)
import UIKit
class Garage: UIViewController, UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.vehicles.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell1", for: indexPath) as UITableViewCell
cell.textLabel?.text = vehicles[indexPath.row].year + " " + vehicles[indexPath.row].make + " " + vehicles[indexPath.row].model
let button = UIButton(type: .custom)
button.backgroundColor = UIColor.green
button.sizeToFit()
cell.accessoryType = .detailButton
return cell
}
func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
print("tapped")
let alert = UIAlertController(title: "Would you like to delete this vehicle?", message: "This action cannot be undone.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: nil))
self.present(alert, animated: true)
alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { action in
print("DELETED")
//
//THIS IS WHERE THE DELETE ACTION IS CALLED
//
var id = self.vehicles[indexPath.row].id
print("got here 1")
let api = "https://api.myapi.com/"
let parameters: [String: String] = ["id": id]
guard let url = URL(string: api + id) else { return }
var request = URLRequest(url: url)
let headers: [String: String] = [
"Content-Type": "application/json"
]
request.allHTTPHeaderFields = headers
request.httpMethod = "DELETE"
let requestBody = try? JSONSerialization.data(withJSONObject: parameters, options: [])
if let requestBody = requestBody {
request.httpBody = requestBody
}
//
//THIS IS WHERE I TRY TO RELOAD THE PAGE
//
print("reloading...")
self.viewDidLoad()
self.viewWillAppear(true)
URLSession.shared.dataTask(with: request) { (data, response, error) in
// print("Data, response, error", data, response, error)
if let data = data {
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:]
print("json", json)
}
DispatchQueue.main.async {
self.tableVehicles.reloadData()
self.view.setNeedsLayout()
self.performSegue(withIdentifier: "GarageToHome", sender: self)
}
}.resume()
//
//THIS IS MY CURRENT WORK-AROUND
//
self.performSegue(withIdentifier: "GarageToHome", sender: self)
}))
}
#IBOutlet weak var label1: UILabel!
struct Vehicle {
var make: String
var model: String
var year: String
var Trim: String
var id: String
init(_ dictionary: [String: Any]) {
self.make = dictionary["make"] as? String ?? ""
self.model = dictionary["model"] as? String ?? ""
self.year = dictionary["year"] as? String ?? ""
self.Trim = dictionary["trim"] as? String ?? ""
self.id = dictionary["id"] as? String ?? ""
}
}
var vehicles = [Vehicle]()
#IBOutlet weak var tableVehicles: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// print("login name:", username)
self.tableVehicles.delegate = self
self.tableVehicles.dataSource = self
self.tableVehicles.reloadData()
}
override func viewWillAppear(_ animated: Bool) {
//vehicles = [Vehicle]()
let defaults = UserDefaults.standard
defaults.synchronize()
let token = UserDefaults.standard.string(forKey: "token")
let isSignedIn = UserDefaults.standard.bool(forKey: "isUserLoggedIn")
var username = UserDefaults.standard.string(forKey: "loginName")
defaults.synchronize()
DispatchQueue.main.async {
self.label1.text = "Welcome, " + username! + "!"
defaults.synchronize()
}
guard let url = URL(string: "https://api.myapi.com") else {return}
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let dataResponse = data,
error == nil else {
print(error?.localizedDescription ?? "Response Error")
return }
do{
let jsonResponse = try JSONSerialization.jsonObject(with:
dataResponse, options: [])
//print(jsonResponse) //Response result
guard let jsonArray = jsonResponse as? [[String: Any]] else {
return
}
for dic in jsonArray{
self.vehicles.append(Vehicle(dic))
}
print(self.vehicles)
DispatchQueue.main.async {
self.tableVehicles.delegate = self
self.tableVehicles.dataSource = self
self.tableVehicles.reloadData()
}
} catch let parsingError {
print("Error", parsingError)
}
}
task.resume()
}
}
First of all, never ever call delegate methods containing will, did and should yourself. Don't do that. Those methods are exclusively called by the framework.
There is a quite easy solution. You know the index path of the deleted row so if the data task succeeds remove the item from the data source array and delete the row in the table view. Reloading the entire table view and updating the data source array from the downloaded data is not necessary.
Anyway you need a clear indicator that the server operation was successful, I don't know if if let data = data in the dataTask is sufficient.
//THIS IS WHERE I TRY TO RELOAD THE PAGE
//
print("reloading...")
URLSession.shared.dataTask(with: request) { (data, response, error) in
// print("Data, response, error", data, response, error)
if let data = data {
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] ?? [:]
print("json", json)
DispatchQueue.main.async {
self.vehicles.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
self.performSegue(withIdentifier: "GarageToHome", sender: self)
}
}
}.resume()
While you are calling both -viewDidLoad and -viewWillAppear(which are reloading your table view and performing your API request to fetch your data and populate your model respectively) one reason you might not be seeing your underlying data model appear to update is that you are calling both before you actually try to send the deletion request to your API endpoint. If you move your call to -viewWillAppear into the completion block of your delete request then that will ensure that your request to fetch the updated data from your API occurs after the deletion is finished.
That is the very least you could do to potentially get this working like you expect, however I would strongly recommend taking a little time to factor out your business and networking logic. You should not manually be calling -viewDidLoad or -viewWillAppear yourself. These are UIViewController lifecycle methods that are called by UIKit for you during the lifecycle of your view controller. Instead I'd recommend (at the very least) factoring out some methods or methods into other classes that handle things like fetching your data model from the server or deleting specific records. Then, you can call those methods from your table view delegate or UIAlertAction in a more re-usable and isolated manner without worrying about any unintended side effects of calling your UIViewController lifecycle methods just to fetch some data again.
So, while I really don't recommend you end up with this as your final code, instead of this:
//THIS IS WHERE I TRY TO RELOAD THE PAGE
//
print("reloading...")
self.viewDidLoad()
self.viewWillAppear(true)
URLSession.shared.dataTask(with: request) { (data, response, error) in
You could do this:
URLSession.shared.dataTask(with: request) { (data, response, error) in
//THIS IS WHERE I TRY TO RELOAD THE PAGE
//
print("reloading...")
self.viewDidLoad()
self.viewWillAppear(true)
Again, I highly recommend, at the very least, extracting your fetch and delete API logic.
You also have several options for how you go about performing the delete action and making it appear to the user that the record was deleted.
As described above, the minimal changes required to what you have would just to ensure that your updated data model is only fetched once the deletion request succeeds.
Alternatively you can optimistically make it appear that your delete request has succeeded by updating your backing data model locally and either deleting the affected row or reloading your table.
Since the former approach is subject to connectivity and networking latency issues it may appear really laggy to the user when they try to delete a record and you lose the ability to take advantage of UITableView animations that make it clear that a row has been deleted.
The latter approach will appear more responsive to the user. For this approach you would need to remove deleted object from your array where you are currently calling -viewDidLoad and -viewWillAppear: and then remove the corresponding row. Something like:
self.vehicles.removeAt(indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
You have a choice of animation types.
One challenge of optimistically deleting the record and row locally in your app is that if the API request fails for some reason then your user experience could appear inconsistent the next time your data model is fetched - it'll appear as if the record was not deleted (which is actually the case). So depending on the importance of consistency and how much error handling you care to do then you'll need to reconcile any errors that come about as part of your deletion request. Here again, you have many options. You can try the delete again behind the scenes, you could alert your user and then add the row back so they can try again, or just do nothing.
Related
I'm creating a IOS program to download json data from url and display in table view of ios. i have issue to download JSON (every 10 second )in loginpage view controller and parse JSON data to tableview controller. Before posting this, i have try to search many times but can't find solution. Below is StoryBoad and the code
Story Board
User will login, after login success, JSON data will be loaded (userlogin = true). Below code in login class loginPage: UIViewController
#IBOutlet weak var usernameLogin: UITextField!
#IBOutlet weak var passwordLogin: UITextField!
#IBAction func loginPress(_ sender: Any) {
username = usernameLogin.text!
password = passwordLogin.text!
let request = NSMutableURLRequest(url: NSURL(string: "http://talectric.com/wp-admin/a_p/users/userlogin.php")! as URL)
request.httpMethod = "POST"
let postString = "username=\(username)&password=\(password)"
request.httpBody = postString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if error != nil {
print("error=\(error!)")
return
}
else
{
do {
let respondString = try JSONSerialization.jsonObject(with: data!, options: []) as? NSDictionary
print(respondString!)
let message = respondString?["message"] as! String
if message == "Check Pass" {
userlogin = true
DispatchQueue.main.async {
let TabViewPageController = self.storyboard?.instantiateViewController(withIdentifier: "TabViewPageID") as! TabViewPage
self.present(TabViewPageController, animated: true, completion: nil)
}
}
else {
DispatchQueue.main.async {
let alertController = UIAlertController(title: "Login", message:
"Username or Password is not correct", preferredStyle: UIAlertController.Style.alert)
alertController.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default,handler: nil))
self.present(alertController, animated: true, completion: nil)
}
}
}
catch let error as NSError {
print(error.debugDescription)
}
}
}
task.resume()
}
after userlogin = true, #objc func taskdo() will load JSON data to nodeidArray but second download not overwrite first element of nodeidArray and nodeidArray is inserted after last element ( i just want nodeidArray to be overwrited)
import UIKit
var timer = Timer()
var userlogin = false
struct Data {}
var username = String()
var password = String()
class loginPage: UIViewController{
var nodeidArray = [String]()
override func viewDidLoad() {
super.viewDidLoad()
timerstart()
}
func timerstart()
{
timer = Timer.scheduledTimer(timeInterval: 5, target: self,selector: #selector(loginPage.taskdo),userInfo:nil,repeats: true)
}
#objc func taskdo()
{
if userlogin == true{
let request = NSMutableURLRequest(url: NSURL(string: "http://talectric.com/wp-admin/a_p/iot/read_all.php")! as URL)
request.httpMethod = "POST"
let postString = "username=\(username)&password=\(password)&authen=wdwfesf9329140dsvfxkciospdkm"
request.httpBody = postString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if error != nil {
print("error=\(error!)")
return
}
else
{
do {
if let respondString = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? NSDictionary {
if let nodedata = respondString.value(forKey: "nodedata") as? NSArray {
for node in nodedata{
if let nodeDict = node as? NSDictionary {
if let nodeid = nodeDict.value(forKey: "nodeid"){
self.nodeidArray.insert(nodeid as! String, at: 0)
}
}
}
}
}
// print(respondString!)
//let message = respondString?["numberofnodeid"] as! Int
//let nodedata = respondString!.value(forKey: "nodedata")//
// let nodeid = (nodedata as AnyObject).value(forKey: "nodeid")
// print(respondString!.value(forKey: "nodedata")!)
print(self.nodeidArray)
let defaults = UserDefaults.standard
defaults.set(self.nodeidArray, forKey: "YourKey")
}
catch let error as NSError {
print(error.debugDescription)
}
}
}
task.resume()
}
}
}
After download JSON in LoginViewController, i can not paste data to tableview controller. I have try to change nodeidArray to static in LoginPage but can't use static variable in #objc func taskdo(). I try UserDefaults also but can't get data in TableViewController (NodeDataPage)
i have test tableview success with Local Variable in Class NodeDataPage but can't test variable from other viewcontroller
import UIKit
class NodeDataPage:
UIViewController,UITableViewDelegate,UITableViewDataSource {
//var nodeidname = ["nodeid1","nodeid2","nodeid3"]
var testArray : [String]() = UserDefaults.standard.objectForKey("YourKey") {
var nodeidname : [NSString] = testArray! as! [NSString]
println(readArray)
}
#IBOutlet weak var tableView: UITableView!
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return nodeidname.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier:"nodeCell",for: indexPath)
cell.textLabel?.text = nodeidname[indexPath.row]
return cell
}}
I specify the questions:
How can i transfer JSON data (download in LoginPage VC) to Table View VC (NodeDataPage VC) ?
How can i run JSON download function in LoginPage VC every 10s with Question 1 also (i try static variable to get in other view and can't run in #objc func taskdo() ?
I want to run JSON download data every 10s after userlogin. Should i put this function in LoginPage VC or other view because i need to get data from server continuously ?
How can i run JSON download func when the app is hidden (not be killed) ?
i have tried to research small part but now it become more complicated. Please help me.
Thank you
Create a separate class like "DataManager" which holds all your data (nodeidArray) as well as makes calls to server to fetch from web service every 10 seconds.
Once user logs in successfully, show the NodeDataPage. In viewDidLoad of this class, create DataManager object and call the method that handles the timer and fetching data from server.
Use notification or observer design pattern to intimate the NodeDataPage to get the data from DataManager and reload the tableview.
Hope I am able to answer your questions.
I am trying to make an app like Instagram. I have parsed json in newsfeed table and their is 1 like button in each cell. When this like button is pressed then it should update the LIKEPOST json that I have and increment the like by 1. The problem I am getting is in updating the like button to like the post for each cell.
I am attaching the code.
This is the function for like button when pressed.
#IBAction func likeBtnPressed(_ sender: Any) {
if !pressed {
let image = UIImage(named: "Like-1.png") as UIImage!
likeBtn.setImage(image, for: .normal)
pressed = true
} else
{
let image = UIImage(named: "liked.png") as UIImage!
likeBtn.transform = CGAffineTransform(scaleX: 0.15, y: 0.15)
UIView.animate(withDuration: 2.0,
delay: 0,
usingSpringWithDamping: 0.2,
initialSpringVelocity: 6.0,
options: .allowUserInteraction,
animations: { [weak self] in
self?.likeBtn.transform = .identity
},
completion: nil)
likeBtn.setImage(image, for: .normal)
pressed = false
likeUpdateServer()
}
}
This is the function for updating the like :
func likeUpdateServer()
{
let userDefaults = UserDefaults.standard
let value = userDefaults.string(forKey: "api_token")
let api_token : String! = value
let post_id = "9231"
print(api_token)
print(post_id)
var request = URLRequest(url: URL(string: "\(URL_BASE)\(LIKEPOST)")!)
request.httpMethod = "POST"
let postString = "api_token=\(api_token!)&&post_id=\(post_id)"
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else { // check for fundamental networking error
print("error=\(String(describing: error))")
print("cant run")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 { // check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(String(describing: response))")
}
else {
let responseString = String(data: data, encoding: .utf8)
print("responseString = \(String(describing: responseString))")
}
}
}
This is my cellForRowAt function :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : PostDataTableViewCell = self.feedTable.dequeueReusableCell(withIdentifier: "contentViewReuse") as! PostDataTableViewCell
let post = posts[indexPath.row]
print(post)
cell.configureCell(post : post)
return cell
}
If your request parameters are right then after your making a dataTask then you need to resume it, so at the end of the likeUpdateServer method you should add this line
task.resume()
After you create the task, you must start it by calling its
resume() method.
You can find the indexpath of the cell by using the following code.
let point : CGPoint = sender.convert(.zero, tableView)
let indexPath = tableView!.indexPathForItem(at: point)
Add this code in your button click method and using the indexPath you can find the postId.
Also, You should flag the liked cells (I hope it will return from the server) Once you liked a cell you can update it in local post model.
I think I know what you want to do.
Every time a user clicks a button, you want to execute a JSON request and update a button.
If so, you’ll have to make an array or a dictionary and store on/off values in it. In the likeBtnPressed function, check for tableView cells indexPath.row and update a value in that “on/off” array at that index.
I think, you mistake reduce cell of Tableview. Therefore, It repeat button like in cell other. You set true or false in function cellForRow of cell. You check true or false. If true, You show button like or not.
First, make an array or a dictionary to store, if tableView buttons are turned on at the top of your viewController code:
var buttonStates = [Bool]()
In the viewDidLoad, fill the array with proper values. You can also make JSON requests to get likes from a server
for i in 0..<numberOfPosts {
buttonStates.append(false)
//in my case, all buttons are off, but be sure to implement your own logic here
}
In the likeBtnPressed function, find the indexPath. The simplest way is to try this:
print(sender.superview?.superview)
If this doesn’t work, simply add more
print(sender.superview?.superview?.superview)
Once print() outputs your tableViewCell class in the debuger, save it in a constant
let cell = ...
Then check for indexPath and change button state in the likeBtnPresses action
if let cellWithClass = cell as? MyClass {
if let indexPath = tableView.indexPath(for: cellWithClass) {
//change button state and make JSON requests here
buttonStates[indexPath.row] = !buttonStates[indexPath.row]
}
}
I suggest that you then reload the tableView
tableView.reloadData()
In the cellForRowAt, check, if the button is on or off
if buttonStates[indexPath.row] {
//on
} else {
/off
}
That should work, as far as I know (and replying from a phone 😂)
when i segue to a viewController with tableView, the tableviewcell immediately send a request to the server using the method fetchUserAvatar(avatarName: handler: (String) -> Void). and this method returns a url that link to the image. download it and cache the image cacheImage is an object of NSCache<NSString, UIImage>. this object cacheImage was initalised in previous view controller and being assigned from pervious to this viewContoller using prepare(for segue: UIStoryboardSegue, sender: Any?). when this viewController shows up, I can't see the image in the cell. but i pop the viewController out and segue to this viewController with tableView again. the image will show. I think(I guess) because
the images weren't all fully downloaded yet. so, i can't see the images. but if i pop out the viewController and load an object of viewController and the viewController gets the images from cache. therefore, the images could be shown.
I want to know how to avoid this problem? thanks.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath) as! MessageCell
let row = indexPath.row
cell.content.text = messages[row].content
cell.date.text = messages[row].createdDateStrInLocal
cell.messageOwner.text = messages[row].user
if let avatar = cacheImage.object(forKey: messages[row].user as NSString){
cell.profileImageView.image = avatar
} else {
fetchUserAvatar(avatarName: messages[row].user, handler: { [unowned self] urlStr in
if let url = URL(string: urlStr), let data = try? Data(contentsOf: url), let avatar = UIImage(data: data){
self.cacheImage.setObject(avatar, forKey: self.messages[row].user as NSString)
cell.profileImageView.image = avatar
}
})
}
return cell
}
fileprivate func fetchUserAvatar(avatarName: String, handler: #escaping (String) -> Void){
guard !avatarName.isEmpty, let user = self.user, !user.isEmpty else { return }
let url = URL(string: self.url + "/userAvatarURL")
var request = URLRequest(url: url!)
let body = "username=" + user + "&avatarName=" + avatarName
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.httpBody = body.data(using: .utf8)
defaultSession.dataTask(with: request as URLRequest){ data, response, error in
DispatchQueue.main.async {
if let httpResponse = response as? HTTPURLResponse {
if 200...299 ~= httpResponse.statusCode {
print("statusCode: \(httpResponse.statusCode)")
if let urlStr = String(data: data!, encoding: String.Encoding.utf8), urlStr != "NULL" {
handler(urlStr)
}
} else {
print("statusCode: \(httpResponse.statusCode)")
if let unwrappedData = String(data: data!, encoding: String.Encoding.utf8) {
print("POST: \(unwrappedData)")
self.warning(title: "Fail", message: unwrappedData, buttonTitle: "OK", style: .default)
} else {
self.warning(title: "Fail", message: "unknown error.", buttonTitle: "OK", style: .default)
}
}
} else if let error = error {
print("Error: \(error)")
}
}
}.resume()
}
I modified it, and move the download code in viewdidload and reload the tableview, the result is the same.
Does your image view have a fixed size, or are you taking advantage of the intrinsic size? Based upon your description, I'd assume the latter. And updating the cache and reloading the cell inside fetchUserAvatar completion handler should resolve that problem.
But you have two issues here:
You should really use dataTask to retrieve the image, not Data(contentsOf:) because the former is asynchronous and the latter is synchronous. And you never want to do synchronous calls on the main queue. At best, the smoothness of your scrolling will be adversely affected by this synchronous network call. At worst, you risk having the watch dog process kill your app if the network request is slowed down for any reason and you block the main thread at the wrong time.
Personally, I'd have fetchUserAvatar do this second asynchronous request asynchronously and change the closure to return the UIImage rather than the URL as a String.
Perhaps something like:
fileprivate func fetchUserAvatar(avatarName: String, handler: #escaping (UIImage?) -> Void){
guard !avatarName.isEmpty, let user = self.user, !user.isEmpty else {
handler(nil)
return
}
let url = URL(string: self.url + "/userAvatarURL")!
var request = URLRequest(url: url)
let body = "username=" + user + "&avatarName=" + avatarName
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.httpBody = body.data(using: .utf8)
defaultSession.dataTask(with: request) { data, response, error in
guard let data = data, error == nil, let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else {
print("Error: \(error?.localizedDescription ?? "Unknown error")")
DispatchQueue.main.async { handler(nil) }
return
}
guard let string = String(data: data, encoding: .utf8), let imageURL = URL(string: string) else {
DispatchQueue.main.async { handler(nil) }
return
}
defaultSession.dataTask(with: imageURL) { (data, response, error) in
guard let data = data, error == nil else {
DispatchQueue.main.async { handler(nil) }
return
}
let image = UIImage(data: data)
DispatchQueue.main.async { handler(image) }
}.resume()
}.resume()
}
This is a more subtle point, but you should not use the cell inside the asynchronously called completion handler closure. The cell could have scrolled out of view and you could be updating the cell for a different row of the table. This is likely only to be problematic for really slow network connections, but it's still an issue.
Your asynchronous closure should be determining the index path of the cell and then reloading just that index path with reloadRows(at:with:).
For example:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath) as! MessageCell
let row = indexPath.row
cell.content.text = messages[row].content
cell.date.text = messages[row].createdDateStrInLocal
cell.messageOwner.text = messages[row].user
if let avatar = cacheImage.object(forKey: messages[row].user as NSString){
cell.profileImageView.image = avatar
} else {
cell.profileImageView.image = nil // make sure to reset this first, in case cell is reused
fetchUserAvatar(avatarName: messages[row].user) { [unowned self] avatar in
guard let avatar = avatar else { return }
self.cacheImage.setObject(avatar, forKey: self.messages[row].user as NSString)
// note, if it's possible rows could have been inserted by the time this async request is done,
// you really should recalculate what the indexPath for this particular message. Below, I'm just
// using the previous indexPath, which is only valid if you _never_ insert rows.
tableView.reloadRows(at: [indexPath], with: .automatic)
}
}
return cell
}
Frankly, there are other subtle issues here (e.g. if your username or avatar name contain reserved characters, your requests will fail; if you scroll quickly on a really slow connection, images for visible cells will get backlogged behind cells that are no longer visible, you risk timeouts, etc.). Rather than spending a lot of time contemplating how to fix these more subtle issues, you might consider using an established UIImageView category that performs asynchronous image requests and supports caching. Typical options include AlamofireImage, KingFisher, SDWebImage, etc.
Let me preface this by saying I'm VERY new to Swift 2 and am building my first app which calls an api (php) for data (JSON). The problem I'm running into is when I make the call to the api the other functions ran before the api can send back the data.
I've researched some type of a onComplete to call a functions after the api response is done. I'm sure for most of you this is easy, but I cant seem to figure it our.
Thanks in advance!
class ViewController: UIViewController {
var Selects = [Selectors]()
var list = [AnyObject]()
var options = [String]()
var index = 0
#IBOutlet var Buttons: [UIButton]!
override func viewDidLoad() {
super.viewDidLoad()
self.API()
self.Render()
}
func API() {
let url = NSURL(string: "http:api.php")
let request = NSMutableURLRequest(URL: url!)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if data == nil {
print("request failed \(error)")
return
}
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments)
if let songs = json["songs"] as? [[String: AnyObject]] {
for song in songs {
self.list.append(song)
}
}
self.Selects = [Selectors(Name: self.list[self.index]["name"] as? String, Options: self.BuildOptions(), Correct: 2)]
}
catch let error as NSError {
print("json error: \(error.localizedDescription)")
}
}
task.resume()
}
func BuildOptions() {
// BuildOptions stuff happens here
}
func Render() {
// I do stuff here with the data
}
}
So I assume your Render() method is called before data gets back from the api? Keeping your api-calling code in the view controllers is bad design, but as you're new i won't expand on that. In your case it's as simple as not calling your Render() method in viewDidLoad() - call it after you're done with parsing the data from JSON (after the self.Selects = [Selectors... line). NSURLSession.sharedSession().dataTaskWithRequest(request) method is called asynchronously , and the callback block with data, response, error parameters is executed after this method is done with fetching your data, so it can happen after the viewDidLoad is long done and intially had no data to work on as the asynchronous method was still waiting for response from the API.
Edit - speaking of handling api calls, it's a wise thing to keep them separated from specific view controllers to maintain a clean reusable code base. You should call the API and wait for a callback from it, so i'd just do that to your API function, it would look like this:
static func callAPI(callback: [AnyObject]? -> Void ) {
let url = NSURL(string: "http:api.php")
let request = NSMutableURLRequest(URL: url!)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if data == nil {
completion(nil)
}
do {
var list = [AnyObject]()
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments)
if let songs = json["songs"] as? [[String: AnyObject]] {
for song in songs {
self.list.append(song)
}
}
completion(list)
}
catch let error as NSError {
print("json error: \(error.localizedDescription)")
completion(nil)
}
}
task.resume()
}
Generally speaking methods should do one specific thing - in your case call the api and return data or error. Initialize your selectors in the view controllers on callback. Your view controller's viewDidLoad would look like this using the code above:
override func viewDidLoad() {
super.viewDidLoad()
YourApiCallingClass.callApi() {
result in
if let list = result {
self.list = list
self.Selects = [Selectors(Name: self.list[self.index]["name"] as? String, Options: self.BuildOptions(), Correct: 2)]
self.Render()
} else {
//Handle situation where no data will be returned, you can add second parameter to the closue in callApi method that will hold your custom errors just as the dataTaskWithRequest does :D
}
}
}
Now you have a nice separation of concerns, API method is reusable and view controller just handles what happens when it gets the data. It'd be nice if you slapped an UIActivityIndicator in the middle of the screen while waiting, it'd look all neat and professional then :P
I have two tableViews, The first table view has fixed data which never will be changed. When a user taps on a specific cell for example cell number 1, an API 1 is called and the 2nd table view is loaded with the returned data when cell number 2 is tapped API 2 is called the 2nd table view is loaded with the returned data.
To solve this issue I have tried this.
In my first Table View I record of which table cell was tapped and send that information to the 2nd table view via prepare for segue:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let destination = segue.destinationViewController as? BioListTableViewController {
let indexPath = self.tableView.indexPathForSelectedRow()
if let row:Int = indexPath?.row {
// PASS which cell was tapped by the user
destination.cellTapped = row
}
}
}
Then within my 2nd table view I use a switch statement which checks whether the cell tapped was 0,1,2 and so on. And based on that a switch case is run. Each switch case has a different function which calls a different API. See below:
import UIKit
struct Note {
var name:String
var job:String
}
struct WeatherSummary {
var id: String
}
class BioListTableViewController: UITableViewController {
var cellTapped = Int()
#IBOutlet var tableview: UITableView!
private var notes = Array<Note>()
var bioArray = NSArray(){
didSet{
tableview.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
switch cellTapped {
case 0:
test()
case 1:
testTwo()
default:
println("Error")
}
var newItem:Note = Note(name: "", job: "")
for x in bioArray {
if let id = x["employeeName"] as? String{
newItem.name = id
}
}
}
func test() {
println("This is TWEST")
var weatherArray = [WeatherSummary]()
var request = NSMutableURLRequest(URL: NSURL(string: "myAPI-Link")!)
var session = NSURLSession.sharedSession()
request.HTTPMethod = "GET"
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
var err: NSError?
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
var task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
println("Response: \(response)")
var strData = NSString(data: data, encoding: NSUTF8StringEncoding)
println("Body: \(strData)")
var err: NSError?
var json = NSJSONSerialization.JSONObjectWithData(data, options: .MutableLeaves, error: &err) as? NSArray
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
// Did the JSONObjectWithData constructor return an error? If so, log the error to the console
if(err != nil) {
println(err!.localizedDescription)
let jsonStr = NSString(data: data, encoding: NSUTF8StringEncoding)
println("Error could not parse JSON: '\(jsonStr)'")
dispatch_async(dispatch_get_main_queue()) {
var alert = UIAlertController(title: "Alert", message: "Oops! Wrong Details, Try Again", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
}else {
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
// The JSONObjectWithData constructor didn't return an error. But, we should still
// check and make sure that json has a value using optional binding.
var newWeather = WeatherSummary(id:"")
if let parseJSON = json {
for weather in parseJSON {
if let id = weather["employeeName"] as? String{
println(" LOOK HERE \(id)")
newWeather.id = id
}
}
weatherArray.append(newWeather)
self.bioArray = parseJSON
} else {
// Woa, okay the json object was nil, something went worng. Maybe the server isn't running?
let jsonStr = NSString(data: data, encoding: NSUTF8StringEncoding)
println("Error could not parse JSON: \(jsonStr)")
}
}
})
task.resume()
}
func testTwo(){
println("THIS IS TEST 2")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Potentially incomplete method implementation.
// Return the number of sections.
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete method implementation.
// Return the number of rows in the section.
return self.bioArray.count ?? 0
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("bioCell", forIndexPath: indexPath) as! UITableViewCell
let weatherSummary: AnyObject = bioArray[indexPath.row]
if let id = weatherSummary["employeeName"] as? String //Dont know the exact syntax.
{
cell.textLabel?.text = id
}
if let job = weatherSummary["jobTitle"] as? String {
cell.detailTextLabel?.text = job
}
return cell
}
}
My Issue:
My Issue is that when I println the returned data it is being printed and I can see it. But my 2nd table view is empty. It does not display the data. I am not sure why the data is not being displayed on the 2nd table view. I can see the data by using println which proves that the API is actually returning real data.
Any suggestions?
Apologies for any mistakes. Please let me know if I have made a mistake and I will fix it.
Thank you.
Hard to answer this without seeing the data or the Storyboard. But a few things you should check set a breakpoint and make sure it is calling:
didSet method of bioArray
is numberOfRowsInSection hit and returning something?
is the delegate and datasource set properly?
is cellForRowAtIndexPath hit and do you have keys named: employeeName and jobTitle
Please check these things first.
I figured out how to fix the issue. Thought I would answer my own question.
Basically I had to reload my table view to display the data.
So I did the following:
didSet{
dispatch_async(dispatch_get_main_queue()) {
self.tableview.reloadData()
}
}
By doing the above my issue was solved. Once I tapped the cell, it took a few seconds but it reloaded my table view once the data was returned from the API and displayed it.