Why do I have to click the row to reveal the image since it has been loaded? - swift

I am on the assignment 4 of Stanford Course "Developing iOS 8 Apps With Swift" by Paul Hegarty. The assignment is developing an app searching Twitter to get some tweets and display them in a tweets table view. And if i click one row of the tweets table view, it segues to a detail table view which displays the hashtag, urls, user mentioned and attached media photos of the tweet in four sections.
The media photo section displays the images attached in the tweet in its own custom cell called mediaCell which contains only a single imageView. But I find, after the image in the image URL is loaded to the imageView of the mediaCell using NSData(contentsOfURL:) and UIImage(data:)method, the imageView's image doesn't show until I click the row which has no segues. Just one click can show the image and if i don't click, the image just exists in the memory and can't be drawn in the correspond imageView.
Here is the code downloading image in the URL and loading it to the imageView in the mediaCell.
The mediaCell is a custom UITableViewCell which only has an imageView called mediaImageView.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(Storyboard.tweetDetailGeneralCellIdentifier, forIndexPath: indexPath)
// Configure the cell...
switch indexPath.section {
case 0:
cell.textLabel?.text = tweet.hashtags[indexPath.row].keyword
return cell
case 1:
cell.textLabel?.text = tweet.urls[indexPath.row].keyword
return cell
case 2:
cell.textLabel?.text = tweet.userMentions[indexPath.row].keyword
return cell
case 3:
let mediaCell = tableView.dequeueReusableCellWithIdentifier(Storyboard.tweetDetailMediaCellIdentifier, forIndexPath: indexPath) as! TweetDetailMediaCell
if !tweet.media.isEmpty {
for media in tweet.media {
let qos = Int(QOS_CLASS_USER_INITIATED.rawValue)
let queue = dispatch_get_global_queue(qos, 0)
dispatch_async(queue){
let imageData = NSData(contentsOfURL: media.url)
if imageData != nil {
dispatch_async(dispatch_get_main_queue()) {
mediaCell.mediaImageView.image = UIImage(data: imageData!)
print("mediaCellImage is loaded")
}
}
}
}
}
return mediaCell
default: break
}
return cell
}
when the "mediaCellImage is loaded" is printed, the image load should be finished, but if i don't click the row, the image never show up. if i click even just one time, it will show up.
there is no change if i add "mediaCell.mediaImageView.setNeedsDisplay" after the print("mediaCellImage is loaded").

The problem is that when the image would be loaded, the mediaCell may be dequeued for another row.
Dequeue the cell one more time when an image would be loaded (inside the main queue):
if let mediaCell = tableView.cellForRowAtIndexPath(indexPath) as? TweetDetailMediaCell {
mediaCell.mediaImageView.image = UIImage(data: imageData!)
}

Finally, i find the real reason lies in the number of prototype cell instance which is created more than the number needed.
whenever the table view asks for a cell, i create a prototype cell instance first. and then i check whether it is suitable for the indexPath. if not, i then create a custom cell instance and return it.
That means some prototype cell instances are created unused. cell instance is expensive, i think apple uses this unused cells for some performance enhance(i am not sure about this). Therefore, they affect the custom cell instance appearance, resulting in situation where i have to click the row to reveal the image. After i correct this error, everything works fine. If anybody knows the detail reason, please post here. Really appreciated.
Hope this answer is helpful.

Related

UICollectionView cell reuse and online images

