Swift TableView Cell Images initially loading out of order - swift

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.

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.

Understanding crash log iPhone

I am a beginner developer, who tries his best. Currently I'm working on a complicated application, and everything works fine: expect one thing. 1 out of 100 times some basic thing crashes my app.
(This application is a Social posting app)
1. When I open a post to view it's comments, everything works fine. 99% of the time. But last time it crashed out of nowhere. I don't really understand.
2. The other time I opened the app after 1 hour no usage, and suddenly the app crashed.
Could someone help me out? Am I right with the following guesses?
First case:
Pastebin Link
I think this is caused by reloadSections:withRowAnimation: . Am I correct?
Second case:
Pastebin Link
And this is because of performBatchUpdates I call on a collection view. Right?
But I don't really understand where could be the problem's root.
Here's my code, it looks completely harmless (for the second example):
It's a collection view inside a tableCell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.section {
case 0 :
let cell = tableView.dequeueReusableCell(withIdentifier: "StoriesCell", for: indexPath) as! StoriesCell
// Could use cell.storiesCollectionView.reloadData() too, but it flickers then
UIView.animate(withDuration: 0, animations: {
cell.storiesCollectionView.performBatchUpdates({
cell.storiesCollectionView.reloadSections(IndexSet(0...0))
})
})
cell.setCollectionViewDataSourceDelegate(self, forRow: indexPath.row)
cell.collectionViewOffset = storedStoriesCollectionViewOffset[indexPath.row] ?? 0
return cell
case 2 :
// ...

Data persistence with reusable cells

I have a large table designed to update a customers data protection preferences.
Some of the table is populated with reusable cells that contain a variable number of checkboxes, and depending on the json returned from the server, some of these checkboxes may be pre-checked.
When I pass the pre-checked state to the cell from tableView cellForRowAt all is well (checkboxes that are pre-checked are pre-checked). The problem I have is that these are reusable cells, and after a user has changed their selections, scrolling up or down the table triggers more calls to the setupCell function, which then resets the checkboxes to their original pre-checked state.
So, the question I have is...
What are the options for me to preserve a user’s selections after they have scrolled a table with resuable cells?
The switch statement in setupCell currently sets the pre-selections with the call to updateSelections(). Obviously this is the cause of the issue and I'm not entirely happy with placing logic directly in the cell anyway, but where is the best place to perform this logic only once? Or, is using reusable cells the wrong approach entirely to have pre-selections?
Any suggestions welcome. Here's a small code snippet to illustrate the point:
// UITableViewDataSource - passing the previous selections to setupCell in the UITableViewCell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let viewModel = viewModels[indexPath.row]
switch viewModel {
case .preferences(let preferenceId, let titleText, let isEnabled):
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Preferences") as? MarketingChannelPreferencesTableViewCell else {
return UITableViewCell()
}
cell.setupCell(id: id, text: text, isPreChecked: isPreChecked)
return cell
}
}
// UITableViewCell
func setupCell(id: String, text: String, isPreChecked: Bool) {
switch id {
case "email":
emailSelected = isPreChecked
updateSelections(id: id, isPreChecked: emailSelected)
case "post":
postSelected = isPreChecked
updateSelections(id: id, isPreChecked: postSelected)
case "text":
textSelected = isPreChecked
updateSelections(id: id, isPreChecked: textSelected)
default:
break
}
}
viewModels hold the information needed to setup each cell right? And you receive the viewModels from the service?
If so, when the user changes a specific checkbox, you should update the according viewModel. Thus, when you call setupCell inside cellForRowAt you should pass the updated info of each viewModel, resulting in the correct state of each checkbox.
You should make some action method for your checkbox buttons that you put on the MarketingChannelPreferencesTableViewCells and change your viewModels based on changing the value of these checkboxes. So when the cells data reload with user scrolling, cells show the new datas of viewModel
There're several ways.
I've created a small project maybe it will give you an solution to the problem.
https://drive.google.com/open?id=1d_RFdr6luNvRTdSC6XNE2vRWTi2IRyuT

Best way to set up multiple tableviewcell layouts

I am in the process of creating a new tableviewcontroller however, I need this to be dynamic in a way that the layouts for each cell could change.
What I mean is that I am parsing news from JSON. Each news item could be of 3 types: Post, achievement, announcement. Each of these types contains different pieces of information. Post would have title, date, image and tags. Announcement would have the same but include an importance label. Achievement will have username, image, date, profile image, social sharing.
The news listing page will display all of the items but ordered by date but the layout for each of the types are completely different.
I have started and successfully got the below working:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if(indexPath.row == 0)
{
let cell: NewsPostTableViewCell = tableView.dequeueReusableCellWithIdentifier("newsPostTableCell") as! NewsPostTableViewCell
return cell
}
else
{
let cell: AchievementCardTableViewCell = tableView.dequeueReusableCellWithIdentifier("achievementCardTableCell") as! AchievementCardTableViewCell
return cell
}
}
to display 2 of the different layouts but I then started thinking, how will I be able to determine in the cellForRowAtIndexPath what layout to apply? Basically in the JSON response there will be a template identifier to notify the app which template to apply but just don't know how to apply this...if possible.
Would it be best to create 3 different view controllers 1 for each of the layouts and embed them?
Depending on how you are parsing the JSON you could store the type of cell into an Array. Then as you look into the array with indexPath.row based on the type it finds there you have it execute a Switch.

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

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.