iOS - Why reloadData tableView data on first application load? - ios

I am working on a simple Flickr app that gets some data from their API and displays it on a tableview instance. Here's a piece of the code for the TableViewController subclass.
var photos = [FlickrPhotoModel]()
override func viewDidLoad() {
super.viewDidLoad()
getFlickrPhotos()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
private func getFlickrPhotos() {
DataProvider.fetchFlickrPhotos { (error: NSError?, data: [FlickrPhotoModel]?) in
//data is received
dispatch_async(dispatch_get_main_queue(), {
if error == nil {
self.photos = data!
self.tableView.reloadData()
}
})
}
}
The application does not seem to load the data if the { tableView.reloadData() } line is removed. Does anyone know why this would happen since I call getFlickrPhotos() within viewDidLoad(). I believe I am also dispatching from the background thread in the appropriate place. Please let me know what I am doing incorrectly.
EDIT -- Data Provider code
class func fetchFlickrPhotos(onCompletion: FlickrResponse) {
let url: NSURL = NSURL(string: "https://api.flickr.com/services/rest/?method=flickr.photos.getRecent&api_key=\(Keys.apikey)&per_page=25&format=json&nojsoncallback=1")!
let task = NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) in
if error != nil {
print("Error occured trying to fetch photos")
onCompletion(error, nil)
return
}
do {
let jsonResults = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as? NSDictionary
let photosContainer = jsonResults!["photos"] as? NSDictionary
let photoArray = photosContainer!["photo"] as? [NSDictionary]
let flickrPhoto: [FlickrPhotoModel] = photoArray!.map{
photo in
let id = photo["id"] as? String ?? ""
let farm = photo["farm"] as? Int ?? 0
let secret = photo["secret"] as? String ?? ""
let server = photo["server"] as? String ?? ""
var title = photo["title"] as? String ?? "No title available"
if title == "" {
title = "No title available"
}
let model = FlickrPhotoModel(id: id, farm: farm, server: server, secret: secret, title: title)
return model
}
//the request was successful and flickrPhoto contains the data
onCompletion(nil, flickrPhoto)
} catch let conversionError as NSError {
print("Error parsing json results")
onCompletion(conversionError, nil)
}
}
task.resume()
}

I'm not familiar with that API, but it looks like the fetchFlickrPhotos method is called asynchronously on a background thread. That means that the rest of the application will not wait for it to finish before moving on. viewDidLoad will call the method, but then move on without waiting for it to finish.
The completion handler that you provide is called after the photos are done downloading which, depending on the number and size of the photos, could be seconds later. So reloadData is necessary to refresh the table view after the photos are actually done downloading.

Related

How to show all data in table view during pagination in swift 3?

