要旨
- ビューコントローラー間のデータの受け渡しは、ストーリーボードを分けてinstantiateInitialViewControllerを使うと簡単
- .storyboardを新規作成したら is Initial View Controllerの設定を忘れない!
内容
UITableViewにタッチすると、その情報を次のビューに送るサンプルです。
こちらのツイートを見たので書きました。
やっとの思いでセクション分けしたUITableViewを作ることに成功したものの、選択された項目によって次のページに遷移させViewを分岐させる(選択された値を渡す)過程でエラー出まくりで詰んでる。。誰か、誰か、、 #Swift pic.twitter.com/y979LBMdVi
— かっぴー (@imachan0322) 2014, 12月 30
たぶん、こういうモノだと思います (違ったらごめんなさい)。
もし違ったとしても、これは前にすごいハマってひどい目にあったので、備忘録も兼ねて書きます。更にやろうと思えば色々できますので、その雛形までに。
GitHubに置いておきます。
※ 私の最近の趣味は「swift」でツイート検索してニヨニヨすることです。
細かい話
ストーリーボードを分けると楽
データを渡す方法はいろいろあるのでしょうが、テーブルビューから次のビューを呼ぶなら「専用コントローラー&専用ストーリーボード」を用意するのが分かりやすくて楽だと思います (今回は関係ありませんが、セル専用xibを作ったりするのも楽ですね。どうもプログラムを書いてUIを作るのは苦手です)。
まず最初にサブのビューコントローラーを用意して、データを渡すために変数を準備します。
class SubViewController: UIViewController {
var targetText: String!
@IBOutlet weak var label: UILabel! {
didSet {
label.text = targetText
}
}
}
ちなみにdidSetの部分はこれを見て爆速でパクりました。
IBOutletに対してのdidSetかー!思いつかなかった / “まだObjective-Cで消耗してるの? - horimislime blog” http://t.co/iPHAYDRulk
— sugitani (@sugitani) 2014, 12月 9
これは本当に便利です。viewDidLoadを占拠する連中をほぼ追放できます。
ストーリーボードの設定は特に難しくないと思いますが、"is Initial View Controller" だけは本当に忘れがちなので注意したいところです。これを忘れると辛い目に遭います。
そうして専用コントローラーを専用ストーリーボードにセットしたなら、いよいよ呼ぶ側の処理をデリゲートにて設定します。
extension ViewController {
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// 渡すデータ
let text = "\(contents[indexPath.section].title) \(contents[indexPath.section].items[indexPath.row])"
// 次のビューコントローラーを用意
let sb = UIStoryboard(name: "Sub", bundle: NSBundle.mainBundle())
let vc = sb.instantiateInitialViewController() as SubViewController
vc.targetText = text
// プッシュ:この時点でIBOutletが用意され、didSetも働く
navigationController?.pushViewController(vc, animated: true)
}
}
テーブルの中身をクラスにすると楽
「UITableViewの正しい使い方」というのが存在するのか知りませんし、おそらく人それぞれだと思いますが、最近の我が家では「データソース機能を事実上もっているクラス」を作るのがブームです。
(何となくこれが一番落ち着くというか… 根拠のない気分的なものです。)
サンプルに入っているTableContentクラスのインスタンスは、セクション単位で情報を保持します。具体的には、セクションタイトルとセクション内部の文字群を持ちます。もちろんもっと大量の情報を持たせることもできますが、そういう時は var items: [HogeClass]!
とでも書き換えて使うことになるかと思います。そっちの方が楽そうだからです。
TableContentクラスには、プロパティリストから情報を持ってくるクラス関数と、セルを作って返すメソッドがあります。
class TableContent {
var title: String!
var items: [String]!
class func readPropertyList() -> [TableContent] {
var contents = [TableContent]()
let filePath = NSBundle.mainBundle().pathForResource("tableContents", ofType: "plist")!
let rawDic = NSDictionary(contentsOfFile: filePath) as [String: [String]]
for title in rawDic["title"]! {
let c = TableContent()
c.title = title
c.items = rawDic[title]
contents.append(c)
}
return contents
}
func makeTableCell(tableview: UITableView, _ indexPath: NSIndexPath) -> UITableViewCell {
let cellID = "tableViewCell"
let cell = tableview.dequeueReusableCellWithIdentifier(cellID) as? UITableViewCell ?? UITableViewCell()
cell.textLabel?.text = items[indexPath.row]
return cell
}
}
.plistを使っているのでローカライズもできます。
ここまで書いておけば、テーブルビューの実装はかなり簡単です。
まずdidLoadの段階でプロパティリストを読んでおきます。
class ViewController: UITableViewController {
// テーブルのコンテンツ
var contents: [TableContent]!
override func viewDidLoad() {
super.viewDidLoad()
// コンテンツの取得
contents = TableContent.readPropertyList()
}
}
データソースの設定が必要ですが、これは機械的に書くだけですね。
ビューコントローラーにはなるべく細かいことを書きたくありませんが、しかしデータソースを別クラスに分離する程でもないので、こういう構造にしてみました。
extension ViewController {
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return contents.count
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return contents[section].items.count
}
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return contents[section].title
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return contents[indexPath.section].makeTableCell(tableView, indexPath)
}
}
なお言うまでもありませんが、この方法が最高で他はダメという趣旨ではありません。
ViewControllerをエクステンションで細切れにすると楽
今回のように短いサンプルだと読みにくいだけですが、1つのクラスでデリゲートの継承を5つも6つもやるときには別ファイルに分けたほうが楽だと思います。
これも何方かの方法をパクりました (思い出せず)。