Swift JSON data on TableView lags - ios

I have an issue with my TableView displaying JSON data. When it is displayed, it currently lags whenever I scroll up and down. I know that I have to use the Grand Central Dispatch methods (GCD) for that, however, I have no clue on how to go about that.
This is my code snippet in my viewDidLoad() method that just grabs the JSON data into a dictionary:
// Convert URL to NSURL
let url = NSURL(string: apiURL)
let jsonData: NSData?
do {
/*
Try getting the JSON data from the URL and map it into virtual memory, if possible and safe.
DataReadingMappedIfSafe indicates that the file should be mapped into virtual memory, if possible and safe.
*/
jsonData = try NSData(contentsOfURL: url!, options: NSDataReadingOptions.DataReadingMappedIfSafe)
} catch let error as NSError
{
showErrorMessage("Error in retrieving JSON data: \(error.localizedDescription)")
return
}
if let jsonDataFromApiURL = jsonData
{
// The JSON data is successfully obtained from the API
/*
NSJSONSerialization class is used to convert JSON and Foundation objects (e.g., NSDictionary) into each other.
NSJSONSerialization class's method JSONObjectWithData returns an NSDictionary object from the given JSON data.
*/
do
{
let jsonDataDictionary = try NSJSONSerialization.JSONObjectWithData(jsonDataFromApiURL, options: NSJSONReadingOptions.MutableContainers) as? NSDictionary
// Typecast the returned NSDictionary as Dictionary<String, AnyObject>
dictionaryOfRecipes = jsonDataDictionary as! Dictionary<String, AnyObject>
// Grabs all of the matched recipes
// This will return an array of all of the matched recipes
matchedRecipes = dictionaryOfRecipes["matches"] as! Array<AnyObject>
// Returns the first 10 recipes shown in the JSON data
recipeCount = matchedRecipes.count
}catch let error as NSError
{
showErrorMessage("Error in retrieving JSON data: \(error.localizedDescription)")
return
}
}
else
{
showErrorMessage("Error in retrieving JSON data!")
}
Thanks!

Give your code inside
dispatch_async(dispatch_get_main_queue(), {
// Your Execution Code
}
This simply works

I've figured it out. It wasn't just the
dispatch_async(dispatch_get_main_queue())
because that is just using the main thread, and you should only use that when you are displaying JSON information onto a view. If I am understanding this correctly, you are supposed to use:
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)){}
Whenever you are trying to download the data, such as an image, before displaying it onto a view. Here is an example code for anyone interested:
//-----------------
// Set Recipe Image
//-----------------
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0))
{
// This grabs the Image URL from JSON
let imageURL = recipeDataDict["imageUrlsBySize"] as! NSDictionary
let imageSize90 = imageURL["90"] as! String
// Create an NSURL from the given URL
let url = NSURL(string: imageSize90)
var imageData: NSData?
do {
/*
Try getting the thumbnail image data from the URL and map it into virtual memory, if possible and safe.
DataReadingMappedIfSafe indicates that the file should be mapped into virtual memory, if possible and safe.
*/
imageData = try NSData(contentsOfURL: url!, options: NSDataReadingOptions.DataReadingMappedIfSafe)
} catch let error as NSError
{
self.showErrorMessage("Error in retrieving thumbnail image data: \(error.localizedDescription)")
}
dispatch_async(dispatch_get_main_queue(),
{
if let image = imageData
{
// Image was successfully gotten
cell.recipeImage!.image = UIImage(data: image)
}
else
{
self.showErrorMessage("Error occurred while retrieving recipe image data!")
}
})
}
Before I had just the dispatch_get_global_queue WITHOUT the main_queue thread, the images would download very slowly (but the tableview did not lag). However, once I added in the main_queue before displaying the JSON data, it was downloaded instantly (or almost instantly) and without any further lags.
More information on: https://tetontech.wordpress.com/2014/06/04/swift-ios-and-grand-central-dispatch/

Related

Property is coming up empty when called from another class when using URLSession