Here i had implemented pagination for the table view and items are loaded by using model class but here the loaded items are replacing with the new items and whenever it calls api it returns the new data and old data is overriding on it and displaying only 10 items at a time i am implementing it for first time can anyone help me how to resolve the issue ?
func listCategoryDownloadJsonWithURL(listUrl: String) {
let url = URL(string: listUrl)!
print(listUrl)
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil { print(error!); return }
do {
if let jsonObj = try JSONSerialization.jsonObject(with: data!) as? [String:Any] {
self.listClassModel = ModelClass(dict: jsonObj as [String : AnyObject])
DispatchQueue.main.async {
guard let obj = self.listClassModel else { return }
let itemsCount = obj.items.count
print(itemsCount)
for i in 0..<itemsCount {
let customAttribute = obj.items[i].customAttribute
for j in 0..<customAttribute.count {
if customAttribute[j].attributeCode == "image" {
let baseUrl = "http://192.168.1.11/magento2/pub/media/catalog/product"
self.listCategoryImageArray.append(baseUrl + customAttribute[j].value)
print(self.listCategoryImageArray)
}
}
}
self.activityIndicator.stopAnimating()
self.activityIndicator.hidesWhenStopped = true
self.collectionView.delegate = self
self.collectionView.dataSource = self
self.collectionView.reloadData()
self.collectionView.isHidden = false
self.tableView.reloadData()
}
}
} catch {
print(error)
}
}
task.resume()
}
You are assigning your result data to model array, each time you call your API. This is the reason that your old data is getting replaced with new one. Rather than assigning, you should append the new data to your datasource array.
if let jsonObj = try JSONSerialization.jsonObject(with: data!) as? [String:Any] {
self.listClassModel.append(contentsOf: ModelClass(dict: jsonObj as [String : AnyObject]))
Also make sure you initialize your array as an empty array first. (maybe in declaration or viewDidLoad) before calling API.

Swift: How to refresh tableview without lag (UIRefreshControl)

Filling my table view with objects from a MYSQL database using PHP and JSON to Swift 3. I have a pull down to refresh function but when I'm pulling down to refresh it lags mid way for a second and then continues (like the wheel won't spin for a second).
How can I update my tableview smoother because I'm guessing as I add more content to the database in the future the bigger the lag. I currently have 12 objects in my database so imagine with 100+ objects.
In viewDidLoad
// Pull to Refresh
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
if #available(iOS 10.0, *) {
myTableView.refreshControl = refreshControl
print("iOS 10")
} else {
myTableView.addSubview(refreshControl)
print("iOS 9 or iOS 8")
}
Then pull to refresh function
// Pull to Refresh
func handleRefresh(refreshControl: UIRefreshControl) {
// Fetching Data for TableView
retrieveDataFromServer()
// Stop Refreshing
refreshControl.endRefreshing()
}
// Retrieving Data from Server
func retrieveDataFromServer() {
// Loading Data from File Manager
loadData()
let getDataURL = "http://example.com/receiving.php"
let url: NSURL = NSURL(string: getDataURL)!
do {
let data: Data = try Data(contentsOf: url as URL)
let jsonArray = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as! NSMutableArray
// Clear the arrays
self.followedArray = [Blog]()
self.mainArray = [Blog]()
// Looping through jsonArray
for jsonObject in jsonArray {
if let blog = Blog(jsonObject:jsonObject as! [String : Any]) {
// Check if Identifiers Match
if followedIdentifiers.contains(blog.blogID) {
self.followedArray.append(blog)
} else {
self.mainArray.append(blog)
}
}
}
} catch {
print("Error: (Retrieving Data)")
}
myTableView.reloadData()
}
Refer to the apple sample code at the following location:
http://developer.apple.com/library/ios/#samplecode/LazyTableImages/Introduction/Intro.html
Couple of suggestion :
Don’t show the data at cellForRowAtIndexPath: method ‘cause at this time cell is not displayed yet. Try to use tableView:willDisplayCell:forRowAtIndexPath: method in the delegate of UITableView.
Re-Use single instance of cell/header/footer even if you need to show more.
Let me know if anything specific is needed.
Spinner.isHidden = false
Spinner.startAnimating()
DispatchQueue.global(qos: .background).async {
loadData()
let getDataURL = "http://example.com/receiving.php"
let url: NSURL = NSURL(string: getDataURL)!
do {
let data: Data = try Data(contentsOf: url as URL)
let jsonArray = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as! NSMutableArray
// Clear the arrays
self.followedArray = [Blog]()
self.mainArray = [Blog]()
// Looping through jsonArray
for jsonObject in jsonArray {
if let blog = Blog(jsonObject:jsonObject as! [String : Any]) {
// Check if Identifiers Match
if followedIdentifiers.contains(blog.blogID) {
self.followedArray.append(blog)
} else {
self.mainArray.append(blog)
}
}
}
} catch {
print("Error: (Retrieving Data)")
}
DispatchQueue.main.async
{
myTableView.reloadData()
self.Spinner.startAnimating()
self.Spinner.isHidden = true
}
}
My guess is that your retrieveDataFromServer() is blocking the main thread, and therefore causing the lag. Try wrapping it in an async block
// Pull to Refresh
func handleRefresh(refreshControl: UIRefreshControl) {
// Fetching Data for TableView
retrieveDataFromServer { [weak refreshControl] in
// This block will run once retrieveDataFromServer() is completed
// Reload data
myTableView.reloadData()
// Stop Refreshing
refreshControl?.endRefreshing()
}
}
// Retrieving Data from Server
func retrieveDataFromServer(completion: (() -> Void)?) {
// Loading Data from File Manager
loadData()
let getDataURL = "http://example.com/receiving.php"
let url: NSURL = NSURL(string: getDataURL)!
do {
let data: Data = try Data(contentsOf: url as URL)
let jsonArray = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as! NSMutableArray
// Clear the arrays
self.followedArray = [Blog]()
self.mainArray = [Blog]()
// Looping through jsonArray
for jsonObject in jsonArray {
if let blog = Blog(jsonObject:jsonObject as! [String : Any]) {
// Check if Identifiers Match
if followedIdentifiers.contains(blog.blogID) {
self.followedArray.append(blog)
} else {
self.mainArray.append(blog)
}
}
}
} catch {
print("Error: (Retrieving Data)")
}
// Calls completion block when finished
completion?()
}
I imagine the lag you're experiencing is due to the network request being executed synchronously on the main thread:
let data: Data = try Data(contentsOf: url as URL)
Network requests are slow and should almost certainly be done off the main thread. The solution here is to move the networking call to a background thread so the main (UI) thread doesn't get blocked (lag).
So how do you do that? Well that is a large question with many different answers.
I highly recommend you spend some time learning about multi-threaded programming (also known as concurrency) in Swift. Going through this Ray Wenderlich tutorial should give you a good foundation.
Then it's probably a good idea to learn about URLSession which is used for performing asynchronous network requests in iOS apps. Again Ray Wenderlich has a great starter tutorial.
Finally... here is a quick and dirty solution for you. It's "hacky" and you probably shouldn't use it, but it will probably fix your lag issue:
func retrieveDataFromServer() {
// Loading Data from File Manager
loadData()
let getDataURL = "http://example.com/receiving.php"
let url: NSURL = NSURL(string: getDataURL)!
// Move to a background queue to fetch and process data from network.
DispatchQueue.global().async {
// Don't touch anything related to the UI here.
do {
let data: Data = try Data(contentsOf: url as URL)
let jsonArray = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as! NSMutableArray
// Create new temp arrays to process json
var tempFollowedArray = [Blog]()
var tempMainArray = [Blog]()
// Looping through jsonArray
for jsonObject in jsonArray {
if let blog = Blog(jsonObject:jsonObject as! [String : Any]) {
// Check if Identifiers Match
if self.followedIdentifiers.contains(blog.blogID) {
tempFollowedArray.append(blog)
} else {
tempMainArray.append(blog)
}
}
}
// Jump back to main (UI) thread to update results
DispatchQueue.main.async {
print("success")
self.followedArray = tempFollowedArray
self.mainArray = tempMainArray
self.myTableView.reloadData()
}
} catch {
DispatchQueue.main.async {
print("Error: (Retrieving Data)")
// This reload is probably not necessary, but it was
// in your original code so I included it.
self.myTableView.reloadData()
}
}
}
}

