0

I'll jump right to it.

I have a cell with a titleLabel that prints a code and a detailTextLabel that prints a decoded version of that code. I have a plist dictionary database containing hundreds of etries like this: "JD" : "John Doe".

When setting the detailTextLabel in cellForRow I initially had a function that looked up the code in the plist and returned the decoded value. This worked, but the scroll was extremely sluggish.

I am now doing the same thing, but this time the decode lookup is happening in the background and returns the result in a callback closure. The scroll is smooth and it works fine. Almost. Sometimes the decoded value is returned for the wrong cell (since it is reused). Is this preventable?

Here is what I have in my cellForRow function:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    //Create the cell
    let cell = tableView.dequeueReusableCellWithIdentifier("RosterCell", forIndexPath: indexPath) as UITableViewCell

    if let object = self.fetchedResultsController.objectAtIndexPath(indexPath) as? Object
    {

        cell.textLabel?.text = object.rawCode

        CodeDecoder.getDescriptionInBackground(CodeType.All, code: rawCode) { (description) -> () in

            if description.isEmpty == false {
                cell.detailTextLabel?.text = description
            }
            else {
                cell.detailTextLabel?.text = "No decode found for code: \(object.rawCode)."
            }
        }

    }
    else {
        cell.textLabel?.text = "Object not found."
        cell.detailTextLabel?.text = ""
    }


    return cell

}

I would greatly appreciate any help or ideas around this.

fisher
  • 1,286
  • 16
  • 29

3 Answers3

2

The other answers in here are good suggestions, but don't hit the root issue that you're having. Since you've already identified that you need to decode data asynchronously, now you need to figure out how to cancel that operation when you reuse that cell. I would venture a guess that the following method:

CodeDecoder.getDescriptionInBackground(CodeType.All, code: rawCode)

runs in a dispatch_async on some background queue.

Instead, you could use an NSOperationQueue which will generate NSOperation objects that can be cancelled. Then in your prepareForReuse method, you could cancel the operation that may be running. Here is a quick example of how this could work.

CodeDecoder

class CodeDecoder {

    let operationQueue: NSOperationQueue

    enum CodeType {
        case All
    }

    init() {
        self.operationQueue = NSOperationQueue()
        self.operationQueue.qualityOfService = .UserInitiated
        self.operationQueue.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount
    }

    func getDescriptionInBackground(#codeType: CodeType, code: Int, completionHandler: (String) -> Void) -> NSOperation {

        let operation = NSBlockOperation()
        weak var weakOperation = operation

        operation.addExecutionBlock {
            if let strongOperation = weakOperation {
                if strongOperation.cancelled {
                    return
                }

                // Run your decode logic

                if strongOperation.cancelled {
                    return
                }

                dispatch_async(dispatch_get_main_queue()) {
                    completionHandler("decoded value")
                }
            }
        }

        self.operationQueue.addOperation(operation)

        return operation
    }
}

CustomViewController

class CustomViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

        //Create the cell
        let cell = tableView.dequeueReusableCellWithIdentifier("RosterCell", forIndexPath: indexPath) as CustomTableViewCell

        if let object = self.fetchedResultsController.objectAtIndexPath(indexPath) as? Object {
            cell.textLabel?.text = object.rawCode

            let operation = CodeDecoder.getDescriptionInBackground(CodeType.All, code: rawCode) { description in

                if description.isEmpty == false {
                    cell.detailTextLabel?.text = description
                }
                else {
                    cell.detailTextLabel?.text = "No decode found for code: \(object.rawCode)."
                }
            }

            cell.operation = operation
        }
        else {
            cell.textLabel?.text = "Object not found."
            cell.detailTextLabel?.text = ""
        }

        return cell
    }
}

CustomTableViewCell

class CustomTableViewCell: UITableViewCell {
    var operation: NSOperation?

    override func prepareForReuse() {
        super.prepareForReuse()

        if let operation = operation {
            if !operation.finished {
                operation.cancel()
                self.operation = nil
            }
        }
    }
}

Definitely double check the weakify / strongify logic. I can't test it since this isn't a fully compiling chunk of code. That should allow you to run all your decoding in the background, update the cells on the main thread and cancel the decoding operations when you need to reuse the cell.