For some reason, the products array is coming back empty when I try and access it from another class. What am I doing wrong, and how can I get the products array to populate? Is it something related to the do/catch?
The print statement shown will give me what I'm looking for, but when I try and use the property in another class after the retrieve method has been called, it comes up empty.
For information, "Product" is a struct that has name, description, etc properties attached.
private let productListUrl = URL(string: "https://api/products.json")
var products = [Product]()
func retrieveProductList() {
if let productListUrl = productListUrl {
URLSession.shared.dataTask(with: productListUrl) { (data, response, error) in
if let data = data {
do {
let jsonData = try JSONSerialization.jsonObject(with: data, options: []) as! [String:Any]
let tempArray: Array = jsonData["products"] as! [Any]
for product in tempArray {
let newProduct = Product(json: product as! [String : Any])
self.products.append(newProduct!)
}
print("In ProductService: \(self.products)")
}
catch {
print("An error occured while attempting to read data")
}
}
}.resume()
}
}
As maddy noted, this is because the URL call is asynchronous.
You basically have 3 options:
Use a semaphore approach and make your retrieveProductList method synchronous.
Change your class to have a delegate property that you can ping when the URL request finishes.
Add a completion handler to your retrieveProductList method that is called when the URL request finishes.
I personally would lean towards option 3:
func retrieveProductList(completion: #escaping ([Product])->())
{
// Right after you print the products...
completion(self.products)
}

Issue while image was converted to NSData

When I pick an image from gallery, I convert it to NSData & assign it a variable like so...
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let image = info[UIImagePickerControllerOriginalImage] as? UIImage {
UIImageWriteToSavedPhotosAlbum(image, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
let data = UIImagePNGRepresentation(image) as NSData?
self.appDelegate.mydata1 = data!
}
Now I'm trying to store self.appDelegate.mydata1 to coredata like so..
guard let appDelegate = ... else {
return
}
let managedContext = ...
let entity = ...
let newProdObj = ...
newProdObj.setValue(self.appDelegate.mydata1, forKey: "imageData") //The attribute imageData in xcdatamodel is of type 'Binary Data'
do {
try managedContext.save()
self.newProductDetails.append(newProdObj as! NewProduct)
} catch let error as NSError {}
Later in another viewcontroller I'm fetching it like so...
guard let appDelegate = ...
let managedContext = ...
let fetchRequest = ...
do {
newProdDetails = try managedContext.fetch(fetchRequest as! NSFetchRequest<NSFetchRequestResult>) as! [NewProduct]
for result in newProdDetails {
print(result)
if let imageData = result.value(forKey: "imageData") as? NSData {
print(imageData)
}}
} catch let error as NSError {}
But when I try to print imageData, the control gets stuck and it goes on continuously printing numbers (which is the data) something like so...<123214 434534 345345 ...etc.
Did go through other posts with the same issue but couldn't get much help from them...
Instead of saving image data in coreData, save your images in document directory and name of those images in UserDefaults/Core Data.
And then you can fetch name from UserDefaults/Core Data and you can get the filPath for those images using this :
// Get path for any filename in document directory
func getFilePathInDocuments(fileName:String, completionHandler:#escaping(String, Bool)->()) {
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let url = NSURL(fileURLWithPath: path)
let fileManager = FileManager.default
let filePath = url.appendingPathComponent(fileName)?.path
if (fileManager.fileExists(atPath: filePath!)) {
completionHandler(filePath!, true)
}else{
completionHandler("File not found!",false)
}
}
If you want to know how to write your images in document directory see this : how to use writeToFile to save image in document directory?
This is not an issue. You are fetching the NSData which is an image, hence its printing the bytes in the image data which are the numbers you are talking about. Also this code is executing in the main thread, hence your app is unresponsive.
As the other answer pointed out save your image in document directory and save the relative path of the imagename in core data. This way you can fetch the name of the imagepath(relative mind you, as full document directory URL can change in app launch), and load the image from the document directory.
Also when saving the image in document directory, please do so in a background queue, using GCD or NSOperation/Operation, as if this image is large it can again block the main thread, as you are operating this in the main thread.
My suggestion is that convert image data to base64 string, and store it to CoreData. Later when you get back image, you can convert it back to NSData. Please refer this for base64 string conversion:
convert base64 decoded NSData to NSString

Images loading in incorrectly even with cache

if let toID = message.chatPartnerId() {
firebaseReference.child(toID).observeSingleEvent(of: .value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
cell.nameLabel.text = dictionary["displayname"] as? String
let pic = dictionary["pictureURL"] as! String
print("THIS IS THE URL FOR EACH DISPLAYNAME")
print(dictionary["displayname"] as? String)
print(pic)
if let imageFromCache = MainPageVC.imageCache.object(forKey: pic as NSString) {
cell.pictureLabel.image = imageFromCache
} else {
let requested = URLRequest(url: URL(string: pic )!)
URLSession.shared.dataTask(with: requested) {data, response, err in
if err != nil {
print(err)
} else {
DispatchQueue.main.async {
let imageToCache = UIImage(data: data!)
MainPageVC.imageCache.setObject(imageToCache!, forKey: pic as NSString)
//cell.pictureLabel.image = nil
cell.pictureLabel.image = imageToCache
}
}
}.resume()
}
}
})
}
return cell
}
I'm running this code in my cellForRowAtIndexPath and I'm getting a ton of really bad behavior. I'm also getting similar behavior on other pages but for some reason this block of code with about a 90% consistency returns incorrect information for cells.
I get a lot of duplicate pictures being used, displaynames in the wrong places, but when I'm actually clicking into a person, my detail page shows the correct information every single time. That code is the typical didSelectRowAtIndexPath and passing the person.
What I don't understand is why on the initial load of this page all of the information is screwed up, but if I click into someone and come back the entire tableview has correct names and pictures. The names/pics also fix if I scroll a cell off the screen then come back to it.
I'm getting this behavior all over my app, meanwhile I see caching/loading done like this everywhere. Is it because I'm running the code in my cellForRowAtIndexPath? The only difference I see is that I'm running it there instead of creating a function inside of my Person class that configures cells and running it like that. What I don't understand is why that would make a difference because as far as I'm aware running a function within cellforRowAtIndexpath would be the same as copy-pasting that same code into there?
Any ideas/suggestions?
Edit: I'm getting a very similar situation when I'm running the following code:
self.PersonalSearchesList = self.PersonalSearchesList.sorted{ $0.users > $1.users }
self.tableView.reloadData()
Where I'm sorting my array before reloading my data. The information sometimes loads in incorrectly at first, but once I scroll the cell off the screen then come back to it it always corrects itself.
if you are using swift 3 here are some handy functions that allow you to save an image to your apps directory from an URL and then access it from anywhere in the app:
func saveCurrentUserImage(toDirectory urlString:String?) {
if urlString != nil {
let imgURL: URL = URL(string: urlString!)!
let request: URLRequest = URLRequest(url: imgURL)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: {
(data, response, error) -> Void in
if (error == nil && data != nil) {
func display_image() {
let userImage = UIImage(data: data!)
if let userImageData = UIImagePNGRepresentation(userImage!) {
let filename = self.getDocumentsDirectory().appendingPathComponent("userImage")
try? userImageData.write(to: URL(fileURLWithPath: filename), options: [.atomic])
}
}
DispatchQueue.main.async(execute: display_image)
}
})
task.resume()
}
}
and then access it with any view controller using this:
extension UIViewController {
func getImage(withName name: String) -> UIImage {
let readPath = getDocumentsDirectory().appendingPathComponent(name)
let image = UIImage(contentsOfFile: readPath)
return image!
}
}
and finally calling it like this:
cell.pictureLabel.image = getImage(withName: "userImage")
If you can run the saveCurrentUserImage function prior to running cellForRowAtIndexPath then you can just check if the photo is nil in the directory before attempting to download it. You might be getting funny behavior when the page initially loads because you have multiple network calls going on at once. I wouldn't recommend making any network calls in cellForRowAtIndexPath because every time the cells are re-initialized it's going to make that network call for each cell.
Hope it helps!
EDIT: This method of image saving and retrieval is for images that you want to persist. If you want to erase them from memory you'll have to delete them from your directory.

