Swift - Slow UITableView scroll (loading image from device storage) - ios

I have a UITableView populated with cells either containing just a text label or a text label and a UIImage. When the app is run, images are downloaded from my server and stored on the device locally. Every time the app is run, the app checks for new images and downloads them. In my cellForRowAtIndexPath function, I load the image from the device storage on a background thread then update the UI on the main thread.
I don't know if I am doing this right but here is my code for displaying images inside my cellForRowAtIndexPath function:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
for image in self.postsImages {
if ((image as! NSArray)[0] as! Int == postIDCurrent) {
// there is an image associated with this post
let postImage = UIImage(contentsOfFile: (currentArray.lastObject as? String)!)
dispatch_async(dispatch_get_main_queue(), {
cell.postImageView.image = postImage
cell.postImageView.clipsToBounds = true
cell.postImageView.userInteractionEnabled = true
cell.postImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(PostsTableViewController.imageTapped(_:))))
})
}
}
})
The for and the if statements both just really check if there is an image associated with the current cell we are going to display.
I'm sorry if I'm not providing sufficient information. If you need anything more, please comment.
Anyways, my question is, what am I doing wrong with this code which causes my UITableView to scroll very slow?

Chances are that postImage is much larger than than the bounds of cell.postImageView, so the assignment on the main thread takes a noticeable amount of time to scale it down. Assuming that postImageView's bounds are foreseeable, scaling the image to the correct size in the background thread will be quite likely to sort this out. If you don't have a resizing function handy, this gist looks reasonable.

Try breaking your for loop:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
for image in self.postsImages {
if ((image as! NSArray)[0] as! Int == postIDCurrent) {
// there is an image associated with this post
let postImage = UIImage(contentsOfFile: (currentArray.lastObject as? String)!)
dispatch_async(dispatch_get_main_queue(), {
cell.postImageView.image = postImage
cell.postImageView.clipsToBounds = true
cell.postImageView.userInteractionEnabled = true
cell.postImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(PostsTableViewController.imageTapped(_:))))
})
break
}
}
})

Related

How to know when all tasks are completed after HTTPRequest (Swift)

