Reputation: 1592
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
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.
}
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.
Is there a better solution?
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()
}
}
Upvotes: 1
Views: 332
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
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
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