Performing Segue after Login

So I'm designing an application where, like most apps, takes users to the "home page" after a successful login. However, I can't quite figure out how to get it to work. The code for my Login page is as follows:
import UIKit
class LoginVC: UIViewController {
#IBOutlet weak var usernameTxt: UITextField!
#IBOutlet weak var passwordTxt: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
//#IBAction func userLogin(_ sender: AnyObject) {
#IBAction func userLogin(_ sender: AnyObject) {
// if textboxes are empty
if usernameTxt.text!.isEmpty || passwordTxt.text!.isEmpty {
// red placeholders
usernameTxt.attributedPlaceholder = NSAttributedString(string: "Username", attributes: [NSForegroundColorAttributeName: UIColor.red])
passwordTxt.attributedPlaceholder = NSAttributedString(string: "Password", attributes: [NSForegroundColorAttributeName: UIColor.red])
} else {
// shortcuts
let username = usernameTxt.text!.lowercased()
let password = passwordTxt.text!
// send request to mysql db
// Create a user in the mySQL db
// the exclamation at the end means we insist to launch it
// url to php file
let url = NSURL(string: "https://cgi.soic.indiana.edu/~team7/login.php")!
// request to the file
let request = NSMutableURLRequest(url: url as URL)
// method to pass data to this file via the POST method
request.httpMethod = "POST"
// what occurs after the question mark in the url
// body to be appended to url from values in textboxes
let body = "username=\(username)&password=\(password)"
// appends body to request that will be sent
request.httpBody = body.data(using: String.Encoding.utf8)
// launching
URLSession.shared.dataTask(with: request as URLRequest, completionHandler: { (data:Data?, response:URLResponse?, error:Error?) -> Void in
if error == nil {
// get main queue in code process to communicate back
DispatchQueue.main.async(execute: {
// do this unless some error which is caught by catch
do {
// get json result
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary
// guard let is the same thing as if let
// asign json to new variable in secure way
// original guard let used
guard let parseJSON = json else {
print("Error while parsing")
return
}
// get id from parseJSON dictionary
let id = parseJSON["id"] as? String
// if there is some id value
if id != nil && response != nil {
print(parseJSON)
// successfully logged in
//let userID = parseJSON["id"] as! String
//let userN = parseJSON["username"] as! String
//let eMail = parseJSON["email"] as! String
//print(parseJSON["username"] ?? String.self)
//let myVC = self.storyboard?.instantiateViewController(withIdentifier: "RetrievalVC") as! RetrievalVC
//myVC.id_Outlet.text = userID
//myVC.full_Outlet.text = userN
//myVC.email_Outlet.text = eMail
//
//self.navigationController?.pushViewController(myVC, animated: true)
}
} catch {
print("Caught an error \(error)")
}
})
// if unable to process request
} else {
print("error: \(error)")
}
}).resume()
//performSegue(withIdentifier: "loginSuccess", sender: LoginVC.self)
}
}
}
I am trying to use
performSegue(withIdentifier: "loginSuccess", sender: LoginVC.self)
In order to perform the segue but I'm not sure where in the code it should go.
Any suggestions or changes I need to make to the code?
It depends on back end logic.I assume that parseJSON["id"] is returned only if user is verified. So you can use this
let id = parseJSON["id"] as? String
// if there is some id value
if id != nil {
// perform segue here
}
You can perform a segue when error is nil and you are response contains data...
if id != nil && response != nil {
performSegue(withIdentifier: "loginSuccess", sender: LoginVC.self)
}

