デフォルトTableViewサンプル集 【操作編】(swift)

# はじめに
[デフォルトTableViewサンプル集(swift)](https://qiita.com/am10/items/9bbbe794e88a96e5420e)これのセル選択や編集などの操作編です。

[サンプル集](https://github.com/adventam10/TableSample)

| 複数選択 | 複数選択 2 | 一部選択不可 | セル編集 |
| --- | --- | --- | --- |
| ![multi_selection](https://user-images.githubusercontent.com/34936885/77222594-0c131680-6b98-11ea-9b0d-06b3d6a0b718.png) | ![multi_selection_2](https://user-images.githubusercontent.com/34936885/77543310-dbf6ab00-6eea-11ea-96c1-df66da3069d5.png) | ![unselectable_parts](https://user-images.githubusercontent.com/34936885/77222599-18976f00-6b98-11ea-81b3-f0cfe25c3f93.png) | ![cell_editing](https://user-images.githubusercontent.com/34936885/77222606-25b45e00-6b98-11ea-80f7-031e4d312d36.png) |

| メニュー | インデックスバー | スワイプ |
| --- | --- | --- |
| <img width="300" alt="menu" src="https://user-images.githubusercontent.com/34936885/77222614-3664d400-6b98-11ea-8e88-00c2d49602b0.png"> | <img width="300" alt="section_index" src="https://user-images.githubusercontent.com/34936885/77423970-b480df00-6e13-11ea-9e81-ca3468a31f86.png"> | <img width="300" alt="swipe" src="https://user-images.githubusercontent.com/34936885/77423948-aaf77700-6e13-11ea-81a1-e040b1bf1f7a.gif"> |


# 複数選択
セルの複数選択処理です。
こんな感じ。

<img width="300" alt="multi_selection" src="https://user-images.githubusercontent.com/34936885/77222594-0c131680-6b98-11ea-9b0d-06b3d6a0b718.png">

下記のように設定

```swift
tableView.allowsMultipleSelection = true
```

処理としては上記のようにフラグを `true` にするだけでできる。

選択したものにチェックマークをつけたい場合は下記も実装。

```swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
    
  // ここ
  cell.accessoryType = tableView.indexPathsForSelectedRows?.contains(indexPath) == true ?  .checkmark : .none
  cell.selectionStyle = .none // 選択時に背景色を変えないため
  return cell
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  if let cell = tableView.cellForRow(at: indexPath) {
    cell.accessoryType = .checkmark // チェックつける
  }
}
    
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
  if let cell = tableView.cellForRow(at: indexPath) {
    cell.accessoryType = .none // チェックはずす
  }
}
```

## パターン２(20200325追記)
そういえば左側にチェックマーク置くのも簡単にできたので追記です。

こんな感じ。

<img width="300" alt="multi_selection2" src="https://user-images.githubusercontent.com/34936885/77543310-dbf6ab00-6eea-11ea-96c1-df66da3069d5.png">

実装は下記３行を追加するだけ！

```swift
tableView.isEditing = true
tableView.allowsMultipleSelection = true
tableView.allowsMultipleSelectionDuringEditing = true
```

# 一部のセル選択不可
一部のセル選択不可にしたい場合は下記のように設定します。

```swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
  
  // 選択不可にしたい場合にnone設定
  cell.selectionStyle = selectable ? .default : .none 
  return cell
}
    
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
  // cell.selectionStyle = .noneだけだとselect自体はできる
   // 選択不可の場合にnilを設定する
  return selectable ? indexPath : nil 
}
```

`cell.selectionStyle = .none` だけだとセルタップ時に `tableView(_:didSelectRowAt:)` は呼ばれるし `indexPathForSelectedRow` にも値が設定されてしまうので `tableView(_:willSelectRowAt:)` も設定する。

# セルの編集
初期表示で編集モードにしたい場合は `tableView.isEditing = true` を設定する。

編集ボタンを表示したい場合は `navigationItem.rightBarButtonItem = editButtonItem`でいける！編集モードの場合は完了ボタンに自動で変わってくれる:clap: `UITableViewController` の場合は編集ボタン押下で自動で編集モードに変わってくれる:clap::clap:(`UIViewController` の場合は `setEditing(_ editing: Bool, animated: Bool)` を実装して `tableView.isEditing = true` で設定してやる必要がある)

編集モードの場合もセル選択を有効にしたい場合は `tableView.allowsSelectionDuringEditing = true` を設定する。

こんな感じ

<img width="300" alt="edit" src="https://user-images.githubusercontent.com/34936885/77222606-25b45e00-6b98-11ea-80f7-031e4d312d36.png">

## 削除
削除ボタンを表示する。

下記を設定する。

```swift
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
  if editingStyle == .delete {
    tableDataList.remove(at: indexPath.row) // 先にリストから削除する
    // テーブルの高さが変わるのでこれで囲む
    tableView.beginUpdates()
    tableView.deleteRows(at: [indexPath], with: .automatic)
    tableView.endUpdates()
  }
}
```

これで削除ボタンタップ時と左にスワイプした時に削除処理がはしる。

## 追加
追加ボタンを表示する。

下記を設定する。

```swift
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
  if editingStyle == .insert {
    // 対象セルの下に追加（先にリストに追加する）
    tableDataList.insert(0, at: indexPath.row + 1)
    tableView.beginUpdates()
    tableView.insertRows(at: [IndexPath(row: indexPath.row + 1, section: 0)], with: .automatic)
    tableView.endUpdates()
  }
}

override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
  return .insert // これを設定しないと削除モードになる
}
```

## 移動
セルを移動させる。

下記を設定する。

```swift
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
  return true
}

override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
  // 移動処理
  let element = tableDataList[sourceIndexPath.row]
  tableDataList.remove(at: sourceIndexPath.row)
  tableDataList.insert(element, at: destinationIndexPath.row)
}
```

削除ボタンを表示したくない場合

```swift
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
  return .none
}
```

編集モード時にインデントを変更したくない場合（削除or追加ボタンがある場合は変わらない）

```swift
override func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
  return false
}
```

### 移動不可

```swift
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
  return indexPath.row != 0 // 一番上のセルを移動不可に設定
}
```

上記の設定で一番上のセル(row=0)は移動不可になりましたが他のセルを一番上に移動させることは可能です:frowning2:移動先としても不可にしたい場合は下記も設定する。

```swift
override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
        return indexPath.row != 0 ? sourceIndexPath : proposedDestinationIndexPath
    }
```

# ロングタップでメニュー表示
<img width="300" alt="copy" src="https://user-images.githubusercontent.com/34936885/77432616-a76aec80-6e21-11ea-8e28-cb1e4d280601.png">

こんな感じのやつすぐ実装できんねや！知らんかった！と思ったら全部 iOS13 で Deprecated だった...

```swift
func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool 
func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool
func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?)
```

代わりにこれを使うらしい `tableView(_:contextMenuConfigurationForRowAt:point:)`

こんな感じ

<img width="300" alt="menu" src="https://user-images.githubusercontent.com/34936885/77222614-3664d400-6b98-11ea-8e88-00c2d49602b0.png">

```swift
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
  let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { actions -> UIMenu? in
    let copy = UIAction(title: "Copy", image: UIImage(systemName: "doc.on.doc"),
                                identifier: nil)
            { action in
                if let cell = tableView.cellForRow(at: indexPath) {
                  UIPasteboard.general.string = cell.textLabel?.text
                }
            }
            return UIMenu(__title: "Menu", image: nil, identifier: nil, children: [copy])
  }
  return configuration
}
``` 

# インデックスバー
標準の電話帳アプリとかにあるやつ。

こんな感じ

<img width="300" alt="section_index" src="https://user-images.githubusercontent.com/34936885/77423970-b480df00-6e13-11ea-9e81-ca3468a31f86.png">

テーブルの style が  `.grouped` でも表示されるがドキュメントには `.plain` のみみたいなことが書いてあるので `.plain` にしましょう。
>The table view must be in the plain style (UITableViewStylePlain). 

参考：[ドキュメント](https://developer.apple.com/documentation/uikit/uitableviewdatasource/1614857-sectionindextitles)

```swift
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
  return ["A", "B", "C"] // インデックスバーの文字設定
}

// 特殊なことをしないのであれば特に実装しなくても対象のセクションが表示される
func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
  return index 
}
```

虫眼鏡アイコン下記のようにすると一番上に虫眼鏡アイコンが表示される。

```swift
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
  return [UITableView.indexSearch, "A", "B", "C"]
}
```

使うなら一番上に置けみたいなことが書いてある。
>This location should generally be the first title in the index.

参考：[ドキュメント](https://developer.apple.com/documentation/uikit/uitableview/1614959-indexsearch)

これの使い道はよくわかりません。。。:thinking:

## カスタム
下記のように色が設定できる。

```swift
tableView.sectionIndexColor = .white // 文字色
tableView.sectionIndexBackgroundColor = .systemPurple // バーの背景色
tableView.sectionIndexTrackingBackgroundColor = .systemRed // ハイライト時の背景色
```

# スワイプ
なんか昔ライブラリ入れてやった気がしますが標準でスワイプアクションがあるそうです。

こんな感じ。

<img width="300" alt="swipe" src="https://user-images.githubusercontent.com/34936885/77423948-aaf77700-6e13-11ea-81a1-e040b1bf1f7a.gif">

（めっちゃアクションを追加できるけどたぶんそんな使い方をすることはない）

下記のように実装する。

```swift
// 右へスワイプ
func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
  let action = UIContextualAction(style: .normal, title: "Action") { (action, view, completionHandler) in
           // なんか処理
           completionHandler(true) // 処理成功時はtrue/失敗時はfalseを設定する
           }
           let configuration = UISwipeActionsConfiguration(actions: [action])
        return configuration
}

// 左へスワイプ
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let action = UIContextualAction(style: .destructive, title: "Action") { (action, view, completionHandler) in
           // なんか処理
           completionHandler(true) // 処理成功時はtrue/失敗時はfalseを設定する
           }
           let configuration = UISwipeActionsConfiguration(actions: [action])
        return configuration
}
```

Action の `style` には `normal` と `destructive` (削除系)の2種類があります。

スワイプを引っ張り切ると `UISwipeActionsConfiguration(actions: [action])` の最初の `action` が実行されます。

これを無効にした場合は `configuration.performsFirstActionWithFullSwipe = false` を設定する。

## カスタム

```swift
action.backgroundColor = .systemBlue // 背景色設定
action.image = UIImage(systemName: "pencil.circle.fill") // 画像設定（タイトルは非表示になる）
```

# さいごに
Table関連は頻繁に処理が追加されてるのでたまにチェックした方がいい。追加されたときは見るけど古いバージョンも動作保証してると使えないのでよく忘れます...たまにチェックしよう！

ドラッグドロップは色々できそうなのでまた時間があれば追加します...

プルリクくれてもええんやで:rolling_eyes:

[サンプル集](https://github.com/adventam10/TableSample)

[プルリク大歓迎（だれかきてくれないかな:eyes::eyes::eyes:）](https://github.com/adventam10/TableSample/blob/master/CONTRIBUTING.ja.md)