Bigair
Bigair

Reputation: 1592

How to store cell's selection, need best practice

Situation

There is a table view shows list of articles: Article. An article is a struct object and is list of articles are initialized form api response.

We needs to add a feature that when a user taps a cell, it changes to selected state. To indicate the selection, I add a check mark to a selected item cell.

cell.accessoryType = .checkmark

Problem

Problem

Here comes a problem. Since cells are recycled, each time a selected article cell is dequed, we need to store which cell is selected.

struct Article is used within entire app so we shouldn’t just add isSelected field to it.

struct Article {
    let title: String
    var isSelected: Bool // <- Not good.  Article used over the app.
}

Temorary solution

Some solution I use is to make bool array to store which row is selected.

var selectedRow: Bool = {
    return articles.map { return false }
}()

But this approach seems odd and is also vulnerable to cell insettion and deletion.

Question

Is there a better solution?


The odd way solution code

import UIKit

struct Article {
    let title: String
}

class SampleTableViewController: UITableViewController {

    // MARK: - Properties
    
    let articles: [Article] = {
        var tempArticles: [Article] = []
        for i in 0...30 {
            tempArticles.append(Article(title: "Article at index:\(i)"))
        }
        return tempArticles
    }()
    
    lazy var selectedRow: [Bool] = {
        return articles.map { _ in false }
    }()
  
    let cellId = "cell"
    
    
    
    // MARK: - Lifecycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellId)
    }


    

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

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

    
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
        cell.textLabel?.text = articles[indexPath.row].title
        
        // Set selected state to a cell
        cell.accessoryType = selectedRow[indexPath.row] ? .checkmark : .none
        return cell
    }
    
    
    // MARK: - Table view delegate
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // Update selection
        // This isn't the best way to update selection but just to make the example simple.
        selectedRow[indexPath.row] = !selectedRow[indexPath.row]
        tableView.reloadData()
    }
}

enter image description here

Upvotes: 1

Views: 332

Answers (3)

Bigair
Bigair

Reputation: 1592

I enhanced Dorian Roy's answer by using generic type.

In this way we only need to declare it once, not for every type of models

struct SelectableListItem<T> {
    let item: T
    var isSelected: Bool
}


Table View Implementation

    var listItems: [SelectableListItem<Article>] = []

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)

        let listItem = listItems[indexPath.row]
        cell.textLabel?.text = listItem.item.title
        cell.accessoryType = listItem.isSelected ? .checkmark : .none

        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // Update data
        listItems[indexPath.row].isSelected = !listItems[indexPath.row].isSelected

        // Update selection
        let cell = tableView.cellForRow(at: indexPath)
        cell?.accessoryType = listItems[indexPath.row].isSelected ? .checkmark : .none
    }

Upvotes: 0

Paulw11
Paulw11

Reputation: 114773

I would use a Set<Article> to track your selected articles. In order to do this, your Article struct needs to be Hashable. As long as all of the properties of your struct are Hashable then all you need to do is add conformance to the Hashable protocol.

struct Article: Hashable {
    let title: String
}


var selectedArticles = Set<Article>()

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
    let article = articles[indexPath.row]
    cell.textLabel?.text = article.title

    // Set selected state to a cell
    cell.accessoryType = selectedArticles.contains(article) ? .checkmark : .none
    return cell
}


// MARK: - Tabkle view delegate

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let article = article[indexPath.row]
    if selectedArticles.contains[article] {
        selectedArticles.remove(article)
    } else {
        selectedArticles.insert(article)
    }
    tableView.reloadRows(at: [indexPath], with: .none)
}

Upvotes: 2

Dorian Roy
Dorian Roy

Reputation: 3104

One way to solve this: Give the table view it's own model that saves the state of the list including the selected state.

class ListItem {
    let article: Article
    var isSelected: Bool
}

Then build your table view from an array of these items:

var listItems = [ListItem]()

Populate that array with ListItems holding your articles and use it as the data source.

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

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
    let item = listItems[indexPath.row]
    cell.textLabel?.text = item.article.title 
    cell.accessoryType = item.isSelected ? .checkmark : .none
    return cell
}

And finally in didSelectRowAtIndexPath you just have to set the new isSelected value of the selected list item.

Upvotes: 3

Related Questions