UITableView自定义单元在滚动上复制UIButton

时间:2019-01-03 17:19:56

标签: ios swift uitableview

我有一个UITableViewController,其中包含许多自定义的UITableViewCells类型。

这些类型之一就是一个包含UIStackView的单元格,该单元格本身包含一个或多个UIButton

在屏幕上滚动并重新打开时,将再次添加按钮。每次滚动事件都会发生这种情况。

Pre Scroll Image Post Scroll Image

我了解到,当单元被重用以提高性能时,可能发生的是我在cellForRowAt中配置单元的设置代码再次被执行。

因此,它将数据源中的3个按钮添加到该单元格中,该单元格已经包含上次渲染的按钮。

我不知道如何清除并防止这种行为,并且非常感谢有人在我迷路时提供见解。

我已经能够准备一个小型应用程序来重新创建此应用程序,因为由于它是封闭源,所以我无法共享当前项目。

我为下面的大量代码表示歉意,但这只是简单地放入项目并重新创建的最低要求。

class TableViewController: UITableViewController {

    let textCellId = "textCellId"
    let buttonCellId = "buttonCellId"

    // MARK: - Mock Data Source

    let cellContent = [
        Message(type: .buttonGroup, buttonGroup: [
            MessageButton(label: "Button #1"),
            MessageButton(label: "Button #2"),
            MessageButton(label: "Button #3")
        ]),
        Message(type: .text, text: "A"),
        Message(type: .text, text: "B"),
        Message(type: .text, text: "C"),
        Message(type: .text, text: "D"),
        Message(type: .text, text: "E"),
        Message(type: .text, text: "F"),
        Message(type: .text, text: "G"),
        Message(type: .text, text: "H"),
        Message(type: .text, text: "I"),
        Message(type: .text, text: "J"),
        Message(type: .text, text: "K"),
        Message(type: .text, text: "L"),
        Message(type: .text, text: "M"),
        Message(type: .text, text: "N"),
        Message(type: .text, text: "O"),
        Message(type: .text, text: "P"),
        Message(type: .text, text: "Q"),
        Message(type: .text, text: "R"),
        Message(type: .text, text: "S"),
    ]

    override func viewDidLoad() {
        super.viewDidLoad()

        registerCells()
        configureTableView()
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return cellContent.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let item = cellContent[indexPath.row]
        switch indexPath.row {
        case 0:
            let cell = tableView.dequeueReusableCell(withIdentifier: buttonCellId, for: indexPath) as! ButtonCell
            cell.buttonGroupContent = item.buttonGroup
            return cell
        default:
            let cell = tableView.dequeueReusableCell(withIdentifier: textCellId, for: indexPath) as! TextCell
            cell.textLabel?.text = item.text
            return cell
        }
    }
}

// MARK: - Misc TableView Setup

extension TableViewController {

    fileprivate func registerCells() {
        tableView.register(TextCell.self, forCellReuseIdentifier: textCellId)
        tableView.register(ButtonCell.self, forCellReuseIdentifier: buttonCellId)
    }

    fileprivate func configureTableView() {
        tableView.allowsSelection = false
        tableView.alwaysBounceVertical = false
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 200
        tableView.separatorStyle = .none
        tableView.backgroundColor = UIColor.lightGray
        tableView.contentInset = UIEdgeInsets(top: 24, left: 0, bottom: 50, right: 0)
        tableView.tableFooterView = UIView()
    }
}

// MARK: - Cell Types

class TextCell: UITableViewCell {

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}


class ButtonCell: UITableViewCell {

    var buttonGroupContent: [MessageButton]? {
        didSet {
            anchorSubViews()
        }
    }

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    fileprivate var button: UIButton {
        let button = UIButton(type: .custom)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.backgroundColor = UIColor.darkGray
        button.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
        button.layer.cornerRadius = 5
        button.layer.masksToBounds = true
        return button
    }

