Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

UITableViewDiffableDataSource are not deinit

I want to use a Combine in my project and face the problem.
Here is the code of the ViewController

import Combine
import UIKit

class ProfileDetailsController: ViewController {
    //

    // MARK: - Views
    @IBOutlet private var tableView: UITableView!

    // MARK: - Properties
    private typealias DataSource = UITableViewDiffableDataSource<ProfileDetailsSection, ProfileDetailsRow>
    private typealias Snapshot = NSDiffableDataSourceSnapshot<ProfileDetailsSection, ProfileDetailsRow>

    @Published private var data: [ProfileDetailsSectionModel] = {
        return ProfileDetailsSection.allCases.map { ProfileDetailsSectionModel(section: $0, data: $0.rows) }
    }()
    
    private lazy var dataSource: DataSource = {
        let dataSource = DataSource(tableView: tableView) { tableView, _, model in
            let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableCell.name) as! TextFieldTableCell
            cell.delegate = self
            cell.setData(model: model)
            return cell
        }
        dataSource.defaultRowAnimation = .fade
        return dataSource
    }()
}

// MARK: - Setup binding
extension ProfileDetailsController {
    override func setupBinding() {
        tableView.registerCellXib(cell: TextFieldTableCell.self)
        $data.receive(on: RunLoop.main).sink { [weak self] models in
            let sections = models.map { $0.section }
            var snapshot = Snapshot()
            snapshot.appendSections(sections)
            models.forEach { snapshot.appendItems($0.data, toSection: $0.section) }
            self?.dataSource.apply(snapshot, animatingDifferences: true)
        }.store(in: &cancellable)
    }
}

// MARK: - Cell delegates
extension ProfileDetailsController: TextFieldTableCellDelegate {
    func switcherAction() { }
}

And here is the code of the cell.

import UIKit

protocol TextFieldTableCellData {
    var placeholder: String? { get }
}

protocol TextFieldTableCellDelegate: NSObjectProtocol {
    func switcherAction()
}

class TextFieldTableCell: TableViewCell {
    //

    // MARK: - Views
    @IBOutlet private var textField: ZWTextField!

    // MARK: - Properties
    public weak var delegate: TextFieldTableCellDelegate?

    override class var height: CGFloat {
        return 72
    }
}

// MARK: - Public method
extension TextFieldTableCell {
    func setData(model: TextFieldTableCellData) {
        textField.placeholder = model.placeholder
    }
}

ViewController’s deinit was not called.
But when I use this code for ViewController

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

import UIKit

class ProfileDetailsController: ViewController {
    //

    // MARK: - Views
    @IBOutlet private var tableView: UITableView!

    // MARK: - Properties
    @Published private var data: [ProfileDetailsSectionModel] = {
        return ProfileDetailsSection.allCases.map { ProfileDetailsSectionModel(section: $0, data: $0.rows) }
    }()
}

// MARK: - Startup
extension ProfileDetailsController {
    override func startup() {
        tableView.dataSource = self
        tableView.registerCellXib(cell: TextFieldTableCell.self)
    }
}

// MARK: - Startup
extension ProfileDetailsController: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return data.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return data[section].data.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let model = data[indexPath.section].data[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableCell.name) as! TextFieldTableCell
        cell.delegate = self
        cell.setData(model: model)
        return cell
    }
}

// MARK: - Cell delegates
extension ProfileDetailsController: TextFieldTableCellDelegate {
    func switcherAction() {}
}

Everything is fine. deinit called. I tried to set dataSource optional and set it nil on deinit, the same result. With Combine deinit called only when I comment this line:

cell.delegate = self

Does anyone know what’s the matter?
Xcode 13.2 iOS 15.2

>Solution :

The Combine stuff is a total red herring. That’s why you can’t locate the problem; you’re looking in the wrong place. The issue is the difference between an old-fashioned data source and a diffable data source. The problem is here:

private lazy var dataSource: DataSource = { // *
    let dataSource = DataSource(tableView: tableView) { tableView, _, model in
        let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableCell.name) as! TextFieldTableCell
        cell.delegate = self // *

I’ve starred the problematic lines:

  • On the one hand, you (self, the view controller) are retaining the dataSource.

  • On the other hand, you are giving the data source a cell provider function in which you speak of self.

That’s a retain cycle! You need to break that cycle. Change

let dataSource = DataSource(tableView: tableView) { tableView, _, model in

To

let dataSource = DataSource(tableView: tableView) { [weak self] tableView, _, model in

(That will compile, because although self is now an Optional, so is cell.delegate.)

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading