Avoid re-loading UITableView data - ios

I have a problem where a table shows a long list of drills that I want to remain at their current scroll position when user navigates away and then returns.
In my view controller, I have code that loads the data for a UITableView whenever the view appears by calling getDrillList(optimize: true) which stores the data in a property called drillListArray.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
getDrillList(optimize: true)
}
Here is the code that loads the data
private func getDrillList(optimize: Bool = false)
{
// MAKE API CALL, THE ARRAY IS POPULATED IN THE COMPLETE HANDLER
let appDelegate = UIApplication.shared.delegate as! AppDelegate
SharedNetworkConnection.apiGetDrillList(apiToken: appDelegate.apiToken, limit: (optimize ? 13 : 0), completionHandler: { data, response, error in
guard let data = data, error == nil else { // check for fundamental networking error
print("error=\(error)")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 { // check for http errors
// 403 on no token
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(response)")
SharedNetworkConnection.apiLoginWithStoredCredentials(completionHandler: { data, response, error in
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let json = try? JSONSerialization.jsonObject(with: data!, options: [])
if let dictionary = json as? [String: Any] {
if let apiToken = dictionary["token"] as? (String) {
appDelegate.apiToken = apiToken
self.getDrillList()
}
}
})
return
}
self.drillListParser = DrillListParser(jsonString: String(data: data, encoding: .utf8)!)
self.drillListArray = (self.drillListParser?.getDrillListArray())!
DispatchQueue.main.async {
self.drillTableView.reloadData()
}
if optimize {
self.getDrillList()
}
})
}
Two questions
First, if user is shown a segue to another view controller and then returns via Back, how can I check if the data is already loaded to avoid loading it a second time? Is it safe to check if the array is empty?
Second, are there any reprocussions I should be aware of with this approach?

Add getDrillList(optimize: true) in viewDidLoad() and it will calls once in lifecycle or you can put a check that checks if already loaded a data or not via a boolean flag.

Related

Alamofire ignoring closure that sets/handles data

I am using Alamofire to perform a network request to the dummy data source https://jsonplaceholder.typicode.com/posts and render it in my application.
I have a file called NetworkingClient.swift that abstracts most of this logic out and allows is to be reused.
public class NetworkingClient {
typealias WebServiceResponse = ([[String: Any]]?, Error?) -> Void
func execute(_ url: URL, completion: #escaping WebServiceResponse) {
Alamofire.request(url).validate().responseJSON { response in
print(response)
if let error = response.error {
completion(nil, error)
} else if let jsonArray = response.result.value as? [[String: Any]] {
completion(jsonArray, nil)
} else if let jsonDict = response.result.value as? [String: Any] {
completion([jsonDict], nil)
}
}
}
}
I call the execute in a set up function I have in my main view controller file:
func setUpView() {
let networkingClient = NetworkingClient()
let posts_endpoint = "https://jsonplaceholder.typicode.com/posts"
let posts_endpoint_url = URL(string: TEST_URL_STRING)
networkingClient.execute(posts_endpoint_url) { (json, error) in
if let error = error {
print([["error": error]])
} else if let json = json {
print(json)
}
}
}
Where I call this inside viewDidLoad() under super.viewDidLoad()
I've set breakpoints inside the response in closure and I wasn't able to trigger any of them, in fact I think it's skipping the entire thing completely and I don't know why.
I am following this youtube video where the video guide does the exact same thing except their request goes through.
What am I missing?
I am using Swift 4, XCode 10, running on iOS 12.1 and my AlamoFire version is 4.7.
It's all about async stuff.your are declaring NetworkingClient object in func called setupView and Alamofire using .background thread to do stuff.so time executing of networkingClient.execute is not clear and after that setUpView deallocate from memory and all it's objects are gone including NetworkingClient.so for preventing this just declare let networkingClient = NetworkingClient() outside of function

How to make dataTaskWithRequest to be chronological in swift(Xcode 9, swift 4)?

I have this question for Xcode9 Swift 4. I am trying to fetch some data from some api, and I need to use these data to display. However, since the urlSession is highly asynchronous, I cannot get the data at the right time (most of the time the data is nil). Here is the code.
func getUserInfo(){
let data = user!.Data as? [String : Any] ?? nil
if let data = data{
let ID = data["ID"] as? Int ?? nil
if let ID = ID{
let jsonUrlString = "SomeString"
let requestUrl = URL(string: jsonUrlString)
var request = URLRequest(url: requestUrl!)
request.httpMethod = "GET"
request.setValue("SomeKey", forHTTPHeaderField: "AppKey")
request.setValue(md5("Someinfo"), forHTTPHeaderField: "Sign")
dataTask = URLSession.shared.dataTask(with: request){(data, response, err) in
guard let data = data, err == nil else { // check for fundamental networking err
print("error=\(String(describing: err))")
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))")
}
do{
guard let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [Any] else{return}
var userInfo = userDetail(json: json)
let dataDic = userInfo.dataArray as? [String:Any] ?? nil
userInfo.ID = dataDic?["ID"] as? Int
userInfo.AccountName = dataDic?["AccountName"] as? String
userInfo.Avatar = dataDic?["Avatar"] as? String
} catch let jsonErr{
print(jsonErr)
}
}
dataTask?.resume()
}
}
}
I am storing the data into variable userInfo, which has properties like ID, Account Name, and Avatar. But when I call the function in another method "configNavigationBar", it cannot initialize userInfo for me.
func configNavigationBar(){
getUserInfo()
if dataTask?.state == .completed{
navigationItem.title = userInfo?.AccountName
navigationItem.setHidesBackButton(true, animated: true)
navigationController?.navigationBar.prefersLargeTitles = true
}
}
Can anybody help me with the question! I deeply appreciate any help.
How about changing title after successful http request? In a callback. You can configure everything except title before receiving data.
func configNavigationBar(){
getUserInfo { accountName in
self.navigationItem.title = accountName
}
navigationItem.setHidesBackButton(true, animated: true)
navigationController?.navigationBar.prefersLargeTitles = true
}
func getUserInfo(_ callback: #escaping (String) -> Void) {
...
var userInfo = userDetail(json: json)
let dataDic = userInfo.dataArray as? [String:Any] ?? nil
userInfo.ID = dataDic?["ID"] as? Int
userInfo.AccountName = dataDic?["AccountName"] as? String
userInfo.Avatar = dataDic?["Avatar"] as? String
// here's the insertion
callback(userInfo.AccountName)
// end of insertion
....
}
You might also need to wrap ui update into main thread if http request is using background thread.
getUserInfo { accountName in
DispatchQueue.main.async {
self.navigationItem.title = accountName
}
}
You simply need to wait until your data has been downloaded.
While download is busy you should decide to show temporary state on your navigation bar.
Then, when download is finished (or fails), you update the navigation bar again.

