Giải quyết vấn đề trùng lặp / lặp lại cells trong Table view

Bạn làm theo một hướng dẫn trên mạng để tạo một tableview đơn giản – hiển thị 1 chút text và images trên cells, dường như nó hoạt động khá là ok . Nhưng khoan, khi bạn cuộn xuống dưới, hình như có gì đó không ổn.

Các images trên cells đang bị lặp lại theo 1 cách nào đó thật huyền bí mặc dù bạn không hề cài đặt cho chúng😱

Ví dụ trên cho thấy một tableview với danh sách công việc cần làm. Khi bạn chạm vào 1 ô, điều đó có nghĩa là công việc đã được thực hiện và dấu tích sẽ được hiển thị. Nhưng khi bạn cuộn sau khi chạm vào một vài ô trên cùng, các ô bên dưới cũng được đánh dấu là đã hoàn thành mặc dù bạn không chạm vào chúng trước đó!

Lý do tại sao điều này xảy ra? Hãy nhớ phương thức dequeueReabilitiesCell (withIdentifier: for 🙂 mà bạn đã sử dụng bên trong phương thức cellForRowAtIndexPath?

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  
    let cell = tableView.dequeueReusableCell(withIdentifier: todoCellIdentifier, for: indexPath) as! TodoTableViewCell
    
    // assign text to label and stuff
    cell.taskLabel.text = "Task \(indexPath.row)"
    return cell
}

Dequeue có nghĩa là có một hàng đợi được sử dụng để lưu trữ các ô xem bảng. Một hàng đợi có nghĩa là gì? 🤔

Khi bạn đăng ký một ô để xem bảng như thế này:

self.tableView.register(UINib(nibName: String(describing: TodoTableViewCell.self), bundle: nil), forCellReuseIdentifier: "todoCellIdentifier")

Điều này sẽ tạo ra một hàng đợi có tên todoCellIdentifier, hàng đợi này được sử dụng để chứa nhiều ô của lớp TodoTableViewCell.

Dưới đây là một minh họa cho thấy cách hàng đợi tái sử dụng hoạt động:

iOS sử dụng cơ chế xếp hàng di động có thể sử dụng lại để tối ưu hóa việc sử dụng bộ nhớ. Giả sử bảng xem của bạn có 1000 ô, thay vì đặt tất cả 1000 ô vào bộ nhớ, hàng đợi được sử dụng để lưu trữ vừa đủ các ô để hiển thị trên màn hình (cộng thêm một vài ô làm bộ đệm), khi một ô mới sắp xuất hiện trên màn hình (do cuộn), một ô sẽ được đưa ra khỏi hàng đợi, được tùy chỉnh trong phương thức cellForRowAtIndexPath, sau đó được chèn vào tableview.

Đối với tableview bên dưới, chỉ có ~ 15 ô hiển thị trên màn hình tại bất kỳ thời điểm nào, do đó, hàng đợi sẽ giữ khoảng ~ 18 ô trong bộ nhớ. (Đây là rất ít hơn 1000!)

Khi người dùng cuộn xuống, ô trên cùng được đẩy ra ngoài màn hình sẽ được đặt vào đầu hàng đợi và ô dưới cùng trong hàng đợi được đưa ra ngoài và được đặt ở dưới cùng của tableview trong màn hình.

Đây là cách quá trình cell hoạt động:

Lưu ý rằng chúng ta đã không thiết lập lại khả năng hiển thị của dấu tick trong phương thức cellForRowAt và ô 18 thực sự là ô 2 được sử dụng lại. Khi chúng ta gõ vào ô 2 trước đó (hiển thị dấu tick của nó), thuộc tính này được chuyển xuống ô 18!

Để giải quyết vấn đề này, chúng ta có thể đặt lại dấu tick để làm cho nó vô hình trong cellForRowAt:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: todoCellIdentifier, for: indexPath) as! TodoTableViewCell
        
    // reset (hide) the checkmark label
    cell.checkLabel.isHidden = true
    
    // get the Task object in the taskArray (data source)
    let task = self.taskArray[indexPath.row]
    
    // if the task has marked done, show the checkmark label
    if(task.done){
      cell.checkLabel.isHidden = false
    }
  
    cell.taskLabel.text = "Task \(indexPath.row)"
    return cell
}

Để làm cho phương thức cellForRowAt trông clean hơn, chúng ta có thể di chuyển code đặt lại sang phương thức prepareForReuse trong lớp cell tùy chỉnh như sau:

//  TodoTableViewCell.swift
class TodoTableViewCell: UITableViewCell {
    override func prepareForReuse() {
        // invoke superclass implementation
        super.prepareForReuse()
        
        // reset (hide) the checkmark label
        self.checkLabel.isHidden = true

    }
}

Và trong phương thức cellForRowAt trong view controller:

// ViewController.swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: todoCellIdentifier, for: indexPath) as! TodoTableViewCell
    
    // get the Task object in the taskArray (data source)
    let task = self.taskArray[indexPath.row]
    
    // if the task has marked done, show the checkmark label
    if(task.done){
      cell.checkLabel.isHidden = false
    }
  
    cell.taskLabel.text = "Task \(indexPath.row)"
    return cell
}

Lưu ý: Tài liệu của Apple khuyên bạn không nên đặt lại thuộc tính liên quan đến nội dung như label.text trong readyForReuse vì lý do hiệu suất.

For performance reasons, you should only reset attributes of the cell that are not related to content, for example, alpha, editing, and selection state. The table view’ s delegate in tableView(_:cellForRowAt:) should always reset all content when reusing a cell.

Hầu hết các sự cố trùng lặp / lặp lại xảy ra do chúng ta không đặt lại các thành phần UI của cell về trạng thái ẩn / trống trước khi sử dụng lại trong tableview. Điều này có thể được giải quyết bằng cách đặt lại dữ liệu các thành phần UI trong hàm prepareForReuse trong lớp cell tùy chỉnh.

*Bài viết được dịch từ nguồn: fluffy.es