cnoon
  • 16,575
  • 7
  • 58
  • 66
  • Excellent reply. This is what I'm looking for. Yes, you're right. I am running the getDescriptionInBackground in the backgorund with GCD and returning the completion on the main queue. I will get back to you as soon as I have tested it. Thank you very much for the comprehensive answer. – fisher Feb 17 '15 at 18:05
  • Amazing. Works perfectly. Just what I had in mind. Thank you! I'm guessing the "let operation = CodeDecoder.getDescriptionInBackground" should be "let operation = CodeDecoder().getDescriptionInBackground" to initialize the CodeDecoder? And while you're here, the reason for why the operation should return if cancelled before and after the "decode logic" is to cancel the operation before the decode logic if the cell is already reused and after so it doesn't return the callback after the decode logic is complete but cell is reused. Is that correct? – fisher Feb 17 '15 at 20:12
  • And one last thing. The weak / strong concept is quite new to me. What is the procedure to check if it works well and no strong referance cycles are made? I'll try my best to read up on it. – fisher Feb 17 '15 at 20:15
  • 1
    I didn't initialize the `CodeDecoder` because I wasn't sure how you built it. It looked to me that you were probably using class methods. If not, then maybe using a singleton. You could certainly instantiate a `CodeDecoder` as you showed, but you need to make sure you don't deallocate that object out from under yourself while you're running your async operations. – cnoon Feb 18 '15 at 01:23
  • 1
    To your second comment, canceling the operation at various points is the only way that calling `operation.cancel()` will ever do anything for you. You want to give that operation as many chances to exit as you can. If you cancel after the operation starts changing the text, you can't stop it anymore. Therefore, you want to cancel in all the sensible places. – cnoon Feb 18 '15 at 01:24
  • 1
    As for weakify / strongify rules, here is a great article for [Objective-C](https://blackpixel.com/writing/2014/03/capturing-myself.html) and here's a really good reference thread for [Swift](http://stackoverflow.com/questions/24320347/shall-we-always-use-unowned-self-inside-closure-in-swift). – cnoon Feb 18 '15 at 01:27
1

Yes it is preventable.

Your problem comes from the fact that the CollectionViewCells are reused, and like you said if you load the text in the main thread it take a while until one cell is loaded and could be presented (because of the text lookup).

What you can do is a few things.

1) load the text you want to present in a earlier stage into an array (prior viewController, loading screen), then load the collectionViewCells from that array. This will cause the loading the drastically go down.

1.1) if you don't want to load all the data prior to the cell loading you can write a mechanism to load more data in the background once x amount of cells were loaded (or something like that)

2) you can create a UICollectionViewCells pool in a prior time. The cells will be loaded from that pool, and every new allocated cell will be inserted into that pool.

You can do several other mechanisms but these are the basics ones

YYfim
  • 1,402
  • 1
  • 9
  • 24
  • Thanks for your reply! Good options. As for option 1 and 1.1: This works, but I like to keep it more dynamic. The objects are from core data, so when an object is inserted, updated or modified that will force me to the whole array again which is kind not very efficient. As for option 2. This is a new concept for me. I'll look into it and report back. So there is no way to use my method and make sure that the result is only displayed for the correct cell? Thanks again for taking your time to answer! – fisher Feb 17 '15 at 14:37
  • the problem that causes the cell to be presented wrong is you method.. the cells are presented before you are updating you model – YYfim Feb 17 '15 at 15:21
  • Keeping it dynamic is not a problem, especially if you are using coreData. fetches queries are quite good and you can user nsfetchedresultscontroller to create a more efficient fetches – YYfim Feb 17 '15 at 15:23
0

Why you not create subclass of UITableViewCell(ObjectTableViewCell for example) with object property? and then, in tableView: rowAtIndexPath: your code will be like this:

let cell = tableView.dequeueReusableCellWithIdentifier("RosterCell", forIndexPath: indexPath) as ObjectTableViewCell
cell.object = self.fetchedResultsController.objectAtIndexPath(indexPath) as? Object
return cell

in ObjectTableViewCell class:

var object: Object? {
   didSet {
      //check object for nil and do other your code here
   }
}
Taras Chernyshenko
  • 2,729
  • 14
  • 27
  • Thanks for your reply. This is something I have tried and the result is the same. I am still required to to do the heavy operation of finding the decode inside the didSet message. – fisher Feb 17 '15 at 14:39