API call function with completion handler crashes when accessed from different VC

Can someone fix my function code because I have created a API call function which will get the imageURL for the specific object in my class and display the results in the second view controller. I have created custom completion handler so that the code from second VC is only executed when dowloading of the imageURL is completed.
However, when I am testing this function in the second view controller to print me data that it has arrived I am getting a crash on the print statement line.
Here is the code for my API call function located in Model class file:
func parseImageData(finished: () -> Void) {
let urlPath = _exerciseURL
let url = URL(string: urlPath!)
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print("Error while parsing JSON")
}
else {
do {
if let data = data,
let fetchedImageData = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves) as? [String:Any],
let images = fetchedImageData["results"] as? [[String: Any]] {
for eachImage in images {
let imageUrl = eachImage["image"] as! String
self._imageUrl = URL(string: imageUrl)
}
print(self._imageUrl)
}
}
catch {
print("Error while parsing data.")
}
}
}
task.resume()
finished()
}
And here in the second view controller I am just testing if I can access the code block:
override func viewDidLoad() {
super.viewDidLoad()
exercise.parseImageData() {
print("Arrived Here?") // I am getting crash on this line moving to debug navigator.
}
}
If the crash says something about force unwrapping nil then it's probably because let task = URLSession.shared.dataTask(with: url!) is unwrapping url which is a nil optional variable here.
But your completion handler is called in the wrong place anyway, try putting your finished() callback into the do statement instead. Because finished was executed the moment you called exercise.parseImageData()
if error != nil {
print("Error while parsing JSON")
}
else {
do {
if let data = data,
let fetchedImageData = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves) as? [String:Any],
let images = fetchedImageData["results"] as? [[String: Any]] {
for eachImage in images {
let imageUrl = eachImage["image"] as! String
self._imageUrl = URL(string: imageUrl)
}
print(self._imageUrl)
finished()
}
}
catch {
print("Error while parsing data.")
}
}

Table only shows when interacting and not by default

