0

in the past couple of days I have been optimizing my app and found some memory leaks. I have implemented a custom UIButton that has a public event that I subscribe to. On one UITableViewController the button is hosted in a UITableCellView and I am subscribing to this event:

Cell:

class SelectionButtonCell : UITableViewCell
{
    @IBOutlet weak var selectionButton: SelectionButton!
    @IBOutlet weak var label: UILabel!
}

UITableViewController:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
    let selectionButtonCell = tableView.dequeueReusableCell(withIdentifier: "selectionButtonCell") as! SelectionButtonCell
    selectionButtonCell.selectionButton.clicked = // This leaks memory because nowhere in my code this is set to nil
    {
        // Some unimportant stuff.
    }

    return selectionButtonCell
}

Button:

public class SelectionButton : UIButton
{
    // MARK: - Events
    public var clicked: (() -> ())?

    public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
    {
        super.touchesEnded(touches, with: event)

        self.clicked?()
    }
}

How do I set this var back to nil? I did not find any function that will be called when navigating back (the UITableViewController is pushed onto a UINavigationController). Is it okay to simply iterate over all cells and set this back to nil in viewWillDisappear(_) or is there a nicer way?

I tried tableView:didEndDisplayingCell:forRowAtIndexPath but it seems that this is only being called when scrolling the UITableView.

Jawad Ali
  • 13,556
  • 3
  • 32
  • 49
inexcitus
  • 2,471
  • 2
  • 26
  • 41
  • 1
    Can you show what's the definition for `clicked`? – Sohil R. Memon Jan 16 '20 at 04:39
  • Sure, I have edited the post and posted the definition of the event. I have stripped all other code for better readability. – inexcitus Jan 16 '20 at 04:42
  • 1
    Thanks. Just need to know why you haven't gone for `UIButton.addTarget` which would be safer option as the `IBOutlet`'s get automatically dealloc once the `UIViewController` is dismissed or pop. – Sohil R. Memon Jan 16 '20 at 04:44
  • How could I distinguish which cell was clicked then? The `sender` will give me the instance but not the index of the cell. Is it possible to use addTarget with a block or do I have to use a selector (probably the latter)? – inexcitus Jan 16 '20 at 04:48
  • 1
    There are various ways to do such task. Check this out already answered question: https://stackoverflow.com/questions/20655060/get-button-click-inside-uitableviewcell – Sohil R. Memon Jan 16 '20 at 04:52
  • 1
    Let me if you still need help. – Sohil R. Memon Jan 16 '20 at 04:52
  • Thank you, feel free to add your comments as an answer so I can accept them. – inexcitus Jan 16 '20 at 04:54
  • 1
    Thanks but SO doesn't accepts only link neither the question which already has the answer :) So, let me know if you have any issues I will surely help. Happy Coding :D – Sohil R. Memon Jan 16 '20 at 04:55

2 Answers2

1
 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
    {
        let selectionButtonCell = tableView.dequeueReusableCell(withIdentifier: "selectionButtonCell") as! SelectionButtonCell
        selectionButtonCell.selectionButton.clicked = 
        { [weak self] in
            // Some unimportant stuff.
        }

        return selectionButtonCell
    }

Also there are number of ways Get button click inside UITableViewCell you are using closure

Jawad Ali
  • 13,556
  • 3
  • 32
  • 49
  • Thanks, I used this solution because it works (deist is now called) and it is by far the easiest solution. – inexcitus Jan 16 '20 at 16:01
1

You don't need the subclass of UIButton. And you don't need the IBOutlet

In SelectionButtonCell declare an IBAction and a callback. Connect the button to the action. In the callback pass the cell.

class SelectionButtonCell : UITableViewCell
{
    @IBOutlet weak var label: UILabel!

    var callback : ((UITableViewCell) -> Void)?

    @IBAction func buttonClicked(_ sender : UIButton)
    {
        callback?(self)
    }
}

In celForRow get the current indexPath from the cell

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
    let selectionButtonCell = tableView.dequeueReusableCell(withIdentifier: "selectionButtonCell") as! SelectionButtonCell
    selectionButtonCell.callback = { buttonCell in
        let actualIndexPath = tableView.indexPath(for: buttonCell)!
        ...
    }

    return selectionButtonCell
}

If cells in the tableview are not inserted, deleted or moved you don't even need the cell parameter in the callback closure. The captured indexPath parameter is still valid.

vadian
  • 274,689
  • 30
  • 353
  • 361