iosswiftclosuresretain-cycle

Cell isn't being deallocated after table view is dismissed, being referenced by closure's context


I'm creating a custom table view cell, which allows the user to add, take, or view uploaded photos.

I discovered that this cell stays in memory forever even after the table view's dismissal, creating weird memory graph. I want the cell to be dismissed properly, but I have a hard time understanding what is going on.

The graph shows that my cell is being strongly referenced by a addPhotoTapAction.context.

addPhotoTapAction: ((ItemInfoCell) -> Void)? is the cell's class variable, used to store the closure handling user input. The closure is defined in the view controller:

let infocell = tableView.dequeueReusableCell(withIdentifier: K.infocellID) as! ItemInfoCell
    if item?.imageUrl == nil {
        self.imageManager.actionController?.actions[2].isEnabled = false
    } else {
        self.imageManager.actionController?.actions[2].isEnabled = true
    }
    infocell.addPhotoTapAction = { [unowned self] _ in
        infocell.addPhotoButton.isEnabled = false
        self.imageManager.pickImage(self) { [weak self] image in
            self?.imageToSave = image
            infocell.itemPhoto.image = self?.imageToSave
            infocell.addPhotoButton.tintColor = UIColor(ciColor: .clear)
            infocell.addPhotoButton.isEnabled = true
            self?.imageManager.actionController?.actions[2].isEnabled = true
        }

The pickImage method is shown below. It's used to present action controller with image picker options (take photo or choose from lib):

func pickImage(_ viewController: UIViewController, _ callback: @escaping ((UIImage) -> ())) {
    picker.delegate = self
    picker.mediaTypes = ["public.image"]
    picker.allowsEditing = true
    pickImageCallback = callback
    self.viewController = viewController
    actionController!.popoverPresentationController?.sourceView = viewController.view
    viewController.present(actionController!, animated: true, completion: nil)
}

...and the callback is stored to be used in picker's didFinishPickingMediaWithInfo call:

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
    picker.dismiss(animated: true, completion: nil)
    if let image = info[.editedImage] as? UIImage {
        let squareImage = makeSquare(image)
        pickImageCallback?(squareImage)
    } else if let image = info[.originalImage] as? UIImage {
        let squareImage = makeSquare(image)
        pickImageCallback?(squareImage)
    }
    viewController = nil
}

I've tried to manually set variable with closure to nil, then switched from [weak self] to [unowned self] to the combination of both. No luck.

I think that either the pickImage(self), or using the class' properties within the closure are creating a strong reference even when using [weak/unowned] capture lists, but I'm still not sure and not being able to fix it.

Update: ItemInfoCell class' code

class ItemInfoCell: UITableViewCell {

@IBOutlet weak var itemPhoto: UIImageView!
@IBOutlet weak var itemLabel: UILabel!
@IBOutlet weak var addPhotoButton: UIButton!

var addPhotoTapAction: ((ItemInfoCell) -> Void)?

override func awakeFromNib() {
    super.awakeFromNib()
}

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

@IBAction func takePhoto(_ sender: Any) {
    if let addPhoto = self.addPhotoTapAction {
        addPhoto(self)
    }
}

}


Solution

  • The problem is that you access the infocell inside it's callback. If you use variable inside own callback, you should mark it as a weak by adding it to the capture list.

    In your code it should be like this:

       infocell.addPhotoTapAction = { [unowned self, weak infocell] _ in
          ...
        }