Lazy image downloading on UITableViewCell using defer - ios

Is it safe and does it make sense to defer an asynchronous image download for a cell? The idea behind this is that I want the callback function from URLSession.shared.image(...) to be executed after creating the cell and only once calling cellForRow(at: indexPath) is valid, since I think that without deferring this method at this point should return nil.
URLSession.shared.image is a private extension that runs a data task and gives a escaping callback only if the url provided in the argument is valid and contains an image.
setImage(image:animated) is a private extension that allows you to set an image in a UIImageView using a simple animation.
If defer is not the way to go, please indicate an alternative.
Any feedback is appreciate, thanks!
override func tableView(_ tableView: UITableView, cellForRowAt
indexPath: IndexPath) -> UITableViewCell {
let cell = baseCell as! MyCell
let datum = data[indexPath.row]
cell.imageView.setImage(placeholderImage, for: .normal)
defer {
URLSession.shared.image(with: datum.previewURL) { image, isCached in
if let cell = tableView.cellForRow(at: indexPath) as? MyCell {
cell.imageView.setImage(image, animated: !isCached)
}
}
}
return cell
}

NSHipster have a good article on how / when to use defer, here.
I wouldn't use defer in such a way. The intention for defer is to clean up / deallocate memory in one block of code, rather than scattering it across many exit points.
Think about having multiple guard statements throughout a function and having to deallocate memory in every one of them.
You shouldn't use this to simply add additional code after the fact.
As mentioned by #jagveer there are many third party libraries that do this already, such as SDWebImage cache. AFNetworking and AlamoFire also have the same built in. No need to re-invent the wheel when its already been done.

In this case, the defer isn't being effective. defer sets up a block to be called when the scope exits, which you are doing immediately.
I think what you want to schedule the block to run in a different thread using Dispatch. You need to get back onto the main thread to update the UI.
As this can happen later, you need to make sure the cell is still being used for the same entry and has not been reused as the user has scrolled further. Fetching the cell again isn't a good idea if it has been reused as you'd end up triggering the initial call again. I usually add some identifier to a custom UITableViewCell class to check against.
Also, you're not creating the cell, just fetching it from some other variable. This is likely to be a problem, if there is more than one cell.
override func tableView(_ tableView: UITableView, cellForRowAt
indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "base") as! MyCell
cell.row = indexPath.row
let datum = data[indexPath.row]
cell.imageView.setImage(placeholderImage, for: .normal)
DispatchQueue.global().async {
// Runs in a background thread
URLSession.shared.image(with: datum.previewURL) { image, isCached in
DispatchQueue.main.async {
// Runs in the main thread; safe for updating the UI
// but check this cell is still being used for the same index path first!
if cell.row == indexPath.row {
cell.imageView.setImage(image, animated: !isCached)
}
}
}
}
return cell
}

Related

How do you launch a model UITableViewController from a UITableViewCell with a delegate?

So, I am pretty sure I am very close to getting this to work. I think it is maybe only the .delegate = line that doesn't work.
Firstly the protocol
protocol beever1: class {
func beever2()
}
Now in the UITableViewCell1:
var delegate1: beever1?
On long press, the below code runs in a funciton (I can't return via UIContextMenuConfiguration because there is a database check in UIContextMenuConfiguration - that's why I am presenting instead of returning).
if let delegate3 = self.delegate1{
delegate3.beever2()
}
Then in the UITableViewController1 that needs to be launched on the longpress
extension UITableViewController1: beever1 {
func beever2() {
let image3 = UIImage(named:"green.png")
if let unwrappedImage1 = image3 {
self.present(ImagePreviewController(image:unwrappedImage1), animated: true, completion: nil)
}
}
}
// I know this part works because I could successfull ypresent it on another VC
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? UITableViewCell1 {
**cell.delegate1 = self**.
// probably where error is but I think it makes sense as is: I am sending delegate info from cell to this UITableViewContoller bacause I want the UITableViewContoller to launch, right?///
return cell
}
return UITableViewCell()
}
Update:
I now think I understand why it isn't presenting, but am not sure how to fix it. UITableViewController1 is not loaded on screen when the long press is made. UITableViewCell1 on a 3rd TableViewController is loaded on screen. Then on long press UITableViewController1 needs to appear. So because UITableViewController1 is not loaded and only needs to be loaded on long press, the cellForRowAt function isn't automatically read
What I think I could do, but am not sure how to code. Option1) on the UITableViewCell1 I need to somehow code the delegate to be self.delegate1 = UITableViewController1, but currently that doesn't work
The Option 2 is the 3rd TableViewController on which the UITableViewCell1 is located. In it's cellForRowAt I could say: cell.delegate1 = UITableViewController1. But that also currently doesn't work
Do y'all agree with my assessment?