I am trying to understand just how UICollectionView cell reuse works.
I am currently implementing a horizontally scrolling UICollectionView with large cells that take up almost the full size of the screen. There are about 100+ cells but you will only ever see ~3 at a time.
As I understand it UICollectionView cell reuse simply maintains a pool of initialized cell objects that way when one cell is out of view it can be cannibalized by a newly viewable cell. That is to say since I am using reuse the collection might only initialize ~3 actual cell objects in memory and I just will switch out their contents.
I am very worried about what this means in the case of custom cells that have image views that are based on images that need to be downloaded. Ideally I would have a scenario where every cells image is only ever downloaded once and it is only downloaded when absolutely necessary.
If there is truly a pool of my custom cell objects then this means that that is totally not happening. As each time a cell comes into view I am starting a completely new download.
How am I supposed to do this right?
The main reason I am asking this is that when scrolling (especially on the initial scroll) I do see some flickering between an image of an old cell and the image the cell is supposed to be displaying. I made a fix but I am fairly sure that it is causing the online images to be downloaded too many times. Am I doing this right?
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath) as? ImageCell else {
assert(false)
}
let image = data[indexPath.row]
cell.display(title: image.title, imageURL: image.imageURL)
return cell
}
And the cell
public class NewsCell: UICollectionViewCell {
private var title: UILabel = UILabel()
private var imageView: UIImageView = UIImageView()
override public init(frame: CGRect = CGRect.zero) {
super.init(frame: frame)
title.font = UIFont.systemFont(ofSize: 12, weight: .bold)
title.textColor = UIColor.white
title.textAlignment = NSTextAlignment.left
imageView.contentMode = UIViewContentMode.scaleAspectFill
imageView.clipsToBounds = true
contentView.addSubview(imageView)
imageView.addSubview(title)
// Layout constraints
}
public func display(title: String, imageURL: URL?) {
self.imageView.image = nil
self.title.text = title
if let url = imageURL {
downloadImage(from: url)
}
}
func downloadImage(from url: URL) {
getData(from: url) { data, response, error in
guard let data = data, error == nil else {
return
}
DispatchQueue.main.async {
self.imageView.image = UIImage(data: data)
}
}
}
func getData(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> Void) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}
}
I am worried by the fact that I have to set the image views image to nil on display in order to prevent the flicker. Should I be doing something differently to avoid frivolous downloading of these images or does this look good?
You can use Prefetching Collection View Data to load your images earlier.
You use data prefetching when loading data is a slow or expensive process—for example when fetching data over the network. In these circumstances, perform data loading asynchronously.
It will require some changes of course. You will have to store your images separately and download them when DataSourcePrefetching method will be called. Also, then the image is downloaded, you'll need to check if there is any cell that waiting for that image. So your UICollectionView won't download anything anymore. It will show the image only, or waiting for it to be downloaded.
There is a problem with your current solution. If you'll scroll too fast, you make face a situation when because of reusing, the same UICollectionViewCell is loading a few images at once. And in this case, the user will see only the last one, and you never can tell which one it would be. To avoid this race condition, you can store the image identifier or its URL in the cell so after downloading is finished, it could check if the downloaded image is the right one.
It's okay to set image to nil while it's downloading, but you also can show to the user some UIActivityIndicatorView so he could see that some work is happening.

Swift TableView Cell Images initially loading out of order

On the first View Controller I have a tableview that lists various sites like google, stack overflow... For each service added an image will load based on the first letter of that site. So an image of a T will load for Twitter.
If the user wants he/she can tap that cell and go to a 2nd VC and add the URL. When the user comes back to the first VC that site will try to pull the favicon instead of the letter image... this works, but not gracefully.
What I initially wanted was for each image to show up as soon as it loaded, obviously not all at once and not so it would disrupt the user interaction with the app.
What is happening now is that they show up a few at a time (which is ok) but not in the right place, initially. So say I have amazon, google, Microsoft, Facebook, and apple...the favicon would actually be out of order so Microsoft might have googles logo and after several seconds it might shift to Facebooks and then depending on how many there are it might shift again until it is all in the right matching place…This also happens if i scroll 'below the fold' and after several moments will right itself (the cell title remains in order however)
So I obviously have something in my code wrong, and would love, at the minimum, get it so it puts Facebook right the first time and then google, etc etc
OR another option could be all of the images START out as the letter image and then the code tries to replace it with the favicon...and get it right on the first try..
Any help would be great
Here is my Table View cellForRowAt code
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "serviceCell", for: indexPath as IndexPath) as! ServiceTableViewCell
let row = indexPath.row
cell.serviceNameLabel.text = serviceArray[row].serviceName
DispatchQueue.global().async {
let myURLString: String = "http://www.google.com/s2/favicons?domain=\(self.serviceArray[row].serviceUrl)"
if let myURL = URL(string: myURLString), let myData = try? Data(contentsOf: myURL), let image = UIImage(data: myData) {
cell.serviceLogoImage.image = image
} else {
cell.serviceLogoImage.image = UIImage.init(named: "\(self.getLetterOrNumberAndChooseImage(text: self.serviceArray[row].serviceName))")
}
}
return cell
}
}
cell.serviceLogoImage.image =
Must be called from the Main Thread, since you are modifying User Interface.
Apple Documentation
Note For the most part, use UIKit classes only from your app’s main
thread. This is particularly true for classes derived from UIResponder
or that involve manipulating your app’s user interface in any way.
Check this thread how to run the different threads from background and to the main.