I have a table in a view controller that is populated through a dictionary from which information is retrieved via a JSON request. In the viewDidLoad() function, I call the function that retrieves the data which is added to `IncompletedDeadlines dictionary:
override func viewDidLoad() {
super.viewDidLoad()
self.IncompleteDeadlines = [String:AnyObject]()
self.retrieveIncompletedDeadlines()
}
Everything works however the table only shows when interacted with. I thought maybe the best way to show the table the moment the view appears is by adding a tableView.reload to viewDidAppear as so:
override func viewDidAppear(_ animated: Bool) {
self.tableView.reloadData()
}
But this doesn't fix it. I have attached pictures for clarity of the situation. Picture one shows the view the moment the view appears. Picture 2 only happens once the table is interacted with i.e. swiped. So my question is how can I get the table to show immediately? I understand there can be a delay because of the load, but I shouldn't have to interact with it for it to show:
When the view is interacted with i.e. swiped:
The retrieveIncompletedDeadlines() function is as so:
func retrieveIncompletedDeadlines(){
let myUrl = NSURL(string: "https://www.example.com/scripts/retrieveIncompleteDeadlines.php");
let request = NSMutableURLRequest(url:myUrl! as URL)
let user_id = UserDetails[0]
request.httpMethod = "POST";
let postString = "user_id=\(user_id)";
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=\(String(describing: error))")
return
}
var err: NSError?
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary
if let parseJSON = json {
let checker:String = parseJSON["status"] as! String;
if(checker == "Success"){
let resultValue = parseJSON["deadlines"] as! [String:AnyObject]
self.IncompleteDeadlines = resultValue
}
self.tableView.reloadData()
}
} catch let error as NSError {
err = error
print(err!);
}
}
task.resume();
self.tableView.reloadData()
}
JSON will be parsed on the background thread but any update to the UI must be done on the main thread hence you have to do it inside DispatchQueue.main.async {} This article explains well what is the problem.
Furthermore I would write a completions handler which returns the data once the operation has finished. This is another interesting article about.
Completion handlers are super convenient when your app is doing something that might take a little while, like making an API call, and you need to do something when that task is done, like updating the UI to show the data.
var incompleteDeadlines = [String:AnyObject]()
override func viewDidLoad() {
super.viewDidLoad()
//please note your original function has changed
self.retrieveIncompletedDeadlines { (result, success) in
if success {
// once all the data has been parsed you assigned the result to self.incompleteDeadlines
self.incompleteDeadlines = result
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
func retrieveIncompletedDeadlines(_ completion:#escaping ([String:AnyObject] , _ success: Bool)-> Void){
let myUrl = NSURL(string: "https://www.example.com/scripts/retrieveIncompleteDeadlines.php");
let request = NSMutableURLRequest(url:myUrl! as URL)
let user_id = UserDetails[0]
request.httpMethod = "POST";
let postString = "user_id=\(user_id)";
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=\(String(describing: error))")
return
}
var err: NSError?
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary
if let parseJSON = json {
let checker:String = parseJSON["status"] as! String;
var resultValue = [String:AnyObject]()
if(checker == "Success"){
resultValue = parseJSON["deadlines"] as! [String:AnyObject]
}
completion(resultValue, true)
}
} catch let error as NSError {
err = error
print(err!);
}
}
task.resume();
}
}

viewDidLoad not getting called for item on UITabViewController

on a button click ,via segue the flow goes to UITabViewController.
self.performSegueWithIdentifier(self.gotoResult, sender: nil)
let myUrl = NSURL(string: "XXXXXXXX");
let request = NSMutableURLRequest(URL:myUrl!);
request.HTTPMethod = "POST";
// Compose a query string
resultVar.city = cityText.text
resultVar.state = streetText.text
let postString = "streetaddr=\(streetText.text)&city=\(cityText.text)&state=\(stateVal)&degree=\(degreeVal)";
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding);
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if error != nil
{
print("error= \(error)")
return
}
// You can print out response object
print("response = \(response)")
// Print out response body
let responseString = NSString(data: data!, encoding: NSUTF8StringEncoding)
print("responseString = \(responseString)")
do {
resultVar.myJSON = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as! NSDictionary
} catch let error2 as NSError? {
print("error 2 \(error2)")
}
}
task.resume()
There is a UITabViewController with three items. when the initial view gets loaded(item1) the viewDidLoad is not getting called. for now I have added the same code in viewDidAppear and when i click on a different tab and come back to item1 ,the fields are populated. But I want it to work on initial load after the segue only. What am I missing?
ViewdidLoad of Item1
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
print("in here CityViewController")
if let parseJSON = resultVar.myJSON {
// Now we can access value of elements by its key
var weather_condition = parseJSON["weather_condition"] as! String
print("weather_condition: \(weather_condition)")
weatherconditionLbl.text = weather_condition
}
The code which gets called when i switch tabs:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
print("in here CityViewController1")
if let parseJSON = resultVar.myJSON {
// Now we can access value of elements by its key
var weather_condition = parseJSON["weather_condition"] as! String
print("weather_condition: \(weather_condition)")
weatherconditionLbl.text = weather_condition + "in " + resultVar.city+","+resultVar.state
}
}
so the code viewdidload is not getting called and viewDidAppear gets called when i switch tabs.
A network request will be slower than rendering the next scene so "parseJSON" is not there. You need to refresh your graphics using a callback from the network request. In order to do that I suggest that you call the network request from "Item 1" every time you need to refresh its content (that is up to you).

Resources