Alamofire request gives memory warning

I am using a Master Detail Application. Master Screen is a Dashboard and on selecting an item, moves to the detailed screen where I trigger an Alamofire request in the backend
Below is the snippet
class APIManager: NSObject {
class var sharedManager: APIManager {
return _sharedManager
}
private var requests = [Request]()
// Cancel any ongoing download
func cancelRequests() {
if requests.count > 0 {
for request in requests {
request.cancel()
}
}
}
func getData(completion: (dataSet: [Data]?, error: NSError?) -> Void) {
let request = Alamofire.request(.GET, "http://request")
.response { (request, response, data, error) in
dispatch_async(dispatch_get_main_queue(), {
if(error == nil) {
if let response = data, data = (try? NSJSONSerialization.JSONObjectWithData(response, options: [])) as? [NSDictionary] {
var dataSet = [Data]()
for (_, dictionary) in data.enumerate() {
let lat = dictionary["Latitude"]
let lng = dictionary["Longitude"]
let id = dictionary["ID"] as! Int
let data = Data(lat: lat!, long: lng!, id: shuttleID)
dataSet.append(data)
}
completion(dataSet: dataSet, error: nil)
}
} else { completion(dataSet: nil, error: error) }
})
}
requests.append(request)
}
}
I have a singleton API manager class and from the detail view controller I call getData() function. Everything works fine.
But, when I push and pop repeatedly, I see rapid increase in the memory and after 10-15 attempts, I get memory warning. However in the AppDelegate I am managing it to show an Alert message and adding a delay timer for 8 seconds. But however after 20-25 attempts app crashes due to memory warning.
In viewWillDisappear(), I cancel any ongoing requests also. But I couldn't able to stop memory warning issue. I commented the part where I call the request, I see no issues, even memory consumption is less.
I welcome ideas.
The problem is you are never removing the requests that you append to the member variable 'requests'.
You will need to ensure to remove the request when you either cancel it or when the request completes successfully.
Do the following modifications-
func cancelRequests() {
if requests.count > 0 {
for request in requests {
request.cancel()
}
}
requests.removeAll() //Delete all canseled requests
}
also
func getData(completion: (dataSet: [Data]?, error: NSError?) -> Void) {
let request = Alamofire.request(.GET, "http://request")
.response { (request, response, data, error) in
dispatch_async(dispatch_get_main_queue(), {
if(error == nil) {
if let response = data, data = (try? NSJSONSerialization.JSONObjectWithData(response, options: [])) as? [NSDictionary] {
var dataSet = [Data]()
for (_, dictionary) in data.enumerate() {
let lat = dictionary["Latitude"]
let lng = dictionary["Longitude"]
let id = dictionary["ID"] as! Int
let data = Data(lat: lat!, long: lng!, id: shuttleID)
dataSet.append(data)
}
requests.removeObject(request)
completion(dataSet: dataSet, error: nil)
}
} else {
requests.removeObject(request)
completion(dataSet: nil, error: error) }
})
}
requests.append(request)
}
Add this Handy extension on Array to remove item to your code:
// Swift 2 Array Extension
extension Array where Element: Equatable {
mutating func removeObject(object: Element) {
if let index = self.indexOf(object) {
self.removeAtIndex(index)
}
}
mutating func removeObjectsInArray(array: [Element]) {
for object in array {
self.removeObject(object)
}
}
}
On analysis, I found that the memory warning was not due to the Alamofire request. It was due to MKMapView. Loading a MKMapView, zooming in and zooming out consumes more memory. So, in viewWillDisappear I did the fix.
override func viewWillDisappear(animated:Bool){
super.viewWillDisappear(animated)
self.applyMapViewMemoryFix()
}
func applyMapViewMemoryFix(){
switch (self.mapView.mapType) {
case MKMapType.Hybrid:
self.mapView.mapType = MKMapType.Standard
break;
case MKMapType.Standard:
self.mapView.mapType = MKMapType.Hybrid
break;
default:
break;
}
self.mapView.showsUserLocation = false
self.mapView.delegate = nil
self.mapView.removeFromSuperview()
self.mapView = nil
}
Courtesy - Stop iOS 7 MKMapView from leaking memory