Closure Capture Memory Leak issue with UITableView

In willDisplay method, I get UIImage and IndexPath from a callback closure. I am using tableView inside that closure. Should I need to make that tableView weak to avoid possible memory leaks, or is it not an issue to use strong tableView?
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard let cell = cell as? ArtistTableViewCell else { return }
guard let imageUrl = cell.viewModel.artistImage() else { return }
// Download image callback closure returns UIImage, IndexPath, and error
ImageDownloadService.shared.downloadImage(imageUrl,indexPath:indexPath) { [weak tableView] (image, index, error) in
DispatchQueue.main.async {
guard let getIndexPath = index else { return }
guard let getImage = image else { return }
guard let getCell = tableView?.cellForRow(at: getIndexPath) as? ArtistTableViewCell else { return }
getCell.setArtistImage(getImage)
}
}
}
It’s not necessary to capture tableView explicitly because it’s provided as local variable in the first parameter of the willDisplay method.
Therefore it will not cause a memory leak.
There is a simple rule: Don’t capture anything which is locally accessible inside the method.
Feel free to prove it with Instruments.
Locale variables are not captured by closure as they are within the same scope, so you don't need to make tableview as weak reference.
weak is preferred. If you retain the tableView and dismiss the view controller while it downloads an image the table view object (and its cells) won't be deallocated until the call download finishes. (however no retain cycle will occur)

First TableView cells are not dequeuing properly

I'm working on the app, which loads flags to a class using https://www.countryflags.io/ API. I am loading a flag when initializing the object using Alamofire get request.
The problem is that the first few TableView cells that are dequeued when starting the app are loaded without flags.
But when I scroll back after scrolling down, they load perfectly.
I thought that it is happening because the request is not processed quickly enough and the first flags are not ready to load before the start of dequeuing cells. But I have no idea how to setup something inside the getFlag() method to help me reload TableView data upon completion or delay dequeuing to the point when all flags are loaded.
Country class with getflag() method
import UIKit
import Alamofire
final class Country {
let name: String
let code: String
var flag: UIImage?
var info: String?
init(name: String, code: String, flag: UIImage? = nil, info: String? = nil) {
self.name = name
self.code = code
if flag == nil {
getFlag()
} else {
self.flag = flag
}
self.info = info
}
func getFlag() {
let countryFlagsURL = "https://www.countryflags.io/\(code.lowercased())/shiny/64.png"
Alamofire.request(countryFlagsURL).responseData { response in
if response.result.isSuccess {
if let data = response.data {
self.flag = UIImage(data: data)
}
}
}
}
}
cellForRowAt method
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let country = countries[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "Country", for: indexPath)
cell.textLabel?.text = country.name
if let flag = country.flag {
cell.imageView?.image = flag
} else {
cell.imageView?.image = .none
}
return cell
}
The init method of Country should not be initiating the asynchronous image retrieval. Instead, you should have the cellForRowAt initiate the asynchronous image retrieval.
But you shouldn’t just blithely update the cell asynchronously, either, (because the row may have been reused by the time your Alamofire request is done). And, a more subtle point, you’ll want to avoid having image requests getting backlogged if you scroll quickly to the end of the tableview, so you want to cancel pending requests for rows that are no longer visible. There are a number of ways of accomplishing all three of these goals (async image retrieval in cellForRowAt, don’t update cell after it has been used for another row, and don’t let it get backlogged if scrolling quickly).
The easiest approach is to use AlamofireImage. Then all of this complexity of handling asynchronous image requests, canceling requests for reused cells, etc., is reduced to something like:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: “Country", for: indexPath)
cell.imageView.af_setImage(withURL: objects[indexPath.row].url, placeholderImage: placeholder)
return cell
}
Note, I’d suggest if you’re going to use the default table view cell, that you supply a placeholder image in this routine, like shown above. Just create a blank image (or whatever) that is the same size as your flags. This ensures that the cell will be properly laid out.
By the way, if you’d like to generate that placeholder image programmatically, you can do something like:
let imageSize = CGSize(width: 44, height: 44)
lazy var placeholder: UIImage = UIGraphicsImageRenderer(size: imageSize).image { _ in
UIColor.blue.setFill()
UIBezierPath(rect: CGRect(origin: .zero, size: imageSize)).fill()
}
Now, that creates a blue placeholder thumbnail that is 44×44, but you can tweak colors and size as suits your application.

