Swift TableView 少しおしゃれな編集モードを作成した

AppleMusicのようなTableViewの編集モードを作成したいと思い、作成した。

![IMG_1940.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517291/9a5557b0-faa4-db50-2cdd-dc2f18345321.png)


###完成後の姿

![Simulator Screen Shot - iPhone SE (2nd generation) - 2021-11-16 at 10.02.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517291/b25ab400-83cc-3d72-f73a-fbe61e493a4e.png)
![Simulator Screen Shot - iPhone SE (2nd generation) - 2021-11-16 at 10.02.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517291/2ec020ab-33ac-68fa-b2d0-b01ad7c12893.png)


```swift
struct Model {
    // MARK: -  SettingView
    
    /// Sectionのタイトル
    public let sectionLists = ["サービス",
                               "設定"]
    
    /// サービスCell初期状態（更新確認、初回利用者はここを確認される）
    public let serviceCellLists = [CellList(id: 0,  title: "[図書館]Webサイト", category: "図書館", isDisplay: true),
                                   CellList(id: 1,  title: "[図書館]MyPage", category: "図書館", isDisplay: true),
                                   CellList(id: 2,  title: "[図書館]貸し出し期間延長", category: "図書館", isDisplay: true),
                                   CellList(id: 3,  title: "[図書館]本購入リクエスト", category: "図書館", isDisplay: true),
                                   CellList(id: 4,  title: "[図書館]開館カレンダー",   category: "図書館", isDisplay: true),
                                   CellList(id: 5,  title: "シラバス",        category: "シラバス",      isDisplay: true),
                                   CellList(id: 6,  title: "時間割",         category: "教務事務システム", isDisplay: true),
                                   CellList(id: 7,  title: "今期の成績表",    category: "教務事務システム", isDisplay: true),
                                   CellList(id: 8,  title: "これまでの成績",       category: "教務事務システム", isDisplay: true),
                                   CellList(id: 9,  title: "出欠記録",       category: "教務事務システム", isDisplay: true),
                                   CellList(id: 10, title: "授業アンケート",  category: "教務事務システム", isDisplay: true),
                                   CellList(id: 11, title: "メール",         category: "Outlook",      isDisplay: true),
                                   CellList(id: 12, title: "キャリア支援室",  category: "就職活動",       isDisplay: true),
                                   CellList(id: 13, title: "履修登録",       category: "教務事務システム", isDisplay: true),
                                   CellList(id: 14, title: "システムサービス一覧", category: "教務事務システム", isDisplay: true),
                                   CellList(id: 15, title: "Eラーニング一覧", category: "教務事務システム", isDisplay: true),
                                   CellList(id: 16, title: "大学サイト", category: "教務事務システム", isDisplay: true)]
    
    /// 設定Cell（固定）
    public let settingCellLists = [CellList(id:100, title: "パスワード",     category: "",             isDisplay: true),
                                   CellList(id:101, title: "このアプリについて", category: "",           isDisplay: true)]
    
}


/// - CellList:
///   - id             : タップ時にどのCellか判定するid
///   - title          : Cellのタイトル
///   - category  : Cellのサブタイトル
///   - isDisplay : Cellに表示するか決定
struct CellList: Codable {
    let id: Int
    let title: String
    let category: String
    var isDisplay: Bool
}
```

```swift

final class DataManager {
    
    ///
    /// アプリの共通データ（シングルトンのため、必ず同じインスタンスを参照している）
    ///

    public var allCellList:[[CellList]] =  [[], []] // SettingViewのCell内容（ViewModelではその都度インスタンスが生成される為）

    
    static let singleton = DataManager() // シングルトン・インタンス

    /// cellの変更状況を保存
    private let KEY_settingCellList = "KEY_settingCellList"
    public func setSettingCellList(data: Data) {
        userDefaults.set(data, forKey: KEY_settingCellList)
    }
    
    public func getSettingCellList() -> Data? {
        return userDefaults.data(forKey: KEY_settingCellList)
    }
}
```

```swift
final class SettingViewModel: NSObject {
    
    public let sectionHight:Int = 15
    public let cellHight:Int = 44
    public var editSituation = true
    
    private let model = Model()
    private var dataManager = DataManager.singleton
    

    func saveCellList(lists:[CellList]){
        let jsonEncoder = JSONEncoder()
        guard let data = try? jsonEncoder.encode(lists) else {
            return
        }
        dataManager.setSettingCellList(data: data)
    }
    
    func loadCellList() -> [CellList] {
        let jsonDecoder = JSONDecoder()
        guard let data = dataManager.getSettingCellList(),
              let bookmarks = try? jsonDecoder.decode([CellList].self, from: data) else {
            return []
        }
            
        return bookmarks
    }
    

    func firstBootDecision() {
    
        var serviceL:[CellList] = []      // 並び順などを反映するcell
                
        // アプリ起動後、初回の表示か判定
        if dataManager.allCellList[0].isEmpty {
            let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String // 今のバージョン
            
            guard let lastTimeVersion = dataManager.getVersion() else {
                // 初回の利用者か判定
                dataManager.allCellList[0].append(contentsOf: model.serviceCellLists)
                dataManager.allCellList[1].append(contentsOf: model.settingCellLists)
                saveCellList(lists: dataManager.allCellList[0])
                return
            }

            
            // アップデート後、初回の表示か判定
            if lastTimeVersion != version {
                // cellの内容を更新する
                let lastL = loadCellList()        // 前回のcell
                var newL = model.serviceCellLists // 新しいcell
                
                for item in lastL {
                    // 前回のcellのidで新しいcellのidを検索（ユーザーが指定した並び順を更新しても保持できる）
                    // 前回のcellのdisplayを検索した新しいcellへ引き継ぎ
                    // 引き継ぎした内容をserviceLに追加後、newLから削除することで新規機能をまとめて追加できる
                    if let userIndex = newL.firstIndex(where: {$0.id == item.id}) {
                        var edit = newL[userIndex]
                        edit.isDisplay = item.isDisplay
                        serviceL.append(edit)
                        newL.remove(at: userIndex)
                    }
                    // elseには前回にはあったが、今回削除された機能の場合追加しないように通る
                }
                serviceL.append(contentsOf: newL)
            
                dataManager.allCellList[0].append(contentsOf: serviceL)
                
            } else {
                dataManager.allCellList[0].append(contentsOf: loadCellList())

            }
            saveCellList(lists: dataManager.allCellList[0])
            dataManager.allCellList[1].append(contentsOf: model.settingCellLists)
            
            dataManager.setVersion(word: version)          // 更新
        }
    }
}

```



