[iOS] UITableViewでラジオボタンを実現する

2015.05.08

UIKitでのラジオボタン

HTMLの入力フォームでよく見かけるラジオボタン

はUIKitのUISegmentedControl

segmented1

で表現できます。

UITableViewControllerのStaticCellで登録画面等を表現する場合にはCellに対して、UISegmentedControlを持つことでこのようなフォームの代替とできます。

このGenderはMaleかFemaleの選択肢しかありませんでしたが、より多くの選択肢があるようなフォームだったらどうでしょう。

例えば次のような5つの選択肢があるフォームを考えます。

このような選択肢に対しては次のようなSegmentedControlで表現することになります。

segmented2

今の場合はまだwidthが300に収まっていますが、この選択肢一つ一つに長い名前がついていたりした場合は横幅が足りなくなってもうお手上げです。

さて、このような複数の選択肢をもった属性を表現する場合にはSegmentedControlを使う以外にUITableViewCell#accessoryTypeにUITableViewCellAccessory.Checkmarkをセットして表現する方法があります。

今回のサンプルコードはGithubの方に上がっています。

画面と画面クラスの作成

Storyboardを用いてUITableViewControllerのStaticCellで以下のような画面を作成します。

storyboard1

UITableViewControllerのサブクラスを作成します。ここではUserAddTableViewControllerとしておきます。

作成方法によってはUITableViewDataSourceのメソッド等がありますが、StaticCellを用いるため、無視して一旦すべて削除します。

import UIKit

class UserAddTableViewController: UITableViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

先ほど作成したStoryboardのUITableViewControllerのクラスをUserAddTableViewControllerに置き換えます。

ここまででまず第一ステップ完了です。

IBOutletを配列で用いる

Objective-CからInterfaceBuilderの一連のUIコンポーネントを参照する時にはIBOutletCollectionを使えました。Swiftでも一連のUIコンポーネントをIBOutletで配列として参照できます。

先ほど作成したUserAddTableViewControllerのCellへの参照を配列で持たせてみましょう。

Storyboardファイルに作成したCellの一つを右クリックし、表示されるメニューから[New Referencing Outlet Collection]をドラック&ドロップし、先ほど作成したUserAddTableViewControllerクラスまで引っ張ります。

storyboard2

適当な名前ageCellsをつけて接続完了です。

storyboard3

[UITableViewCell]として宣言されたプロパティがUserAddTableViewControllerクラスに宣言されました。いったん宣言されてしまえば、同じフィールドの横にある丸いボタンから各セルにドラック&ドロップで接続できます。

storyboard4

フィールドの横にある丸いボタンの上にカーソルを置くと接続されているセルがわかります。

storyboard5

全セルを接続し終わったら次はセルがタップされた時の挙動を定義しましょう。

セルタップ時の挙動実装

UITableViewDelegatetableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)デリゲードメソッドに下記のように実装を記述します。

extension UserAddTableViewController: UITableViewDelegate {
    
    override func tableView(
        tableView: UITableView,
        didSelectRowAtIndexPath indexPath: NSIndexPath)
    {
        if let selectedCell = tableView.cellForRowAtIndexPath(indexPath) 
            where contains(ageCells, selectedCell) { // -- (1)
            ageCells.map { cell in
                cell.accessoryType = .None
            } // -- (2)
            selectedCell.accessoryType = .Checkmark // -- (3)
        }
        
        tableView.deselectRowAtIndexPath(indexPath, animated: true) // -- (4)
    }
}

上記コードの解説です。

-- (1) のif文での条件判定ではSwift1.2で導入されたif文でのwhere節if let 構文の組み合わせを用いて選択されたCellを取得しています。UITableView#cellForRowAtIndexPathメソッドは指定IndexPathのUITableViewCellをオプショナルにくるんで返しますが、中身が存在していてかつ、その中身がageCellsに含まれている場合のみにif文の中身が走るようになっています。contains関数は第一引数のSequenceType準拠オブジェクトが第二引数の要素を含むかどうかを判定します。

-- (2) の文ではageCells内の全てのCellの#accessoryTypeをいったん.Noneに戻しています。その後、-- (3) で選択されたCellの#accessoryTypeを.Checkmarkにしてチェックマークを表示します。

最後に -- (4)ではセルそのものの選択を解除しています。これはアプリの仕様によってはケースバイケースでしょう。

これだけで接続セル内で一つしか選択されないラジオボタン似の挙動を示すUIの完成です!

cells

UISegmentedControlではどうしても収まりきらないテキストが出てしまう問題も解決です!

SwiftのEnumとラジオボタンUIを紐つける

表示されているもののうちどれか一つしか選択しない場面はSwiftのEnumと相性がいいです。

選択肢がEnumとして切りだされている場合は下記の様に選択された状態を持たせる挙動を記述できます。

enum AgeLevel { // -- (5)
    case Over0
    case Over10
    case Over20
    case Over30
    case Over40
    case Over50
    case Over60
}

class UserAddTableViewController: UITableViewController {
    
    private var selectedLevel: AgeLevel = .Over0 // -- (6)

    @IBOutlet var ageCells: [UITableViewCell]!
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

extension AgeLevel {
    init?(cellIndexPath: NSIndexPath) { // -- (7)
        switch (cellIndexPath.row, cellIndexPath.section) {
        case (0, 0) : self = .Over0
        case (1, 0) : self = .Over10
        case (2, 0) : self = .Over20
        case (3, 0) : self = .Over30
        case (4, 0) : self = .Over40
        case (5, 0) : self = .Over50
        case (6, 0) : self = .Over60
        default : return nil
        }
    }
}

extension UserAddTableViewController: UITableViewDelegate {
    
    override func tableView(
        tableView: UITableView,
        didSelectRowAtIndexPath indexPath: NSIndexPath)
    {
        if let selectedCell = tableView.cellForRowAtIndexPath(indexPath)
            where contains(ageCells, selectedCell) {
            ageCells.map { cell in
                cell.accessoryType = .None
            }
            selectedCell.accessoryType = .Checkmark
        }
        
        if let level = AgeLevel(cellIndexPath: indexPath) { // -- (8)
            selectedLevel = level
        }
        
        tableView.deselectRowAtIndexPath(indexPath, animated: true)
    }
}

-- (5) はラジオボタンに対応した各年齢層をEnumで表したものです。

-- (6) は画面クラスが現在選択中の年齢層を示しています。デフォルトで.Over0としてあるため、Storyboardにも適宜CellのAccesoryTypeに修正を加える部分がありますが、特にここでは明記しません。

-- (7) はセルのインデックスパスに対応するAgeLevelを生成するイニシャライザを定義したExtensionです。指定IndexPath以外ではnilを返す様になっています。(Failable Initializers)

-- (8) では選択されたセルに対応するAgeLevelを画面クラスのフィールドに設定するように修正を加えています。

サンプルコードでは上記のコードを更に発展させてありがちな属性をもったユーザーエンティティの追加フォームが実装されています。ぜひご覧ください!

参考サイト