[iOS] UITableViewでラジオボタンを実現する
UIKitでのラジオボタン
HTMLの入力フォームでよく見かけるラジオボタン
はUIKitのUISegmentedControl
で表現できます。
UITableViewControllerのStaticCellで登録画面等を表現する場合にはCellに対して、UISegmentedControlを持つことでこのようなフォームの代替とできます。
このGenderはMaleかFemaleの選択肢しかありませんでしたが、より多くの選択肢があるようなフォームだったらどうでしょう。
例えば次のような5つの選択肢があるフォームを考えます。
このような選択肢に対しては次のようなSegmentedControlで表現することになります。
今の場合はまだwidthが300に収まっていますが、この選択肢一つ一つに長い名前がついていたりした場合は横幅が足りなくなってもうお手上げです。
さて、このような複数の選択肢をもった属性を表現する場合にはSegmentedControlを使う以外にUITableViewCell#accessoryTypeにUITableViewCellAccessory.Checkmarkをセットして表現する方法があります。
今回のサンプルコードはGithubの方に上がっています。
画面と画面クラスの作成
Storyboardを用いてUITableViewControllerのStaticCellで以下のような画面を作成します。
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クラスまで引っ張ります。
適当な名前ageCellsをつけて接続完了です。
[UITableViewCell]として宣言されたプロパティがUserAddTableViewControllerクラスに宣言されました。いったん宣言されてしまえば、同じフィールドの横にある丸いボタンから各セルにドラック&ドロップで接続できます。
フィールドの横にある丸いボタンの上にカーソルを置くと接続されているセルがわかります。
全セルを接続し終わったら次はセルがタップされた時の挙動を定義しましょう。
セルタップ時の挙動実装
UITableViewDelegateのtableView(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の完成です!
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を画面クラスのフィールドに設定するように修正を加えています。
サンプルコードでは上記のコードを更に発展させてありがちな属性をもったユーザーエンティティの追加フォームが実装されています。ぜひご覧ください!