I am currently facing an issue that has been bothering me for days. I have tried several solutions but don't seem to be able to fix the issue. Let me illustrate what is going on.
I have a feed with posts in a UITableView. As soon as the user selects one of the cells, he is shown a detailed view of this post in a new UIViewController. In there, I set up all views and call a function loadPostDetails() in the viewDidLoad() method. As soon as the view shows up, a custom loading indicator is presented which is set to disappear (hide) when the details are loaded and then the UITableView of this UIViewController (let's call it PostController) is shown.
In my loadPostDetails()function, I make a HTTPRequest which acquires the JSON data I need. This returns three kinds of info: the post details themselves, the likes and the comments. I need to handle each of these three elements before I reload the UITableView and show it. Currently, I do it like this:
HTTP.retrievePostDetails(postID: postID, authRequired: true) { (error, postDetails) in
if(error != nil) {
self.navigationController?.popViewController(animated: true)
} else {
if let postDetails = postDetails, let postInfo = postDetails["postInfoRows"] as? [[String: Any]], let postLikesCount = postDetails["postLikesCount"] as? Int, let postLikesRows = postDetails["postLikesRows"] as? [[String: Any]], let postCommentsRows = postDetails["postCommentsRows"] as? [[String: Any]] {
DispatchQueue.main.async {
let tableFooterView = self.postTableView.tableFooterView as! tableFooterView
if let postTitle = postInfo[0]["postTitle"] as? String, let postText = postInfo[0]["postText"] as? String {
self.postTitleLabel.text = postTitle
self.postTextLabel.text = postText
}
for (index, postCommentRow) in postCommentsRows.enumerated() {
tableFooterView.postComments.append(Comment(userID: postCommentRow["userID"] as! Int, userProfilePicURL: postCommentRow["userProfilePicURL"] as! String, userDisplayName: postCommentRow["userDisplayName"] as! String, commentTimeStamp: postCommentRow["commentTimeStamp"] as! TimeInterval, commentText: postCommentRow["commentText"] as! String))
}
var likeDisplayNames = [String]()
for postLikeRow in postLikesRows {
likeDisplayNames.insert(postLikeRow["userDisplayName"] as! String, at: 0)
}
if(postLikesCount > 2) {
tableFooterView.likesLabel.text = "\(likeDisplayNames[0]), \(likeDisplayNames[1]) and \(postLikesCount - 2) others"
} else {
tableFooterView.likesLabel.text = "\(postLikesCount) likes"
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
self.screenDotsLoader.isHidden = true
self.screenDotsLoader.stopAnimating()
self.postTableView.isHidden = false
self.postTableView.reloadData()
})
}
}
}
Note: I add more text to UILabels, like the date and the profile picture of the user, but I have removed a couple of lines to make it more readible and because the extra code is irrelevant for this problem.
Now, as you might already see, I call the reload stuff 1 second later, so in 95% of the cases it works just fine (but still, it is not perfect, as it is a "hack"). In the other 5%, the layout can't figure out the right constraints, resulting in a very bad layout.
I have in the last days tried to play with DispatchGroups(), but I couldn't figure out how to do it. I am in fact trying to know when all tasks have been performed, so when all UILabels have been updated, all UIImageViews have been updated etc. Only then, I want to reload the UITableView.
I was hoping someone could point me in the right direction so I can enhance my user experience a bit more. Thank you!
DispatchGroups is used when you do a bunch of asynchronous tasks together , and need to be notified upon finish of all , but currently you don't do this as when you receive the response , all are inside the main thread which is synchronous ( serial ) which means all the stuff before reloading the table will happen before it's reload , so you're free to use dispatch after if this will make sense to your UX

Collection view not showing images properly

In view B I have an imageview which can have an image from gallery. The image is stored in coredata as NSData
Now if I click on a submit button & return back to a view A, I have a collectionview where I want to display the image. If I again go to view B and add another image, click on submit & return to view A I should see 2 different images.
But when I return, what I’m seeing is the first image twice instead of 2 different images. But when I run the app again I see all different images
as required...
In viewWillAppear of view A this is what I’ve done…
//Fetching from Database
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Customers2")
do {
customerDetails2 = try managedContext.fetch(fetchRequest as! NSFetchRequest<NSFetchRequestResult>) as! [Customers2]
for result in customerDetails2 {
if let imageData = result.value(forKey: "image") as? NSData {
}
}
collectionview2.reloadData()
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
In numberOfItemsInSection of view A….
count1 = customerDetails2.count
In cellForRowAtIndexPath of view A…
let dfg = customerDetails2[indexPath.row]
let asd = dfg.image
if let image = UIImage(data: asd! as Data) {
imageArray.append(image)
cell1.customerImageView.image = imageArray[indexPath.row]
}
Issue is With the below line
imageArray.append(image)
As you mentioned the issue occurs after adding the second image
So you might be reloading the collection view again to show the images.
The above code will be executed again and it will append the image for the row zero at the index 1 and row 1 at index 2 so when you try to show the image for row1, it will be same as row 0 ..
Use a dictionary if you want to cache the images
like below
//Declare a poperty
var imageDict = Dictionary<String,UIImage>()
//At cellForRow
imageDict["\(indexpath.row)"] = image
also check if the image exists in cache
if let image = imageDict["\(indexpath.row)"] as? UIImage {
//Load Image
} else {
//Initialise from data
}

Why does data returned from coredata become nil after tableview scroll in swift?

Update Note: This question does not seem to be a duplicate of How can I fix crash when tap to select row after scrolling the tableview? because my issue is specifically about one of my CoreData objects going nil after the table scrolls. But the cell returns data as long as the app doesnt crash due to the nil from the topic object.
Basically this part fails: let estimatedSkills = topic.topicEstimatedSkill and let topic = self.topics!.object(at: indexPath.row) as! Topic even though at first, a debugprint shows that the object is not nil, after scroll, self.topic becomes nil.
Original:
I hope you can help me because I am a beginner in Swift and apple development in general.
I inherited an ios app that didnt work because the structure of data returned from our server changed. The previous developers cant be reached, so I am trying to make out what to do here. On top of that, in the middle of development, apple forced me to update to swift3 and xcode8 changing the gamerules making everything even more confusing.
So basically I have a tableview that gets data from an object fetched from CoreData. It fills out the cells when initiated, but scrolling makes the data from the object return nil.
The object in question is named Topic
and the code fails at:
//TODO - this sometimes gives nil but shouldnt
if let estimatedSkills = topic.topicEstimatedSkill {
value = estimatedSkills.doubleValue * Double(cell.starViewContainer.subviews.count);
debugPrint("success getting estimated skills!" , indexPath, estimatedSkills)
} else {
debugPrint("Didnt get estimated skills :(")
}
Then when I return to the original cells, the info in those are gone too.
Here's the tableview function:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Identifiers.cells.superTrainer.rawValue, for: indexPath) as! SuperTrainerOverViewCell;
cell.backgroundColor = UIColor.clear;
let topic = self.topics!.object(at: indexPath.row) as! Topic;
cell.titleLabel.text = topic.topicName;
var value = 0.0
//TODO - this sometimes gives nil but shouldnt
if let estimatedSkills = topic.topicEstimatedSkill {
value = estimatedSkills.doubleValue * Double(cell.starViewContainer.subviews.count);
debugPrint("success getting estimated skills!" , indexPath, estimatedSkills)
} else {
debugPrint("Didnt get estimated skills :(")
}
let wholeStar = Int(floor(value));
for index in (0..<wholeStar) {
let imageView = cell.starViewContainer.subviews[index] as! UIImageView;
imageView.image = UIImage(named: "icon-star-100");
}
if(wholeStar < cell.starViewContainer.subviews.count) {
let decimal = fmod(value, 1);
var image = UIImage(named: "icon-star-100");
if(decimal < 0.15) {
image = UIImage(named: "icon-star-0");
}else if(decimal >= 0.15 && decimal < 0.4) {
image = UIImage(named: "icon-star-25");
}else if(decimal >= 0.4 && decimal < 0.65) {
image = UIImage(named: "icon-star-50");
}else if(decimal >= 0.65 && decimal < 0.80) {
image = UIImage(named: "icon-star-75");
}
let imageView = cell.starViewContainer.subviews[wholeStar] as! UIImageView;
imageView.image = image;
}
return cell;
}
The class has a topics property which gets set at:
override func viewDidLoad() {
self.navigationItem.title = "SUPERTRAINER".localized;
self.tableView.backgroundColor = UIColor.init(rgba: hexColors.gray.rawValue);
if(Utility.isConnectedToNetwork()) {
self.getSuperTrainerData(student: self.student!);
} else {
self.topics = Topic.getTopics(student: self.student!, context: NetworkService.sharedInstance.coreDataHandler.context!);
Utility.showNoConnectionAlertView();
}
self.tableView.tableFooterView = UIView(frame: CGRect.zero);
}
// MARK: Custom Methods
func getSuperTrainerData(student: Student) {
_ = SwiftSpinner.show("FETCHING_DATA".localized);
Utility.backgroundThread( background: { () -> Void in
self.networkService.getSuperTrainerData(student: student) { (complet, returnDics, errorMessage) -> Void in
if(complet) {
self.removeSuperTrainerData();
self.createSuperTrainerData(dics: returnDics!);
} else {
Utility.showAlertView(title: "LOGIN_FAILED_TITLE".localized, message: errorMessage);
}
}
}) { () -> Void in
self.networkService.coreDataHandler.saveContext();
self.topics = Topic.getTopics(student: self.student!, context:self.networkService.coreDataHandler.context!)!;
SwiftSpinner.hide();
self.tableView.reloadData();
}
}
Anyone have any ideas? :) Im too new at swift to figure out all of this quickly right now, so any help is super appreciated!
I found out eventually that it was actually a threading(concurrence) problem.
Basically, in the getSuperTrainerData function, a bunch of objects were supposed to be created. Given that they returned nill all the time, the ViewController would of course refuse to create the rows.. however, entering the same view twice would have given the app time to store and cache the objects returned from a network call.
I make a call to Utility.backgroundThread which is just a wrapper for dispatchQueue. This means that networkService was placed inside a background thread. But inside networkService there is a call to urlSession. Call to servers create their own background threads, so even though I called my server call throuhg a background thread, it created its own background thread and never returned to the main call.
The solution that I used was to just make a server call, and place the background-object-creation call in the completion handler like so:
self.networkService.getSuperTrainerData(student: student) { (complet, returnDics, errorMessage) -> Void in
if(complet) {
DispatchQueue.global(qos: DispatchQoS.userInitiated.qosClass ).async {
self.removeSuperTrainerData();
self.createSuperTrainerData(dics: returnDics!);
DispatchQueue.main.async( execute: {
print("main queue without completion")
self.networkService.coreDataHandler.saveContext();
self.topics = Topic.getTopics(student: self.student!, context:self.networkService.coreDataHandler.context!)!;
SwiftSpinner.hide();
self.tableView.reloadData();
})
}
} else {
Utility.showAlertView(title: "LOGIN_FAILED_TITLE".localized, message: errorMessage);
}
}
Hope this helps someone :)

