UITableView Floating Cell After Async Fetch - ios

This is in the same vein as a previous question I here
Basically, UITableView cells would occasionally overlap the data underneath them - I tracked that down to reloadRows acting wonky with estimatedHeight, and my solve was to cache the height when calling willDisplay cell: and then return that height, or an arbitrary constant if the row hasn't been seen yet, when calling heightForRow
But now the problem is back! Well, a similar one: after propagating a UITableView with some data, some of it fetched asynchronously, I want to be able to search and repopulate the UITableView.
This data I'm fetching may or may not already be present on the TableView, and in any case I don't consider that - I hit the backend, grab some stuff, and display it.
Except, it gets wonky:
As you can see from the screenshot, there's a cell overlaid on top of another cell with the same content. The TableView only reports there being 2 rows, via numberOfRows, but the View Hierarchy says there are 3 cells present when I click through to the TableView.
Only thing I can figure is there's some weird race condition or interaction that happens when I reloadRow after fetching the openGraph data.
What gives?
Some code:
Search
fileprivate func search(searchText: String, page: Int) {
postsService.searchPosts(searchText: searchText, page: page) { [weak self] posts, error in
if let weakSelf = self {
if let posts = posts, error == nil {
if !posts.isEmpty {
weakSelf.postListController.configureWith(posts: posts, deletionDelegate: nil, forParentView: "Trending")
weakSelf.page = weakSelf.page + 1
weakSelf.scrollTableViewUp()
} else {
// manually add a "No results found" string to tableFooterView
}
} else {
weakSelf.postListController.configureWith(posts: weakSelf.unfilteredPosts, deletionDelegate: nil, forParentView: "Trending")
weakSelf.scrollTableViewUp()
}
}
}
}
**configureWith*
func configureWith(posts: [Post], deletionDelegate: DeletionDelegate?, forParentView: String) {
self.posts = posts
for post in posts {
//some data pre-processing
if some_logic
if rawURLString.contains("twitter") {
let array = rawURLString.components(separatedBy: "/")
let client = TWTRAPIClient()
let tweetID = array[array.count - 1]
client.loadTweet(withID: tweetID, completion: { [weak self] (t, error) in
if let weakSelf = self {
if let tweet = t {
weakSelf.twitterCache.addTweetToCache(tweet: tweet, forID: Int(tweetID)!)
}
}
})
}
openGraphService.fetchOGData(url: rawURL, completion: { [weak self] (og, error) in
weakSelf.openGraphService.fetchOGImageData(url: ogImageURL, completion: { (data, response, error) in
if let imageData = data {
weakSelf.imageURLStringToData[ogImageString] = imageData
weakSelf.queueDispatcher.dispatchToMainQueue {
for cell in weakSelf.tableView.visibleCells {
if (cell as! PostCell).cellPost == post {
let cellIndexPath = IndexPath(row: weakSelf.posts.index(of: post)!, section: 0)
weakSelf.tableView.reloadRows(at: [cellIndexPath], with: UITableViewRowAnimation.automatic)
}
}
}
}
})
})
}
self.deletionDelegate = deletionDelegate
self.parentView = forParentView
queueDispatcher.dispatchToMainQueue { [weak self] in
if let weakSelf = self {
weakSelf.tableView.reloadData()
}
}
scrollToPost()
}

Related

UITableView Async image not always correct