```swift
final class SettingsViewController: BaseViewController {
    
    // MARK: - IBOutlet
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var editButton: UIButton!
    
    private let model = Model()
    private let viewModel = SettingViewModel()
    
    private let dataManager = DataManager.singleton
    

    
    // MARK: - IBAction
    @IBAction func editButton(_ sender: Any) {
        
        if viewModel.editSituation {
            editButton.setTitle("終了", for: .normal)
            
        }else{
            editButton.setTitle("編集", for: .normal)
            
        }
        
        tableView.allowsMultipleSelectionDuringEditing = viewModel.editSituation // 編集モード時、複数選択を許可
        tableView.setEditing(viewModel.editSituation, animated: true)            // 編集モード起動、停止
        viewModel.editSituation = !viewModel.editSituation                       // 編集モード, 使用モード反転
        
        self.tableView.reloadData()
        
        // 編集モード時、チェックがついていて欲しいcellを指定
        if !viewModel.editSituation {
            for i in 0 ..< dataManager.allCellList[0].count {
                print(dataManager.allCellList[0][i].isDisplay)
                if dataManager.allCellList[0][i].isDisplay {
                    self.tableView.selectRow(at: [0,i], animated: true, scrollPosition: .bottom)
                }
            }
            
        }
    }
}


```

```swift

// MARK: - TableView
extension SettingsViewController:  UITableViewDelegate, UITableViewDataSource{
    
    /// セクションの高さ
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return CGFloat(viewModel.sectionHight)
    }
    
    /// セクション数
    func numberOfSections(in tableView: UITableView) -> Int {
        return dataManager.allCellList.count
    }
    
    /// セクションのタイトル
    func tableView(_ tableView: UITableView,
                   titleForHeaderInSection section: Int) -> String? {
        return model.sectionLists[section]
    }
    
    /// セクション内のセル数
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataManager.allCellList[section].count
    }
    
    /// cellの中身
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let tableCell : UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "TableCell", for: indexPath)
        
        tableCell.textLabel!.text = dataManager.allCellList[indexPath.section][indexPath.item].title
        tableCell.accessoryType = UITableViewCell.AccessoryType.disclosureIndicator // 「>」ボタンを設定
        tableCell.textLabel?.font = UIFont.systemFont(ofSize: 17)
        
        return tableCell
    }
    
    
    /// 並び替え
    func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
        if sourceIndexPath.section == proposedDestinationIndexPath.section {
            return proposedDestinationIndexPath
        }
        return sourceIndexPath
    }
    
    
    /// 「編集モード」並び替え検知
    func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        let todo = dataManager.allCellList[sourceIndexPath.section][sourceIndexPath.row]
        dataManager.allCellList[sourceIndexPath.section].remove(at: sourceIndexPath.row)
        dataManager.allCellList[sourceIndexPath.section].insert(todo, at: destinationIndexPath.row)
        viewModel.saveCellList(lists: dataManager.allCellList[0])
    }
    
    
    /// セルの高さ
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        if !viewModel.editSituation {
            return CGFloat(viewModel.cellHight)
        }else{
            if !dataManager.allCellList[indexPath.section][indexPath.row].isDisplay {
                return 0
            }else{
                return CGFloat(viewModel.cellHight)
            }
        }
    }
    
    
    // セルを選択した時のイベント
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        // 編集モードでの選択時処理無効
        if !viewModel.editSituation {
            // チェックボックスTrueの場合
            if indexPath.section == 0 {
                dataManager.allCellList[indexPath.section][indexPath.row].isDisplay = true
                
            }
            
            viewModel.saveCellList(lists: dataManager.allCellList[0])
            return
        }
        
        self.dismiss(animated: true, completion: nil)
        
        guard let delegate = delegate else {
            return
        }
        
        let cellId = dataManager.allCellList[indexPath[0]][indexPath[1]].id
        
        switch cellId {
        case 0: // 図書館Webサイト
        // 処理

        default:
            return
        }
    }
    
    // 編集モード時、チェックが外された時
    func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
        
        if indexPath.section == 0 {
            dataManager.allCellList[indexPath.section][indexPath.row].isDisplay = false
            
        }
        viewModel.saveCellList(lists: dataManager.allCellList[0])
    }
    
    /// 編集できるセクションを限定
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        if indexPath.section == 1 { return false }
        return true
    }
    
}

```

コードはこちら
https://github.com/akidon0000/univIP

備忘録程度に適当に書いているので怒らないでください。。。。