Assign callback in a correct way

I am confused between 2 methods to get callback in my one class from another class.
This is my scenario :
class TableCell: UITableViewCell {
var callBack:(()->())?
}
I want to use this callback in my controller class. I know these 2 ways :
Method 1:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Identifier", for: indexPath) as! CustomCell
cell.callBack = {[weak self] () in
}
return cell
}
Method 2:
func callBackFunction() {
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Identifier", for: indexPath) as! CustomCell
cell.callBack = callBackFunction
return cell
}
In first method the reference is weak, is it the same for method 2 ? which one is a better approach ? Pleas add proper explanation too.
Before directly choosing one of the mentioned options, we should recognize what is the [weak self] part is. The [weak self] called closure capture list; What's the reason of it?! Well, keep in mind that closures in Swift are reference types, whenever you assign a function or a closure to a constant or a variable, you are actually setting that constant or variable to be a reference to the function or closure. Which means that at some point, if you misusing closures in your code, it could leads to retains cycles.
Citing from The Swift Programming Language - Closures:
If you assign a closure to a property of a class instance, and the
closure captures that instance by referring to the instance or its
members, you will create a strong reference cycle between the closure
and the instance. Swift uses capture lists to break these strong
reference cycles.
Which means that you have to follow the first approach if you are aiming to use self in the body of the closure. Using the weak item self in the capture list resolves (prevents) retains cycles, and that's why you would go with the first method.
For more about how it is done, I would highly recommend to check: Automatic Reference Counting, Resolving Strong Reference Cycles for Closures section.

Swift if statement in CellForRowAtIndexPath

I have a tableView where I want to display different Cells depending on what a variable seguedDisplayMonth is set to. Is this possible and if so can I get any hint on how to do this? I've tried the following but it doesn't seem to work.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Income Cell", forIndexPath: indexPath)
let income = myIncomeArray[indexPath.row]
if seguedDisplayMonth == "All" {
var text = "\(income.money) kr"
cell.textLabel?.text = text
cell.detailTextLabel?.text = income.name
}
return cell
}
I also thought that maybe I need to reload the data after changing the seguedDisplayMonth which gets changed from a different tableView and through a segue.
Call mcTableSwag.reloadData() once seguedDisplayMonth is changed. (Likely call it in the function that actually changes seguedDisplayMonth.
Alternatively you could reload certian cells with some method like reloadVisibleCellsAtIndexPath(...) (Im not sure what it is called exactly, but it should be on the Apple UITableView documentation.
I managed to fix it finally. I will explain how I did it incase anyone runs into the same problem.
I implemented another array myVisibleIncomeArray.
In viewDidLoad() I called a function which does the following:
for inc in myIncomeArray {
if self.monthLabel.text == "All" {
self.myVisibleIncomeArray.append(inc)
totalSum += inc.money
print("Added to myVisible")
}
}
Then I reloadData() and use myVisibleIncomeArray for the other functions.
Not sure if it was the smartest fix, but it's a fix nonetheless.

Resources