Assign callback in a correct way - ios

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.

Related

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)

Unable to set a variable in UITableViewCell from parent ViewController

I have a UITableView and a custom TableViewCell class, I am setting a non-null value in viewDidLoad method, but while setting the value to UITableViewCell in cellForRow method , the value magically becomes nil. I am unable to access the set variable from my custom UITableViewCell
Here is the snippet of code. Print statement in ViewDidLoad prints the value while the one in cellForRowAtIndexPath returns nil
override func viewDidLoad() {
super.viewDidLoad()
Docco360Util.shared().getDoctorsWithResultBlock { (doctorObjects, error) in
if let err = error{
print(err)
}else{
self.doctors=doctorObjects
print(doctorObjects![1].professionalHeader)//output is printed here
self.doctorTableView.reloadData()
}
}
// Do any additional setup after loading the view.
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifierForUsers = "DoctorTableViewCell"
var cell = tableView.dequeueReusableCell(withIdentifier: identifierForUsers, for: indexPath) as! DoctorTableViewCell
if cell == nil{
cell=DoctorTableViewCell(style: .default, reuseIdentifier: identifierForUsers)
}
print(self.doctors[indexPath.row].professionalHeader)//output in nil here
cell.doctor=self.doctors[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "DoctorTableViewCell", for: indexPath) as! DoctorTableViewCell
print(self.doctors[indexPath.row].professionalHeader)
cell.doctor = self.doctors[indexPath.row]
return cell
}
First, you should ensure that the closure of Docco360Util.shared().getDoctorsWithResultBlock is executed in the main (UI) thread. If not, you should put it into a DispatchQueue.main.async block.
Then, you should verify that the result block is executed (e.g. the print statement prints something). Btw, why do you call print(doctorObjects![1].professionalHeader) with index 1 and not 0? In tableView(tableView:cellForRowAt:) you element with index 0 - maybe this doctor is nil?
You could start by setting a breakpoint to your property declaration, a watchpoint, or implement a property observer (willSet) and add a breakpoint her, to check if somebody is messing up with your variables.
If all this does not help, you might want to post more code, expecially all lines of code where you write your self.doctors array.
I could finally fix it. The problem was in declaration of variables. I was using Objective-C header file for declarations. While declaring I declared them as "weak" instead of "retain" and that was causing the problem.

Delegate Method to UItableViewCell Swift

I have a Social Network Feed in form UItableView which has a cell. Now each cell has an image that animates when an even is triggered. Now, This event is in form of a string, will be triggered at every cell. the options for the event are defined in another class(of type NSObject).
My issue:
I constructed a protocol delegate method in table view, which will be called whenever the event is triggered for each cell. Then, I define this function in UITableViewCell Class, since my the image will be animating on that.
All is working well but I am unable to figure out how to assign the delegate of TableView class to cell class. What I mean is, how can I use UITableView.delegate = self in cellView class. I have tried using a static variable, but it doesn't work.
I have been playing around the protocols for a while now but really unable to figure out a solution to this.
I hope I am clear. If not, I will provide with an example in the comments. I am sorry, This is a confidential project and I cant reveal all details.
If I understand you correctly, you are trying to make each of your cells conform to a protocol that belongs to their UITableView? If this is the case then this cannot be done. The Delegation design pattern is a one to one relationship, i.e only one of your UITableViewCells would be able to conform to the UITableView's delegate.
Delegation is a simple and powerful pattern in which one object in a program acts on behalf of, or in coordination with, another object. The delegating object keeps a reference to the other object—the delegate—and at the appropriate time sends a message to it. The message informs the delegate of an event that the delegating object is about to handle or has just handled. The delegate may respond to the message by updating the appearance or state of itself or other objects in the application, and in some cases it can return a value that affects how an impending event is handled. The main value of delegation is that it allows you to easily customize the behavior of several objects in one central object.
Quote from the Apple Docs
I would suggest that your UITableViewCell should call a block (Objective-C) or a closure (Swift) whenever your specified event is triggered to achieve what you are looking for. Set up this closure in your tableView:cellForRowAtIndexPath function.
EXAMPLE
TableViewController
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCellID", for: indexPath) as! MyTableViewCell
cell.eventClosure = {
//Do something once the event has been triggered.
}
return cell
}
TableViewCell
func eventTriggered()
{
//Call the closure now we have a triggered event.
eventClosure()
}
If I correctly understood your question, maybe this could help:
class ViewController: UIViewController, YourCustomTableDelegate {
#IBOutlet weak var tableView: YourCustomTableView!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.customTableDelegate = self
}
// table delegate method
func shouldAnimateCell(at indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
cell.animate(...)
}
}
}
Try something like this:
Define your delegate protocol:
protocol CustomCellDelegate: class {
func animationStarted()
func animationFinished()
}
Define your CustomCell. Extremely important to define a weak delegate reference, so your classes won't retain each other.
class CustomCell: UITableViewCell {
// Don't unwrap in case the cell is enqueued!
weak var delegate: CustomCellDelegate?
/* Some initialization of the cell */
func performAnimation() {
delegate?.animationStarted()
UIView.animate(withDuration: 0.5, animations: {
/* Do some cool animation */
}) { finished in
self.delegate?.animationFinished()
}
}
}
Define your view controller. assign delegate inside tableView:cellForRowAt.
class ViewController: UITableViewDelegate, UITableViewDataSource {
/* Some view controller customization */
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: CustomCell.self)) as? CustomCell
cell.delegate = self
cell.performAnimation()
return cell
}
}