Selecting Multiple Table View Cells At Once in Swift

I am trying to make an add friends list where the user selects multiple table view cells and a custom check appears for each selection. I originally used didSelectRowAtIndexPath, but this did not give me the results I am looking for since you can highlight multiple cells, but unless you unhighlight the original selected row you cannot select anymore. I then tried using didHighlighRowAtIndexPath, but this doesn't seem to work because now I am getting a nil value for my indexPath. Here is my code:
override func tableView(tableView: UITableView, didHighlightRowAtIndexPath indexPath: NSIndexPath) {
let indexPath = tableView.indexPathForSelectedRow
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as! AddedYouCell
let currentUser = PFUser.currentUser()?.username
let username = currentCell.Username.text
print(currentCell.Username.text)
let Friends = PFObject(className: "Friends");
Friends.setObject(username!, forKey: "To");
Friends.setObject(currentUser!, forKey: "From");
Friends.saveInBackgroundWithBlock { (success: Bool,error: NSError?) -> Void in
print("Friend has been added.");
currentCell.Added.image = UIImage(named: "checked.png")
}
}
How can I solve this? Thanks
I'm not going to write the code for you, but this should help you on your way:
To achieve your goal, you should separate the data from your views (cells).
Use an Array (i.e. friendList) to store your friend list and selected state of each of them, and use that Array to populate your tableView.
numberOfCellsForRow equals friendList.count
In didSelectRowAtIndexPath, use indexPath.row to change the state of your view (cell) and set the state for the same index in your Array
In cellForRowAtIndexpath, use indexPath.row to retrieve from the Array what the initial state of the cell should be.

tableView.cellForRowAtIndexPath(indexPath) return nil