I have a UITableView and during the initial loading of my app it sends multiple API requests. As each API request returns, I add a new row to the UITableView. So the initial loading adds rows in random orders at random times (Mostly it all happens within a second).
During cell setup, I call an Async method to generate an MKMapKit MKMapSnapshotter image.
I've used async image loading before without issue, but very rarely I end up with the image in the wrong cell and I can't figure out why.
I've tried switching to DiffableDataSource but the problem remains.
In my DiffableDataSource I pass a closure to the cell that is called when the image async returns, to fetch the current cell in case it's changed:
let dataSource = DiffableDataSource(tableView: tableView) {
(tableView, indexPath, journey) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "busCell", for: indexPath) as! JourneyTableViewCell
cell.setupCell(for: journey) { [weak self] () -> (cell: JourneyTableViewCell?, journey: Journey?) in
if let self = self
{
let cell = tableView.cellForRow(at: indexPath) as? JourneyTableViewCell
let journey = self.sortedJourneys()[safe: indexPath.section]
return (cell, journey)
}
return(nil, nil)
}
return cell
}
Here's my cell setup code:
override func prepareForReuse() {
super.prepareForReuse()
setMapImage(nil)
journey = nil
asyncCellBlock = nil
}
func setupCell(for journey:Journey, asyncUpdateOriginalCell:#escaping JourneyOriginalCellBlock) {
self.journey = journey
// Store the async block for later
asyncCellBlock = asyncUpdateOriginalCell
// Map
if let location = journey.location,
(CLLocationCoordinate2DIsValid(location.coordinate2D))
{
// Use the temp cached image for now while we get a new image
if let cachedImage = journey.cachedMap.image
{
setMapImage(cachedImage)
}
// Request an updated map image
journey.createMapImage {
[weak self] (image) in
DispatchQueue.main.async {
if let asyncCellBlock = self?.asyncCellBlock
{
let asyncResult = asyncCellBlock()
if let cell = asyncResult.cell,
let journey = asyncResult.journey
{
if (cell == self && journey.id == self?.journey?.id)
{
self?.setMapImage(image)
// Force the cell to redraw itself.
self?.setNeedsLayout()
}
}
}
}
}
}
else
{
setMapImage(nil)
}
}
I'm not sure if this is just a race condition with the UITableView updating several times in a small period of time.
I think this is because when the image is available then that index is not there. Since the table view cells are reusable, it loads the previous image since the current image is not loaded yet.
if let cachedImage = journey.cachedMap.image
{
setMapImage(cachedImage)
}
else {
// make imageView.image = nil
}
I can see you already cache the image but I think you should prepare the cell for reuse like this:
override func prepareForReuse() {
super.prepareForReuse()
let image = UIImage()
self.yourImageView.image = image
self.yourImageView.backgroundColor = .black
}

Index out of range when dismissing view controller with collection view

When I dismiss my customVC while inserting items inside collection view, the app crashes with index out of range error. It doesnt matter how much I remove all collectionView data sources, it still crashes. Heres what I do to insert items:
DispatchQueue.global(qos: .background).async { // [weak self] doesn't do much
for customs in Global.titles.prefix(8) {
autoreleasepool {
let data = self.getData(name: customs)
Global.customs.append(data)
DispatchQueue.main.async { [weak self] in
self?.insertItems()
}
}
}
}
func insertItems() { // this function helps me insert items after getting data and it works fine
let currentNumbers = collectionView.numberOfItems(inSection: 0)
let updatedNumber = Global.customs.count
let insertedNumber = updatedNumber - currentNumbers
if insertedNumber > 0 {
let array = Array(0...insertedNumber-1)
var indexPaths = [IndexPath]()
for item in array {
let indexPath = IndexPath(item: currentNumbers + item, section: 0)
indexPaths.append(indexPath)
}
collectionView.insertItems(at: indexPaths)
}
}
I tried to remove all items in customs array and reload collection view before dismissing but still getting error:
Global.customs.removeAll()
collectionView.reloadData()
dismiss(animated: true, completion: nil)
I suspect that since I load data using a background thread, the collection view inserts the items even when the view is unloaded (nil) but using DispatchQueue.main.async { [weak self] in self?.insertItems() } doesn't help either.

Issue trying to complete Firebase Storage download before showing tableview