Return The Value of UITableView

I have UITableView and the contect is Dynamic Prototype.
so I have 5 cells and each cell has it own identifier.
so when I try to return the value in (cellForRowAt)
it would let me.
would please help with that ?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (indexPath.section) == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as UITableViewCell!
// cell?.textLabel!.text = objectsArray[indexPath.section].sectionObjects[indexPath.row]
return cell!
}
else if (indexPath.section) == 2 {
let cell3 = tableView.dequeueReusableCell(withIdentifier: "cellThree") as UITableViewCell!
return cell3!
}
else if (indexPath.section) == 3 {
let cell4 = tableView.dequeueReusableCell(withIdentifier: "cellFour") as UITableViewCell!
return cell4!
}
else if (indexPath.section) == 4 {
let cell5 = tableView.dequeueReusableCell(withIdentifier: "cellFive") as UITableViewCell!
return cell5!
}
return cell!
}
Thanks !
New Update :-
so the error that is showing to me is (Use of unresolved identifier 'cell')
so when I return at the end (return cell!) it shows this error.However, if I deleted that line it shows me another error asking me to return a value
so bottomline I'm confised what value should I return at the end of (cellForRowAt).
Return values
This method can only return one cell each time it's called. The method will be called each time a new cell is about to appear on the screen. Take a look at the method signature:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
That UITableViewCell at the end is the expected return value and in this case it's a single cell.
The error
Let's take a look at your error:
Use of unresolved identifier 'cell'
"Unresolved identifier" means that it has no idea what "cell" is as it has not been declared in the current scope. But don't you declare cell in this method? Yes you do, but they are in a separate scope. Variable scope defines how long a variable lives and where it can be seen. You can tell when a new variable scope is declared when you see a new set of curly brackets {}, which each of your if statements declare. Variables declared within each set of curly brackets can only be seen by code contained within those brackets. Once execution leaves those brackets, those variables are gone. Let's take a look at what variables are viewable in the scope of your final return statement by removing the scopes created by your if statements:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return cell!
}
Now it is clear that cell doesn't exist at the point of the return. You'll need to declare cell in the method's scope and give it some value. Please note that force unwrapping your cell optionals will likely lead to a crash at runtime as the method must not return nil. You'll want to create cells when tableView.dequeueReusableCell() returns nil.
EDIT: I just realized you aren't using the for indexPath parameter in dequeueReusableCellWithIdentifier. Use that, otherwise it's the wrong one. See:
https://developer.apple.com/documentation/uikit/uitableview/1614878-dequeuereusablecellwithidentifie
https://www.natashatherobot.com/ios-using-the-wrong-dequeuereusablecellwithidentifier/
#Nawaf you can't return multiple values from any function, hence you can't return multiple cells from one call of cellForRow. You may be misunderstanding how cellForRowAtIndexPath works. Right now your code is not populating anything in the cells.
Use of unresolved identifier 'cell'
Furthermore, your error can be occurring for multiple reasons. For one, check that your view controller is correctly assigned to the one in storyboard and that you have assigned a reuseIdentifier in storyboard.
See the links for more info:
https://developer.apple.com/documentation/uikit/uitableviewdatasource/1614861-tableview?language=objc
How does cellForRowAtIndexPath work?
Also a side note for code quality: don't force cast to the cell because that can cause unintended errors. Use conditional binding instead:
guard let cell = tableView.dequeueReusableCellWithIdentifier(...) as? UITableViewCell else {
// What to do when the cast failed
}
Good luck :)

Lazy image downloading on UITableViewCell using defer

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
}

Resources