UITableView is Lagging when Displaying Images Swift

I have a tableView that shows images in the cells, and I'm fetching the data in viewDidLoad function
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableview.dequeueReusableCellWithIdentifier("cardSelectionCell", forIndexPath: indexPath) as! MyCell
let card = fetchedResultsController.objectAtIndexPath(indexPath) as! Card
cell.Name?.text = card.name
var image: NSData = card.photo as NSData
cell.logo.image = UIImage(data: image)
let date = NSDate()
let formatter = NSDateFormatter()
formatter.timeStyle = .MediumStyle
cell.policyExpiryDate.text = formatter.stringFromDate(date)
return cell
}
But the problem is that when I start scrolling, the tableView is very laggy
so i tried to create a dictionary to convert the images in viewDidLoad()
var imageDictionary = [Card:UIImage]()
AllCards = context.executeFetchRequest(request, error: nil) as! [Card]
for card in AllCards {
imageDictionary[card] = UIImage(data: card.photo as NSData)
}
and in the tableView function:
cell.logo.image = imageDictionary[card]
but it still lagging
one more question: if one card is added to the core data, i have to fetch the Array of images again, so i tried to create the array in viewDidAppear() but the images does not appear in the first load and the tableView is still lagging.
First of all it depends really on the size of your image:
var image: NSData = card.photo as NSData
cell.logo.image = UIImage(data: image)
This lines can be very heavy. You can easily measure it like this for example:
let time1 = CACurrentMediaTime()
var image: NSData = card.photo as NSData
cell.logo.image = UIImage(data: image)
print(CACurrentMediaTime() - time1)
If it takes much time (for example more than 8 milliseconds) than you should optimize it. You can do it it many ways, for example, you can store UIImage references in some dictionary or array and then pass on displaying in cellForIndexPath:.
The other way is going to AsyncKit way - making your UI assync, load data in one thread and pass it to draw on main thread only. Now all you process are in main thread.
UPD: I think that the best approach in this case is to store pathes to images, then run in background process for UIImage resize and return preview. Also you can make cache for resized and not resized UIImage, the path or URL can be a key - so next time it will cost only to take UIImage from cache not to launch all the resize process again.
Also shoul notice that this operation:
cell.logo.image = UIImage(data: image)
create a system cache and can make big allocations in memory, because garbage collector won't kill it immediately, like in the case with UIWebView. So you can add your own autorelease pool like this around your function:
autoreleasepool { () -> () in
cell.logo.image = UIImage(data: image)
}
I was having this issue too, I solved it by using dispatch_async. This way the table cell will load before the image finishes loading.
let priority = DISPATCH_QUEUE_PRIORITY_HIGH
dispatch_async(dispatch_get_global_queue(priority, 0)) {
if let imgData = card.photo as? NSData {
let image = UIImage(data: imageData)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
cell.logo.image = image
})
}
}

SDWebImage implementation in parse using Swift

I am using parse to retrieve my images and labels and display it on a collection view. The problem was that the collection view loads all the images and labels at once making the load time long and memory usage was high. I was thinking that I would load 10 cells each time however I was recommended to use SDWebImage to make the app lighter. However I don't know how to implement it with parse using swift. I am suspecting that I would put some code in this piece of code below
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("newview", forIndexPath: indexPath) as! NewCollectionViewCell
let item = self.votes[indexPath.row]
let gesture = UITapGestureRecognizer(target: self, action: Selector("onDoubleTap:"))
gesture.numberOfTapsRequired = 2
cell.addGestureRecognizer(gesture)
// Display "initial" flag image
var initialThumbnail = UIImage(named: "question")
cell.postsImageView.image = initialThumbnail
// Display the country name
if let user = item["uploader"] as? PFUser{
item.fetchIfNeeded()
cell.userName!.text = user.username
var profileImgFile = user["profilePicture"] as! PFFile
cell.profileImageView.file = profileImgFile
cell.profileImageView.loadInBackground { image, error in
if error == nil {
cell.profileImageView.image = image
}
}
var sexInt = user["sex"] as! Int
var sex: NSString!
if sexInt == 0 {
sex = "M"
}else if sexInt == 1{
sex = "F"
}
var height = user["height"] as! Int
cell.heightSexLabel.text = "\(sex) \(height)cm"
}
if let votesValue = item["votes"] as? Int
{
cell.votesLabel?.text = "\(votesValue)"
}
// Fetch final flag image - if it exists
if let value = item["imageFile"] as? PFFile {
println("Value \(value)")
cell.postsImageView.file = value
cell.postsImageView.loadInBackground({ (image: UIImage?, error: NSError?) -> Void in
if error != nil {
cell.postsImageView.image = image
}
})
}
return cell
}
I have implemented SDWebImage using Pods and have imported through the Bridging Header. Is there anyone who knows how to implement SDWebImage with parse using Swift?
You should rethink your approach -
I believe you are using collectionViewDelegate method - collectionView(_:cellForItemAtIndexPath:)
this fires every time the collection view needs a view to handle.
In there you can access the cell imageView and set its image (For Example)-
cell.imageView.sd_setImageWithURL(url, placeholderImage:placeHolderImage, completed: { (image, error, cacheType, url) -> Void in })
And if you wish to fade in the image nicely, you could -
cell.imageView.sd_setImageWithURL(url, placeholderImage:placeHolderImage, completed: { (image, error, cacheType, url) -> Void in
if (cacheType == SDImageCacheType.None && image != nil) {
imageView.alpha = 0;
UIView.animateWithDuration(0.0, animations: { () -> Void in
imageView.alpha = 1
})
} else {
imageView.alpha = 1;
}
})
EDIT
I see the you use Parse, so you don't need SDWebImage, you need to use Parse - PFImageView, It will handle your background fetch for the image when it loads. You will need to save reference to your PFObject, but I believe you already do that.
For example (inside your cellForItemAtIndexPath)-
imageView.image = [UIImage imageNamed:#"..."]; // placeholder image
imageView.file = (PFFile *)someObject[#"picture"]; // remote image
[imageView loadInBackground];
How many objects are displaying in the collection view?
Since you mentioned SDWebImage, are you downloading the images in the background as well?
If you want to load the images as the user scrolls, have a look at the documentation for SDWebImage. The first use case describes how to display images in table view cells withouth blocking the main thread. The implementation for collection view cells should be similar.

Resources