3

I have a list of reddit posts that I want to display the thumbnail of, if it exists. I have it functioning, but it's very buggy. There are 2 main issues:

  • Images resize on tap
  • Images shuffle on scroll

This is the code:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Post", forIndexPath: indexPath) as UITableViewCell
    let post = swarm.posts[indexPath.row]
    cell.textLabel!.text = post.title

    if(post.thumb? != nil && post.thumb! != "self") {
        cell.imageView!.image = UIImage(named: "first.imageset")
        var image = self.imageCache[post.thumb!]

        if(image == nil) {
            FetchAsync(url: post.thumb!) { data in // code is at bottom, this just drys things up
                if(data? != nil) {
                    image = UIImage(data: data!)
                    self.imageCache[post.thumb!] = image
                    dispatch_async(dispatch_get_main_queue(), {
                        if let originalCell = tableView.cellForRowAtIndexPath(indexPath) {
                            originalCell.imageView?.image = image
                            originalCell.imageView?.frame = CGRectMake(5,5,35,35)
                        }
                    })
                }
            }
        } else {
            dispatch_async(dispatch_get_main_queue(), {
                if let originalCell = tableView.cellForRowAtIndexPath(indexPath) {
                    originalCell.imageView?.image = image
                    originalCell.imageView?.frame = CGRectMake(5,5,35,35)
                }
            })
        }
    }

    return cell
}

This is the app when it loads up - looks like everything is working:

loaded up, looks like everything is working

Then if I tap on an image (even when you scroll) it resizes:

pictures resize on tap

And if you scroll up and down, the pictures get all screwy (look at the middle post - Generics fun):

pictures shuffle on scroll

What am I doing wrong?

** Pictures and Titles are pulled from reddit, not generated by me **


EDIT: FetchAsync class as promised:

class FetchAsync {
    var url: String
    var callback: (NSData?) -> ()

    init(url: String, callback: (NSData?) -> ()) {
        self.url = url
        self.callback = callback
        self.fetch()
    }

    func fetch() {
        var imageRequest: NSURLRequest = NSURLRequest(URL: NSURL(string: self.url)!)
        NSURLConnection.sendAsynchronousRequest(imageRequest,
            queue: NSOperationQueue.mainQueue(),
            completionHandler: { response, data, error in
                if(error == nil) {
                    self.callback(data)
                } else {
                    self.callback(nil)
                }
        })
        callback(nil)
    }
}
cadlac
  • 2,802
  • 3
  • 18
  • 34
  • I think that your `originalCell` constant is the old value of the cell, because you getting it from `tableView.cellForRowAtIndexPath(indexPath)`and you're in the function that "refresh" it. So try making your changes on `cell` – Pintouch Oct 16 '14 at 12:42
  • The reason for that is that it was put in a background queue, so the table cell could be long gone by the time it downloads the image. Switching `originalCell` to `cell` did not fix anything, if anything it seemed to make the bugs worse, but that could be my imagination – cadlac Oct 16 '14 at 14:54
  • Did you try running it with debugger? You can try calling `tableView.reloadData`. (I can't test right now, so it's just a suggestion) – Pintouch Oct 16 '14 at 15:26
  • Apparently not enough :). I'll put some more work into it tonight. and I don't think reloading the entire tableView every time a new cell is displayed is the solution... it might work, but I don't think it would resolve the underlying issue. – cadlac Oct 16 '14 at 18:40
  • Sounds like your datamodel is not matching up. – CaptainCOOLGUY Oct 18 '14 at 08:37

3 Answers3

5

Unfortunately, this seems to be a limitation of the "Basic" table view cell. What I ended up doing was creating a custom TableViewCell. A relied on a tutorial by Ray Wenderlich that can be found here: http://www.raywenderlich.com/68112/video-tutorial-table-views-custom-cells

It's a bit of a bummer since the code is so trivial, but I guess on the bright side that means it's a 'simple' solution.

My final code:

PostCell.swift (all scaffolded code)

import UIKit

class PostCell: UITableViewCell {

    @IBOutlet weak var thumb: UIImageView!
    @IBOutlet weak var title: UILabel!

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

}

PostsController.swift

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("PostCell", forIndexPath: indexPath) as PostCell
    let post = swarm.posts[indexPath.row]
    cell.title!.text = post.title

    if(post.thumb? != nil && post.thumb! != "self") {
        cell.thumb!.image = UIImage(named: "first.imageset")
        cell.thumb!.contentMode = .ScaleAspectFit
        var image = self.imageCache[post.thumb!]

        if(image == nil) {
            FetchAsync(url: post.thumb!) { data in
                if(data? != nil) {
                    image = UIImage(data: data!)
                    self.imageCache[post.thumb!] = image
                    dispatch_async(dispatch_get_main_queue(), {
                        if let postCell = tableView.cellForRowAtIndexPath(indexPath) as? PostCell {
                            postCell.thumb!.image = image
                        }
                    })
                }
            }
        } else {
            dispatch_async(dispatch_get_main_queue(), {
                if let postCell = tableView.cellForRowAtIndexPath(indexPath) as? PostCell {
                    postCell.thumb!.image = image
                }
            })
        }
    }

    return cell
}

And my measly storyboard: enter image description here

cadlac
  • 2,802
  • 3
  • 18
  • 34
2

I'm not sure the best way to do this, but here a couple of solutions:

  1. Use AFNetworking, like everyone else does. It has the idea of a place holder image, async downloading of the replacement image, and smart caching. Install using cocoa pods, make a bridging file with #import "UIImageView+AFNetworking.h"

  2. Create two different types of cells. Before grabbing a cell with dequeReusableCell... in your cellForRowAtIndexPath, check if it's expanded. If expanded, return and populate an expanded cell otherwise return and populated an unexpanded cell. The cell is usually expanded if it is the 'selected' cell.

Your mileage may vary

Charles Merriam
  • 19,908
  • 6
  • 73
  • 83
1

it is a huge mistake to call tableView.cellForRowAtIndexPath from within UITableViewDataSource's implementation of tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell. Instead when the async fetch of the thumb image is completed, update your model with the image, then request tableView to reloadRows for that specific cell's indexPath. Let your data source determine the correct indexPath. If the cell is offscreen by the time the image download is complete there will be no performance impact. And of course reloadRows on the main thread.

dferrero
  • 467
  • 6
  • 6