【Swift】セクション単位で収束できるTableViewを実装する

  • 3
    Like
  • 0
    Comment

次の要件を満たすTableViewを実装します。
- セクションヘッダーをタップすると、セクション下にあるすべての項目を非表示にする(収束)
- 収束状態のセクションヘッダーをタップすると、セクション下にあるすべての項目を表示する(展開)


セクションデータクラスの実装

各セクションの表示状態を決定するにあたり、収束/展開どちらの状態であるかを知る必要があります。
収束の状態 isConvergence をプロパティに持つ SectionData クラスを実装します。

class SectionData: NSObject {
    var isConvergence: Bool = false
    var rows = [String]()
}

プロパティ rows はセクションに属するデータソースのリストです。
サンプルではString型のリストですが、用途に合わせて変更してください。


タップ可能なセクションヘッダーの実装

タップに反応するヘッダーを実装します。
タップイベントをUITableViewの実装先へ通知するまでがヘッダーの役割とします。

protocol TableHeaderViewDelegate: class {
    /// 収束状態の変更要求
    func changeConvergenceState(view: TableHeaderView, section: Int)
}

class TableHeaderView: UILabel {

    fileprivate var section: Int = 0
    weak var delegate: TableHeaderViewDelegate? = nil

    convenience init(section: Int) {
        self.init(frame: CGRect.zero)
        self.section = section
        self._init()
    }
}

// MARK: - fileprivate functions
fileprivate extension TableHeaderView {
    func _init() {
        self.isUserInteractionEnabled = true
        self.textAlignment = .center

        self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleView_Tap(sender:))))
    }

    @objc func handleView_Tap(sender: UITapGestureRecognizer) {
        self.delegate?.changeConvergenceState(view: self, section: self.section)
    }
}

サンプルでは必要最小限、セクションの情報を示す為のUILabelに機能を追加しています。
UILabelにタップイベントを追加し、 TableHeaderViewDelegate の changeConvergenceState() メソッドをコールします。
UILabelでは isUserInteractionEnabledtrue を設定しないとタップイベントが反応しないことに注意してください。


UITableViewをメンバに持つ、UIViewControllerの実装

UIViewControllerに今回の要件を実装します。
UIViewControllerにUITableViewを追加し、 UITableViewDataSource と UITableViewDelegate を関連づけます。

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!

    fileprivate lazy var sections: [SectionData] = {
        var sections = [SectionData]()

        for section in 0...4 {
            let data = SectionData()
            for row in 0...4 {
                data.rows.append("\(section.description): \(row.description)")
            }
            sections.append(data)
        }

        return sections
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return self.sections.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let sectionData = self.sections[section]

        // 実装ポイント
        // セクションが収束している場合(isConvergence)、0を返します
        return sectionData.isConvergence ? 0 : sectionData.rows.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        var cell = self.tableView.dequeueReusableCell(withIdentifier: "cell")
        if cell == nil {
            cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
        }

        cell?.textLabel?.text = self.sections[indexPath.section].rows[indexPath.row]

        return cell!
    }
}

// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 60.0
    }

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let view = TableHeaderView(section: section)
        view.delegate = self
        view.text = "section \(section.description) header"
        view.backgroundColor = #colorLiteral(red: 0.721568644, green: 0.8862745166, blue: 0.5921568871, alpha: 1)
        return view
    }
}

// MARK: - TableHeaderViewDelegate
extension ViewController: TableHeaderViewDelegate {
    func changeConvergenceState(view: TableHeaderView, section: Int) {
        // 実装ポイント
        // セクションがタップされた際、該当するセクションの収束状態を反転し、リロードします
        self.sections[section].isConvergence = !self.sections[section].isConvergence
        self.tableView.reloadSections(IndexSet([section]), with: .automatic)
    }
}

sections プロパティにはテーブルに表示するすべてのデータが入ります。

セクションの行数を返すメソッド tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int では、収束の状態により返す値を変えています。
収束している場合は0を、展開している場合は実データの数を返します。

セクションヘッダーを返すメソッド 'tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?' では、先ほど実装した TableHeaderView を返します。
その際 delegate を関連づけることで、 changeConvergenceState(view: TableHeaderView, section: Int) がコールされるようになります。

セクションヘッダーがタップされた際にコールされるメソッド changeConvergenceState(view: TableHeaderView, section: Int) では、該当するセクションの収束状態を反転し、リロードを行います。
リロードを行うことで tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int がコールされ、収束状態にあった表示に更新されます。


データ操作を行う際の注意事項

セクションの収束状態を表現するため、 tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int では実データに合わない数(0)を返しています。
その為、無条件にTableViewに対して insertmove などを行うと例外が発生しますので注意が必要です。
実装の際は特に収束状態のセクションに気をつけてください。

追加の場合

セクションが収束している場合、tableViewに insert は行わない

更新の場合

セクションが収束している場合、tableViewに reload は行わない

削除の場合

セクションが収束している場合、tableViewに delete は行わない

移動の場合
移動元セクションが収束している、移動先セクションが収束していない場合

moveRow は使わない。移動先セクションに対して insert を行う

移動元セクションが収束していない、移動先セクションが収束している場合

moveRow は使わない。移動元セクションに対して delete を行う

移動元/移動先共に収束している

tableViewに対する操作は行わない


サンプルコード

サンプルをGitHubに公開しています。
https://github.com/ICTFractal/ConvergenceSectionTableViewSample/tree/master