Edited at

UITableViewをプログラムから選択する実装について(チェックマークが表示・非表示されない問題)

More than 1 year has passed since last update.


はじめに

こんにちは。

以下のようにUITableViewをコードから選択したくなりました。

※左は単一選択,右は複数選択です。

single.gif multiple.gif

左上のボタン等で選択しているのですが、実装に少し悩んだため書いて見たいと思います。

環境は(Xcode: 9.1, Swift:4.0.2)至らぬ点などコメント頂けたら幸いです。


悩んだ点について

selectRow(at:animated:scrollPosition:)deselectRow(at:animated:)

※ Apple Reference


Calling this method does not cause the delegate to receive a tableView(:willSelectRowAt:) or tableView(:didSelectRowAt:) message, nor does it send UITableViewSelectionDidChange notifications to observers.


以上のようにコードからセルを選択・非選択した場合、delegateメソッドは呼ばれないようで、チェックマークをつけたりなどの処理をしていた場合はうまくUIに反映されません。


解決方法

UITableViewの内部のdelegateメソッドを(ViewControllerを通して)あるタイミングで呼び出してしまうのは少し嫌な感じがするのですが、以下の流れで動かすことができました。


MainViewController.swift

//現在選択されているセルを解除することを通知します

if let selectedIndexPath = self.tableView.indexPathForSelectedRow {
self.tableView(self.tableView, didDeselectRowAt: selectedIndexPath)
}
//セルを選択
self.tableView.selectRow(at: selectIndexPath, animated: false, scrollPosition: .none)
//セルが選択されることを通知します
self.tableView(self.tableView, didSelectRowAt: selectIndexPath)


ソースコード

ページの下に記述したExtensionを使って書いて見ました。合わせて見ていただけたらと思います。


単一選択


MainViewController.swift

import UIKit

class MainViewController: UIViewController {

@IBOutlet weak var tableView: UITableView!

override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "CustomTableViewCell", bundle: nil), forCellReuseIdentifier: "CustomTableViewCell")
tableView.delegate = self
tableView.dataSource = self
}

override var preferredStatusBarStyle: UIStatusBarStyle {
return UIStatusBarStyle.lightContent
}

@IBAction func segmentControlValueChange(_ sender: UISegmentedControl) {
var selectIndexPath = IndexPath(row: 0, section: 0)
switch sender.selectedSegmentIndex {
case 0:
selectIndexPath = IndexPath(row: 0, section: 0)
case 1:
selectIndexPath = IndexPath(row: 2, section: 0)
case 2:
selectIndexPath = IndexPath(row: 5, section: 0)
default: return
}
self.tableView.selectRowWithSendNotification(at: selectIndexPath, animated: false, scrollPosition: .none)
}

@IBAction func deselectButtonAction(_ sender: Any) {
self.tableView.deselectAllRows(animated: false)
}
}

extension MainViewController: UITableViewDataSource {
//テーブルの行数を返却
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
//テーブルの行ごとのセルを返却する
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomTableViewCell", for: indexPath)
cell.selectionStyle = .none
return cell
}
}

extension MainViewController: UITableViewDelegate {
//セルが選択された時
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! CustomTableViewCell
cell.accessoryType = .checkmark
}
//セルが非選択の時
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! CustomTableViewCell
cell.accessoryType = .none
}
}



複数選択


MainViewController.swift

import UIKit

class MainViewController: UIViewController {

@IBOutlet weak var tableView: UITableView!

override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "CustomTableViewCell", bundle: nil), forCellReuseIdentifier: "CustomTableViewCell")
tableView.delegate = self
tableView.dataSource = self
tableView.allowsMultipleSelection = true
}

override var preferredStatusBarStyle: UIStatusBarStyle {
return UIStatusBarStyle.lightContent
}

@IBAction func select0ButtonAction(_ sender: Any) {
self.tableView.selectRowWithSendNotification(at: IndexPath(row: 0, section: 0), animated: false, scrollPosition: .none)
}

@IBAction func select2ButtonAction(_ sender: Any) {
self.tableView.selectRowWithSendNotification(at: IndexPath(row: 2, section: 0), animated: false, scrollPosition: .none)
}

@IBAction func select5ButtonAction(_ sender: Any) {
self.tableView.selectRowWithSendNotification(at: IndexPath(row: 5, section: 0), animated: false, scrollPosition: .none)
}

@IBAction func deselectButtonAction(_ sender: Any) {
self.tableView.deselectAllRows(animated: false)
}
}

extension MainViewController: UITableViewDataSource {
//テーブルの行数を返却
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
//テーブルの行ごとのセルを返却する
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomTableViewCell", for: indexPath)
cell.selectionStyle = .none
return cell
}
}

extension MainViewController: UITableViewDelegate {
//セルが選択された時
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! CustomTableViewCell
cell.accessoryType = .checkmark
}
//セルが非選択の時
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! CustomTableViewCell
cell.accessoryType = .none
}
}



終わりに

delegate通知を送りながらselectRowまたはdeselectRowするExtensionを書いて見たのですが、メソッドの命名に自信がなくて、コメントでいい感じの命名があれば教えていただきたいのです:;(∩´﹏`∩);:


UITableViewExtension.swift

extension UITableView {

/**
行をdelegateに通知しながら選択します
*/

func selectRowWithSendNotification(at: IndexPath?, animated: Bool, scrollPosition: UITableViewScrollPosition) {
if !allowsMultipleSelection {
if let selectedIndexPath = indexPathForSelectedRow {
self.delegate?.tableView?(self, didDeselectRowAt: selectedIndexPath)
}
}
if let selectIndexPath = at {
self.selectRow(at: selectIndexPath, animated: animated, scrollPosition: scrollPosition)
self.delegate?.tableView?(self, didSelectRowAt: selectIndexPath)
}
}
/**
行の選択をdelegateに通知しながら解除します
*/

func deselectRowWithSendNotification(at: IndexPath, animated: Bool) {
if let selectedIndexPath = indexPathForSelectedRow {
self.deselectAllRows(animated: animated)
self.delegate?.tableView?(self, didDeselectRowAt: selectedIndexPath)
}
}
/**
行の選択状態をdelegate先に通知しながら全て解除します
*/

func deselectAllRows(animated: Bool) {
guard let selectedIndexPaths = self.indexPathsForSelectedRows else { return }
for selectedIndexPath in selectedIndexPaths {
self.deselectRow(at: selectedIndexPath, animated: animated)
self.delegate?.tableView?(self, didDeselectRowAt: selectedIndexPath)
}
}
}


参考にさせていただいた記事

見て頂いてありがとうございます。