I have a table view where depending on the cell class it will download an image from Firebase. I've noticed when using the app that cells with the same cell identifier will show the previous downloaded image before showing the new one. This is what I have before changing it.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableData[indexPath.row]["Image"] != nil {
let cell = tableView.dequeueReusableCell(withIdentifier: "imageNotesData", for: indexPath) as! ImageNotesCell
cell.notes.delegate = self
cell.notes.tag = indexPath.row
cell.notes.text = tableData[indexPath.row]["Notes"] as! String
guard let imageFirebasePath = tableData[indexPath.row]["Image"] else {
return cell }
let pathReference = Storage.storage().reference(withPath: imageFirebasePath as! String)
pathReference.getData(maxSize: 1 * 1614 * 1614) { data, error in
if let error = error {
print(error)
} else {
let image = UIImage(data: data!)
cell.storedImage.image = image
}
}
return cell
}
else {
let cell = tableView.dequeueReusableCell(withIdentifier: "notesData", for: indexPath) as! NotesCell
//let noteString = tableData[indexPath.row]["Notes"] as! String
cell.notes.text = tableData[indexPath.row]["Notes"] as! String
cell.notes.delegate = self
cell.notes.tag = indexPath.row
return cell
}
}
Knowing that this is not a good user experience and that it looks clunky, I tried to move the pathReference.getData to where I setup the data but the view appears before my images finish downloading. I have tried to use a completion handler but I'm still having issues.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
getSectionData(userID: userID, city: selectedCity, completion: {(sectionString) in
self.setupTableCellView(userID: userID, city: selectedCity, section: sectionString) { (tableData) in
DispatchQueue.main.async(execute: {
self.cityName?.text = selectedCity
self.changeSections.setTitle(sectionString, for: .normal)
self.currentSectionString = sectionString
self.setupTableData(tableDataHolder: tableData)
})
}
})
}
func setupTableCellView(userID: String, city: String, section: String, completion: #escaping ([[String:Any]]) -> () ) {
let databaseRef = Database.database().reference().child("Users").child(userID).child("Cities").child(city).child(section)
var indexData = [String:Any]()
var indexDataArray = [[String:Any]]()
databaseRef.observeSingleEvent(of: .value, with: { (snapshot) in
for dataSet in snapshot.children {
let snap = dataSet as! DataSnapshot
//let k = snap.key
let v = snap.value
indexData = [:]
for (key, value) in v as! [String: Any] {
//indexData[key] = value
if key == "Image" {
//let pathReference = Storage.storage().reference(withPath: value as! String)
print("before getImageData call")
self.getImageData(pathRef: value as! String, completion: {(someData) in
print("before assigning indexData[key]")
indexData[key] = someData
print("after assigning indexData[key]")
})
} else {
indexData[key] = value
}
}
indexDataArray.append(indexData)
}
completion(indexDataArray)
})
}
func getImageData(pathRef: String, completion: #escaping(UIImage) -> ()) {
let pathReference = Storage.storage().reference(withPath: pathRef as! String)
pathReference.getData(maxSize: 1 * 1614 * 1614, completion: { (data, error) in
if let error = error {
print(error)
} else {
let image = UIImage(data:data!)
print("called before completion handler w/ image")
completion(image!)
}
})
}
I don't know if I am approaching this the right way but I think I am. I'm also guessing that the getData call is async and that is why it will always download after showing the table view.
You can't do this.
Make the request from Firebase.
Over time, you will get many replies - all the information and all the changing information.
When each new item arrives - and don't forget it may be either an addition or deletion - alter your table so that it displays all the current items.
That's OCC!
OCC is "occasionally connected computing". A similar phrase is "offline first computing". So, whenever you use any major service you use every day like Facebook, Snapchat, etc that is "OCC": everything stays in sync properly whether you do or don't have bandwidth. You know? The current major paradigm of device-cloud computing.
Edit - See Fattie's comments about prepareForReuse()!
With reusable table cells, the cells will at first have the appearance they do by default / on the xib. Once they're "used", they have whatever data they were set to. This can result in some wonky behavior. I discovered an issue where in my "default" case from my data, I didn't do anything ecause it already matched the xib, but if the data's attributes were different, I updated the appearance. The result was that scrolling up and down really fast, some things that should have had the default appearance had the changed appearance.
One basic solution to just not show the previous image would be to show a place holder / empty image, then call your asynchronous fetch of the image. Not exactly what you want because the cell will still show up empty...
Make sure you have a local store for the images, otherwise you're going to be making a server request for images you already have as you scroll up and down!
I'd recommend in your viewDidLoad, call a method to fetch all of your images at once, then, once you have them all, in your success handler, call self.tableview.reloadData() to display it all.

TableView reloading too early

I'm running into a weird issue where my tableView is reloading too early after retrieving JSON data. The strange thing is sometimes it reloads after getting all the required data to fill the tableView and other times it reloads before it can acquire the data. I'm not entirely sure why it's doing this although I do notice sometimes the data is returned as nil. Here is what I use to retrieve the data:
var genreDataArray: [GenreData] = []
var posterStringArray: [String] = []
var posterImageArray: [UIImage] = []
override func viewDidLoad() {
super.viewDidLoad()
GenreData.updateAllData(urlExtension:"list", completionHandler: { results in
guard let results = results else {
print("There was an error retrieving genre data")
return
}
self.genreDataArray = results
for movie in self.genreDataArray {
if let movieGenreID = movie.id
{
GenrePosters.updateGenrePoster(genreID: movieGenreID, urlExtension: "movies", completionHandler: {posters in
guard let posters = posters else {
print("There was an error retrieving poster data")
return
}
for poster in posters {
if let newPoster = poster {
if self.posterStringArray.contains(newPoster){
continue
} else {
self.posterStringArray.append(newPoster)
self.networkManager.downloadImage(imageExtension: "\(newPoster)",
{ (imageData)
in
if let image = UIImage(data: imageData as Data){
self.posterImageArray.append(image)
}
})
break// Use to exit out of array after appending the corresponding poster string
}
} else {
print("There was a problem retrieving poster images")//This gets called sometimes if the poster returns nil
continue
}
}
})
}
}
DispatchQueue.main.async {
self.genresTableView.reloadData()//This is reloading too early before the data can be retrieved
}
})
}
The data is being retrieved asynchronously, and thus your table view can sometimes reload without all the data. What you can do is have the table view reload at the end of the asynchronous data retrieval, or you can reload the cells individually as they come in instead of the whole table using
let indexPath = IndexPath(item: rowNumber, section: 0)
tableView.reloadRows(at: [indexPath], with: .top)
TRY THIS-:
var genreDataArray: [GenreData] = []
var posterStringArray: [String] = []
var posterImageArray: [UIImage] = []
override func viewDidLoad() {
super.viewDidLoad()
genredataArray.removeAll()
posterStringArray.removeAll()
posterImageArray.removeAll()
NOW HERE CALL YOUR CLASS FUNCTION AS ABOVE
}
I guess in that case, you should use
dispatch_async(dispatch_get_main_queue(),{
for data in json as! [Dictionary<String,AnyObject>]
{
//take data from json. . .
}
//reload your table -> tableView.reloadData()
})
You should get the main queue of the thread.