Swift: Refresh Button functionality is loading the new data including the existing data

I am loading data on collection view from a set of JSON Data. I have added a refresh button on the navigation bar. I have used the same function as used in Viewdidload to the refresh button. When I tap on the refresh button, The new data is displayed after the existing data in the collection view rather than displaying just the new data. Can someone please help me on how to erase the existing data and display only the new data when refresh button is tapped.
func Dataparsed() {
let quizurl = NSURL(string:"Your URL")
let task = NSURLSession.sharedSession().dataTaskWithURL((quizurl)!, completionHandler: { (data, response, error) -> Void in
do{
let string = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.AllowFragments) as! NSDictionary
let swapLibs = string as? NSDictionary
let QuizData = swapLibs!.valueForKey("Your Key") as! NSArray
self.YourMutableArray.removeAllObjects()
for dict in QuizData {
self.playQuizArray.addObject(playInfo.PlayQuizInfo(dict as? NSDictionary))
}
self.colletionView.reloadData()
print(self.playQuizArray)
}
catch {
print("json error: \(error)")
}
})
task.resume()
}
Hope It Helps.Thanks.
when you recieve the json data on button click try to clear the list first
var someArray = [string]()
someArray.RemoveAll();
then add the new data loaded from json after that reload the collection
collectionview.ReloadData()