    fileprivate let buttonGroupStackView: UIStackView = {
        let stackView = UIStackView()
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .vertical
        stackView.distribution = .fillEqually
        stackView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
        stackView.isLayoutMarginsRelativeArrangement = true
        stackView.spacing = UIStackView.spacingUseSystem
        return stackView
    }()
}

extension ButtonCell {
    fileprivate func anchorSubViews() {
        guard let buttons = buttonGroupContent?.enumerated() else { return }

        for (index, b) in buttons {
            let btn = button
            btn.setTitle(b.label, for: .normal)
            btn.frame = CGRect(x: 0, y: 0, width: 200, height: 40)
            btn.tag = index
            buttonGroupStackView.addArrangedSubview(btn)
        }

        addSubview(buttonGroupStackView)

        NSLayoutConstraint.activate([
            buttonGroupStackView.topAnchor.constraint(equalTo: topAnchor),
            buttonGroupStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
            buttonGroupStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
            buttonGroupStackView.trailingAnchor.constraint(equalTo: trailingAnchor)
            ])
    }
}





// MARK: - Misc for setup

struct MessageButton {
    let label: String
}

enum MessageType {
    case text, buttonGroup
}

struct Message {
    let type: MessageType
    let text: String?
    let buttonGroup: [MessageButton]?

    init(type: MessageType, text: String? = nil, buttonGroup: [MessageButton]? = nil) {
        self.type = type
        self.text = text
        self.buttonGroup = buttonGroup
    }
}

2 个答案:

答案 0 :(得分:1)

由于单元格是可重复使用的,因此内容保持不变。因此,您以前的按钮仍在堆栈视图中,并且每次都添加下一个按钮。

要解决此问题,请在向UIStackView添加新按钮之前,先删除旧按钮

extension ButtonCell {

    fileprivate func anchorSubViews() {
        ...

        for case let button as UIButton in buttonGroupStackView.subviews {
            button.removeFromSuperview()
        }

        for (index, b) in buttons {
            ...
            buttonGroupStackView.addArrangedSubview(btn)
        }    
        ...
    }
}

答案 1 :(得分:0)

buttonbuttonGroupStackView属性设为可选,并将weak设为可选。 addSubview方法的启用将保留对其子视图的强烈引用。因此它永远不会被删除。并覆盖prepareForReuse()进行必要的清理,并确保已从单元格中删除了stackview。这是您的操作方法:

class ButtonCell: UITableViewCell {

    var buttonGroupContent: [MessageButton]? {
        didSet {
            anchorSubViews()
        }
    }
    fileprivate weak var buttonGroupStackView: UIStackView?

    // MARK: - Initialization
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        self.initialSetup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.initialSetup()
    }

    private func initialSetup() -> Void {
        let stackView = UIStackView()
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .vertical
        stackView.distribution = .fillEqually
        stackView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
        stackView.isLayoutMarginsRelativeArrangement = true
        stackView.spacing = UIStackView.spacingUseSystem

        self.addSubview(stackView)
        self.buttonGroupStackView = stackView

        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: topAnchor),
            stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
            stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
            stackView.trailingAnchor.constraint(equalTo: trailingAnchor)
            ])
    }

    // MARK: - Subclass Overrides
    override func prepareForReuse() {
        super.prepareForReuse()

        self.buttonGroupStackView?.subviews.forEach({ $0.removeFromSuperview()} )
    }

    // MARK: - Private
    fileprivate func createButton() -> UIButton {
        let button = UIButton(type: .custom)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.backgroundColor = UIColor.darkGray
        button.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
        button.layer.cornerRadius = 5
        button.layer.masksToBounds = true
        return button
    }

}

extension ButtonCell {
    fileprivate func anchorSubViews() {
        guard let buttons = buttonGroupContent?.enumerated() else { return }

        for (index, b) in buttons {
            let btn = self.createButton()
            btn.setTitle(b.label, for: .normal)
            btn.frame = CGRect(x: 0, y: 0, width: 200, height: 40)
            btn.tag = index
            self.buttonGroupStackView?.addArrangedSubview(btn)
        }
    }
}

除非有其他要求,否则始终建议使用weak引用子视图或IBOutlet属性。