I got a validation function that loop through my table view, the problem is that it return nil cell at some point.
for var section = 0; section < self.tableView.numberOfSections(); ++section {
for var row = 0; row < self.tableView.numberOfRowsInSection(section); ++row {
var indexPath = NSIndexPath(forRow: row, inSection: section)
if section > 0 {
let cell = tableView.cellForRowAtIndexPath(indexPath) as! MyCell
// cell is nil when self.tableView.numberOfRowsInSection(section) return 3 for row 1 and 2
// ... Other stuff
}
}
}
I'm not really sure what I'm doing wrong here, I try double checking the indexPath row and section and they are good, numberOfRowsInSection() return 3 but the row 1 and 2 return a nil cell... I can see my 3 cell in the UI too.
Anybody has an idea of what I'm doing wrong?
My function is called after some tableView.reloadData() and in viewDidLoad, is it possible that the tableview didn't finish reloading before my function is executed event though I didn't call it in a dispatch_async ??
In hope of an answer.
Thank in advance
--------------------------- Answer ------------------------
Additional explanation :
cellForRowAtIndexPath only return visible cell, validation should be done in data model. When the cell is constructed in
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
It should change itself according to the validation state.
As stated in the documentation, cellForRowAtIndexPath returns:
An object representing a cell of the table, or nil if the cell is not visible or indexPath is out of range.
Hence, unless your table is fully displayed, there are some off screen rows for which that method returns nil.
The reason why it returns nil for non visible cells is because they do not exist - the table reuses the same cells, to minimize memory usage - otherwise tables with a large number of rows would be impossible to manage.
So, to handle that error just do optional binding:
// Do your dataSource changes above
if let cell = tableView.cellForRow(at: indexPath) as? MyTableViewCell {
// yourCode
}
If the cell is visible your code got applied or otherwise, the desired Cell gets reloaded when getting in the visible part as dequeueReusableCell in the cellForRowAt method.
I too experienced the issue where cellForRowAtIndexPath was returning nil even though the cells were fully visible. In my case, I was calling the debug function (see below) in viewDidAppear() and I suspect the UITableView wasn't fully ready yet because part of the contents being printed were incomplete with nil cells.
This is how I got around it: in the viewController, I placed a button which would call the debug function:
public func printCellInfo() {
for (sectionindex, section) in sections.enumerated() {
for (rowIndex, _) in section.rows.enumerated() {
let cell = tableView.cellForRow(at: IndexPath(row: rowIndex, section: sectionindex))
let cellDescription = String(describing: cell.self)
let text = """
Section (\(sectionindex)) - Row (\(rowIndex)): \n
Cell: \(cellDescription)
Height:\(String(describing: cell?.bounds.height))\n
"""
print(text)
}
}
}
Please note that I'm using my own data structure: the data source is an array of sections, each of them containing an array of rows. You'll need to
adjust accordingly.
If my hypothesis is correct, you will be able to print the debug description of all visible cells. Please give it a try and let us know if it works.

Cannot load image from PARSE into PFTableViewCell's imageView property

I'm working with the PFQueryTableViewController and setting it up to find only friendships for this user which has been approved or sent to him.
"fromUser" and "toUser" are pointers to the user class, where I need the username and profilePicture from for each of the users contained in the queries results.
Now I try to fetch those in my cellForRowAtIndexPath method and load the image:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell? {
let cellIdentifier = "contactCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! PFTableViewCell
if let user = object?["fromUser"] as? PFUser {
user.fetchInBackgroundWithBlock({ (user, error) -> Void in
if error != nil {
println("Could not fetch user object")
}
let user = user as! PFUser
cell.textLabel?.text = user.username!
cell.imageView?.file = user["profilePicture"] as? PFFile
cell.imageView?.loadInBackground()
})
} }
Getting the username to display in the tableView works just fine, but the image is actually never loaded. I tried different approaches to get the image loaded, but my cell's imageView property is always nil.
The prototype's class is set to PFTableViewCell
The controller is linked to the view in storyboard
Please let me know, if you guys have any idea why this built in property is nil and how to fix that.
Thanks,
Well I found a workaround which actually works quite good:
Create a prototype cell in your TableView (Storyboard) and set it's class to the normal "UITableViewCell"
Set the reuseIdentifier property of this cell to a value of your liking
Let your custom cell's file owner (I created a nib file for this cell) to be a subclass of PFTableViewCell
Create custom outlets for the textLabel and imageView
Register that Nib for the reuseIdentifier set in Step 2 in your TableViewController
Finally, use that class in your cellForRowAtIndexPath method like this:
let cell: PeopleTableViewCell! = tableView.dequeueReusableCellWithIdentifier("peopleCell") as? PeopleTableViewCell
I hope this will fix your problems too.
Regards,
[Edit]: It seems like SO doesn't like my code snippet to be formatted. It's embedded in code tags..
The PFTableViewCell does not work well with standard styles. So you have to make your cell a custom cell.
These re the steps I took to make it work (in Storyboard):
subclass PFTableViewCell with you own class
in the storyboard, customize the prototype cell using a custom cell
drag a UIImageView from the palette but then set its class as PFImageView
connect it to an IBOutlet in your PFQueryTableViewController subclass
implement cellForRowAtIndexPath: in the PFQueryTableViewController the way you did (your code was OK).
That way worked for me.