Load image from Parse to UICollectionView cell without lag

I have a pretty elaborate problem and I think someone with extensive async knowledge may be able to help me.
I have a collectionView that is populated with "Picture" objects. These objects are created from a custom class and then again, these objects are populated with data fetched from Parse (from PFObject).
First, query Parse
func queryParseForPictures() {
query.findObjectsInBackgroundWithBlock { (objects: [PFObject]?, err: NSError?) -> Void in
if err == nil {
print("Success!")
for object in objects! {
let picture = Picture(hashtag: "", views: 0, image: UIImage(named: "default")!)
picture.updatePictureWithParse(object)
self.pictures.insert(picture, atIndex: 0)
}
dispatch_async(dispatch_get_main_queue()) { [unowned self] in
self.filtered = self.pictures
self.sortByViews()
self.collectionView.reloadData()
}
}
}
}
Now I also get a PFFile inside the PFObject, but seeing as turning that PFFile into NSData is also an async call (sync would block the whole thing..), I can't figure out how to load it properly. The function "picture.updatePictureWithParse(PFObject)" updates everything else except for the UIImage, because the other values are basic Strings etc. If I would also get the NSData from PFFile within this function, the "collectionView.reloadData()" would fire off before the pictures have been loaded and I will end up with a bunch of pictures without images. Unless I force reload after or whatever. So, I store the PFFile in the object for future use within the updatePictureWithParse. Here's the super simple function from inside the Picture class:
func updateViewsInParse() {
let query = PFQuery(className: Constants.ParsePictureClassName)
query.getObjectInBackgroundWithId(parseObjectID) { (object: PFObject?, err: NSError?) -> Void in
if err == nil {
if let object = object as PFObject? {
object.incrementKey("views")
object.saveInBackground()
}
} else {
print(err?.description)
}
}
}
To get the images in semi-decently I have implemented the loading of the images within the cellForItemAtIndexPath, but this is horrible. It's fine for the first 10 or whatever, but as I scroll down the view it lags a lot as it has to fetch the next cells from Parse. See my implementation below:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(Constants.PictureCellIdentifier, forIndexPath: indexPath) as! PictureCell
cell.picture = filtered[indexPath.item]
// see if image already loaded
if !cell.picture.loaded {
cell.loadImage()
}
cell.hashtagLabel.text = "#\(cell.picture.hashtag)"
cell.viewsLabel.text = "\(cell.picture.views) views"
cell.image.image = cell.picture.image
return cell
}
And the actual fetch is inside the cell:
func loadImage() {
if let imageFile = picture.imageData as PFFile? {
image.alpha = 0
imageFile.getDataInBackgroundWithBlock { [unowned self] (imageData: NSData?, err: NSError?) -> Void in
if err == nil {
self.picture.loaded = true
if let imageData = imageData {
let image = UIImage(data: imageData)
self.picture.image = image
dispatch_async(dispatch_get_main_queue()) {
UIView.animateWithDuration(0.35) {
self.image.image = self.picture.image
self.image.alpha = 1
self.layoutIfNeeded()
}
}
}
}
}
}
}
I hope you get a feel of my problem. Having the image fetch inside the cell dequeue thing is pretty gross. Also, if these few snippets doesn't give the full picture, see this github link for the project:
https://github.com/tedcurrent/Anonimg
Thanks all!
/T
Probably a bit late but when loading PFImageView's from the database in a UICollectionView I found this method to be much more efficient, although I'm not entirely sure why. I hope it helps. Use in your cellForItemAtIndexPath in place of your cell.loadImage() function.
if let value = filtered[indexPath.row]["imageColumn"] as? PFFile {
if value.isDataAvailable {
cell.cellImage.file = value //assign the file to the imageView file property
cell.cellImage.loadInBackground() //loads and does the PFFile to PFImageView conversion for you
}
}

Resources