Swift 2.0 iOS9 UITableView update does not render

When i load a JSON file inside my UITableViewController it loads and updates my datasource and view, but only renders the update when i touch my screen.
The loading and parsing code i'm using looks like this:
func fetchData() {
let jsonUrl = 'http://myrestserver.com/apicall'
let session = NSURLSession.sharedSession()
let urlObject = NSURL(string: jsonUrl)
let task = session.dataTaskWithURL(urlObject!) {
(data, response, error) -> Void in
do {
let jsonData = try NSJSONSerialization.JSONObjectWithData(data!, options:NSJSONReadingOptions.MutableContainers ) as! NSDictionary
var items :[Article] = []
let jsonItems = jsonData["channel"] as! NSArray
for (_, item) in jsonItems.enumerate() {
let article = Article()
article.id = item["id"] as? String
article.title = item["title"] as? String
article.guid = item["guid"] as? String
items.append(article)
}
self.articles.insertContentsOf(items, at: 0)
} catch {
print("error fetchData")
}
}
task.resume()
self.tableView.reloadData()
}
Is there a method i'm not aware of to handle this re-rendering?
I've tried render methods for UITableViewCell like described here:
setNeedsLayout and setNeedsDisplay
But there is no luck, can someone explain what is the best practice for rendering new records?
Best regards,
Jos
#nwales is correct, though I would recommend getting familiar with property observers for reloading your data. Once your data is reloaded simply update your property and it will automatically fire your update.
var data: [String] = [""] {
didSet {
// you could call a function or just reload right here
self.tableView.reloadData()
}
}
using #nwales method:
var data: [String] = [""] {
didSet {
dispatch_async(dispatch_get_main_queue(),{
myTableView.reloadData()
})
}
}
After you've parsed the JSON try adding the following
dispatch_async(dispatch_get_main_queue(),{
myTableView.reloadData() //myTableView = your table view instance
})

Resources