How do I get a value from an NSURLSession task into an instance variable?

I have a tableView which I want to fill with a list of items provided by a web service. The service returns a JSON object with status (success or failure) and shows (an array of strings).
In viewDidLoad I call the custom method getShowsFromService()
func getShowsFromService() {
// Send user data to server side
let myURL = NSURL(string: "https://myurl.com/srvc/shows.php")
// Create session instance
let session = NSURLSession.sharedSession()
var json:NSDictionary = [:]
// Create the task
let task = session.dataTaskWithURL(myURL!) { //.dataTaskWithRequest(request) {
(data, response, error) in
guard let data = data else {
print("Error: \(error!.code)")
print("\(error!.localizedDescription)")
return
}
do {
json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as! NSDictionary
} catch {
print (error)
}
let sts = json["status"] as! NSString
print("\(sts)")
}
// Resume the task so it starts
task.resume()
let shows = json["shows"] as! NSArray
for show in shows {
let thisshow = show as! String
showsArray.append(thisshow)
}
// Here I get "fatal error: unexpectedly found nil while unwrapping an Optional value"
}
The method receives the JSON object and puts it into a dictionary. Then I want to use that dictionary to call json['shows'] in order to get to the array of shows which I want to store in an instance variable called showsArray. The idea is to use showsArray in tableView(cellForRowAtIndexPath) in order to fill in the data.
The problem is that I can't get the Dictionary into the variable. If I try to do it inside the task, I get an error that says I need to call self.showsArray and if I do, the data doesn't go inside the array. If I do it outside the task I get an error because it says I'm trying to force unwrap a nil value.
How can I get the Dictionary created within the task out into the showsArray var?
The dataTaskWithURL method makes an async call, so as soon as you do task.resume() it will jump to the next line, and json["shows"] will return nil as the dictionary is empty at this point.
I would recommend moving that logic to a completion handler somewhere in your class. Something along the lines of:
func getShowsFromService() {
let myURL = NSURL(string: "https://myurl.com/srvc/shows.php")
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(myURL!, completionHandler: handleResult)
task.resume()
}
//-handle your result
func handleResult(data: NSData?, response: NSURLResponse?, error: NSError?) {
guard let data = data else {
print("Error: \(error!.code)")
print("\(error!.localizedDescription)")
return
}
do {
if let json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as! NSDictionary {
if let shows = json["shows"] as! NSArray {
//- this is still in a separate thread
//- lets go back to the main thread!
dispatch_async(dispatch_get_main_queue(), {
//- this happens in the main thread
for show in shows {
showsArray.append(show as! String)
}
//- When we've got our data ready, reload the table
self.MyTableView.reloadData()
self.refreshControl?.endRefreshing()
});
}
}
} catch {
print (error)
}
}
The snippet above should serve as a guide (I dont have access to a playground atm).
Note the following:
as soon as the task completes (asynchronously -> different thread) it will call the new function handleResult which will check for errors and if not, it will use the dispatcher to perform your task on the main thread. I'm assuming showsArrays is a class property.
I hope this helps
EDIT:
As soon as you fetch your data you need to reload the table (updated code above). You can use a refresh control (declare it as a class property).
var refreshControl: UIRefreshControl!
Then when you finish getting your data you can refresh:
self.MyTableView.reloadData()
self.refreshControl?.endRefreshing()
This will call your delegate methods to populate the rows and sections.

Resources