Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 389 - Implement Navigation

チュートリアル索引に戻る→

2015年10月25日改訂:環境環境:Xcode7.1Swift2.1

アップルのチュートリアルに取り組んでいます。

developer.apple.com

Navigationの組み込みステップです。

このレッスンでFoodTrackerアプリにナビゲーション・コントローラーとセグエの組み込みを学びます。

  • ストーリーボードを使って既存のView Controllerにナビゲーションコントローラーを取りこむ
  • View Controller間にセグエを設定
  • アトリビュート・インスペクタを使ってストーリーボードにあるセグエの属性を変更
  • prepareForSegue(_: sender: )メソッドを使って、View Controller間のデータのやり取り
  • unwindセグエの実行
  • 「stack」ビューを使って、明快に柔軟性のあるレイアウト作成

ナビゲーション・コントローラーは、2つ以上のView Controllerを繋げる役割がありますが、ナビゲーション・コントローラーで制御されるビューコントローラーのセットを「ナビゲーション・スタック」と呼び、最初にアイテムを加えるコントローラーをroot view controllerと言って、コントローラーが切り替わっても画面から消えることがありません。

ナビゲーションコントローラーを既存のView Controllerに組み込んでいくのは簡単です。

Main.storyboardを開いた状態で、table view controllerのシーンドックをクリックして選択します。

f:id:yataiblue:20151027211938j:plain

ここでメニューから「Editor」->「Embed In」->「Navigation Controller」を選ぶだけです。

f:id:yataiblue:20151203184045j:plain

これでストーリーボードのエントリーポイントも新しくできたNavigation Controllerに移ります。そして既存のtable view controllerの上部に「Navigation Bar」が作られてroot view controllerになります。

f:id:yataiblue:20151027212033j:plain

ナビゲーション・バーの設定をしていきます。中央部分をダブルクリックするとタイトルが入力できるようになるので、「Your Meals」とタイプして、オブジェクト・ライブラリから「Bar Button Item」をナビゲーション・バーの右側にドラッグ&ドロップします。「Item」というデフォルトの名前が付いています。

f:id:yataiblue:20151027213443j:plain

ボタン・バーの「Item」が選択されている状態で、アトリビュート・インスペクタを開いて、「System Item」から「Add」を選びます。するとボタンタイトルが「+」に変化します。

f:id:yataiblue:20151027213741j:plain

ここでためしにラン(Cmd + R)すると、table view controllerの上段にタイトルと右側に「+」ボタンが表示されているナビゲーション・バーが表示されます。

f:id:yataiblue:20151027214505j:plain

しかし、この状態で「+」を押しても何も起こりません。ここに動作を加えていきます。

じつはこれが非常に簡単なんです。ナビゲーション・バーの「+」ボタンを押すことで、MealViewControllerに切り替えるセグエ(トランジション)の動きを簡単に設定できます。「+」ボタンをミール・シーンに「Ctrl + ドラッグ」します。

f:id:yataiblue:20151027215928j:plain

MealViewControllerでリリースするとダイアログが表示されるので「Action Segue」から「Show」を選ぶだけです。

これがXcodeを使った実装法で、コードをしらなくてもできます。でもiOSデベロッパーを目指すならコードで実装する方法も理解する必要があります。

yataiblue.hatenablog.com

セグエに関してじっくり取り組むのもいいのですが、チュートリアルをやり遂げる方が重要なので、Xcode実装法に慣れることにしましょう。

「Show Segue」がどんな動きをするのか分かりますか?

この動きが分かる人はセグエの知識は問題無く習得できていると言えます。

そういう私ですが... 当然のように忘れてました(^^;)

分からない人はラン(Cmd + R)して「+」ボタンを押してみるといいでしょう。すーっと画面が左から右に流れます。iPhoneの設定でお馴染みの項目から詳細画面に切り替わる時の効果がみられます。そして驚いたことにBack Buttonが自動的に作られているんです。

これがXcode開発の自動化です。コーディングによる実装とXcodeの自動化がごちゃ混ぜ開発環境は初心者にとって理解しがたいです(^^;)

新しいMealエントリーを登録するのに、このShowセグエは相応しくないということで、Modalセグエに切り替えます。

Main.storyboardでセグエ(Segue)を選択した状態で、アトリビュート・インスペクタを開いて、「Kind」のポップアップメニューから「Present Modally」を選びます。そして、Identifierに「AddItem」とタイプします。このIdentifierを使ってSegueを制御します。

f:id:yataiblue:20151027225032j:plain

次にMealViewControllerが選ばれている状態(シーンドッグ選択状態)で、メニューから「Editor」->「Embed In」->「Navigation Controller」を選択します。

f:id:yataiblue:20151027225825j:plain

MealViewControllerにナビゲーション・バーが埋め込まれので、ダブルクリックしてタイトルを「New Meal」にして、オブジェクト・ライブラリから2つの「Bar Button Item」を左と右にドラッグ&ドロップして、アトリビュート・インスペクタを開き、「System Item」から左側は「Cancel」、右側は「Save」を選びます。表示もそれぞれ「Cancel」と「Save」に変化します。

ここでラン(Cmd + R)させます。

f:id:yataiblue:20151027230655j:plain

「+」ボタンを押すと、下から画面がせり上がってきて、ナビゲーション・バーに「Cancel」と「Save」が並んでいます。

しかし、無反応です。

ここで少しレイアウト調整をします。ミール・シーンの名前ラベルを消去してナビゲーション・バーが設置されたので上部に余分な空白が空きました。Main.storyboardのMealViewControllerで設定している「Stack View」を選択して、右下部にある「Pin」ボタンから「Update Constraints」を選択すると調整されます。

f:id:yataiblue:20151028084541j:plain

次はナビゲーション・バーに設置しているボタン(Item)の実装に進みます。

Cancelボタンでも、Saveボタンでも、ボタンを押すことで、source view controller(ボタンがあるMealViewControllerのこと)が消滅して、新しいview controllerのインスタンスを作ります。これを実現するメソッドprepareForSegue(_, sender:)です。

yataiblue.hatenablog.com

上記でも説明していますが、Xcodeで「Ctrl + ドラッグ」してセグエ(Swgue)を作成した場合には、performSegueWithIdentifier(identifier: String, sender: AnyObject?)メソッドは必要ないので注意が必要です。

MealViewControllerで入力するデータを取りこんで、MealTableViewControllerにそのデータを手渡すしくみをコーディングしていきます。

ますプロジェクト・ナビゲータで「MealViewController.swift」を選択して、コード内の「// MARK: Properties」の1番下に次の変数を入力します。

var meal = Meal?()

オプショナルなMealクラスタイプの変数です。ユーザーが入力するデータを保持するためです。

そしてセグエを実行する「Save 」ボタン(Item)をプロパティとして持たせます。ナビゲーション・バーにある「Save」ボタンを「Crtl + ドラッグ」して「//: MARK: Properties」の下に@IBOutletプロパティとして作ります。@IBActionじゃないので注意が必要です。

 @IBOutlet weak var saveButton: UIBarButtonItem!

f:id:yataiblue:20151028121236j:plain

次に、Segueを実行するメソッドをオーバーライドします。「// MARK: Actions」の上に「// MARK: Navigations」というカテゴリーを用意して次のメソッドをコーディングします。

override func prepareForSegue(segue: UIStoryboardSegue,
                sender: AnyObject?) {
}

[重要] このメソッドは、source view controller(この場合MealViewController)で発動されて、source view controller上のデータを保持したり、source view controllerの後始末をします。

override func prepareForSegue(segue: UIStoryboardSegue,
                sender: AnyObject?) {
 if sender === saveButton {
  let name = nameTextField.text ?? ""
  let photo = photoImageView.image
  let rating = ratingControl.rating

  meal = Meal(name: name, photo: photo, rating: rating)
 }
}

===」はクラスインスタンスの同一性を比べる時に使います。意味は「identical to」で、クラスはリファレンスタイプなので、オリジナルのオブジェクトかどうか確認するために使うようです。senderがUIBarButtonItemクラスのインスタンスオブジェクトのsaveButtonだったら、if{}を実行するということです。

「??」これは次のリンクに説明があります:Swiftで遊ぼう! - 285 - 備忘録 「a ? b : c」と「a ?? b」 - Swiftで遊ぼう! on Hatena

切り替わり先に渡すデータを保持するprepareForSegue(_:sender:)メソッドをsource view controllerに実装させました。実はこのメソッドを使って、切り替わり先のview controller(MealTableViewControllerですね)をインスタンス化させることもできます。しかし、ここではunwind segueを用意します。

切り替わり先のview controllerは「destination view controller」と呼びます。今回は、MealViewControllerからMealTableViewControllerに切り替わる、戻る(Exit)ことになるので、MealTableViewControllerがdestination view controllerになります。

dstination view controllerをインスタンス化するメソッドは、MealTableViewControllerに加えます。

Main.storyboardにあるsegueから発動されるメソッドなので、@IBActionの枕詞を付けてコードします。

MealTableViewController.swiftを開きます。

このクラスの最後の「}」の前に次のメソッドを独自に書き込みます。

@IBAction func unwindToMealList(sender: UIStoryboardSegue) {
}

そして、このメソッドに次のif文を入れます。

if let sourceViewController = sender.sourceViewController as? MealViewController,
                                             meal = sourceViewController.meal {

[重要] このif文は特殊です。色々調べてみたのですが、こういう書き方の説明がありません。「as?」を使ってダウンキャスト判定と、「,」を使ってオプショナル・チェイニングを加えることができます。

まず、「if let」という書き方はオプショナル型の変数がnilかどうかの判断と同時にnilでなければ定数として扱うSwiftならではの書き方です。「as?」は、オプショナル・タイプ・キャスト・オペレータと呼ばれダウンキャストの可否を調べることと利用を一度に判定します。sender.sourceViewControllerはUIViewController型なので、MealViewControllerとしてダウンキャストできるかどうか確認して、ダウンキャストできなかったらnilでif文は実行されません。ダウンキャストできる場合、「,」以下の評価が入ります。sourceViewControllerにmealというプロパティに値があるかどうか判定して、mealがnilならif文は実行されないのですが、mealに値があればmealに値が渡されてif文が実行されます。

最初のコードでTableViewにリストを1つ増やすためにインデックスを作ります。これはObective-CのライブラリNSIndexPath()というメソッドを使います。

let newIndexPath = NSIndexPath(forRow: meals.count, inSection: 0)

forRowは、Row番号を引数として渡します。Row番号は一般的に最初の項目が「0」になります。そしてアレー型mealsの総数は「meals.count」になるので、最後の項目は「meals.count - 1」ということになり、新しい項目は、次の行になるるため「meals.count」を引数として与えて、cellのために新しい場所を確保します。

そして、MealViewCotrollerで取りこんだデータ「meal」をMealTableViewContollerの「meals」に加えることでViewController間のデータの受け渡しができます。

meals.append(meal)

そして最後にTableViewに新しいcellを作るメソッドです。

tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)

これでunwind segueのためのメソッドが用意できました。

@IBAction func unwindToMealList(sender: UIStoryboardSegue) {
if let sourceViewController = sender.sourceViewController as? MealViewController,
                                               meal = sourceViewController.meal {
 let newIndexPath = NSIndexPath(forRow: meals.count, inSection: 0)
 meals.append(meal)
 tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
 }
}

このメソッドの関連づけはstoryboardでする必要があります。「Save」と表示されているナビゲーション・バーのItemを「Ctrl + ドラッグ」してシーン・ドッグの「Exit」でリリースさせると、メニューウインドウが出現して、この中にメソッドが出てくるので選択します。

ここでランします。だいぶ見た目がアプリらしくなりました。

f:id:yataiblue:20151028144351j:plain

自分でサンプルを登録できるようになりました。

次は入力の調整に入ります。食事名の入力が済むまで、「Save」ボタンが薄くグレーアウトして空欄のセーブができないようにします。

ここで復習です。UITextFieldは入力を受け付けることができるのでUIControlをスーパークラスとして持っています。しかし、UITextFieldのインタラクティブな操作は全てデリゲートされているので、MealViewControllerクラスで操作するためにUITextFieldDelegateプロトコールを準拠させる必要があるということは分かりますか?

UITextFieldDelegateメソッドのみをカテゴライズしているので、カテゴリーから飛びます。

f:id:yataiblue:20151028174341j:plain

テキストフィールドに入力が始まった時に「Save」ボタンがグレーアウトするために、saveBUttonのプロパティ、「enabled」を使います。これはBoolean型で、「true」ならアクティブ、「falese」なら「インアクティブです。

func textFieldDidBeginEditing(textField: UITextField) {
 saveButton.enabled = false
}

ボタンのアクティブ判定をするメソッドを用意します。

このメソッドはnameTextFieldの操作によって制御されるので次のようにコードします。

func checkValidMealName() {
 let text = nameTextField.text ?? ""
 saveButton.enabled = !text.isEmpty
}

nameTextField何か入力されると、nameTextField.textに値が入り、その値はtextに代入されます。しかし、「??」が使われているので、何も値が入っていない状態は、nameTextField.textはオプショナル型でnilになり。nilの場合、textに空白の""を代入します。「text」はオプショナルじゃありません。

次にtextが空欄かどうか判定するプロパティが「isEmpty」ですが、空欄だったらfalseで、値があればtrueを返したいので「!」を付けます。

saveButton.enabled = !text.isEmpty

なんとも分かりやすい判定です。

このメソッドをどこで使うか理解しておく必要があります。

まず、テキストフィールドの編集が終了したかどうか判定するUITextFieldのデリゲーションメソッド内で使用します。ついでにナビゲーション・バーのタイトルも変更します。

func textFieldDidEndEditing(textField: UITextField) {
 checkValidMealName()
 navigationItem.title = textField.text
}

f:id:yataiblue:20151028192416j:plain

もう一つ、最後にアプリが立ち上がった時に確認する必要があります。viewDidLoad()メソッド内に入れます。

override func viewDidLoad() {
 super.viewDidLoad()
        
 nameTextField.delegate = self
        
 checkValidMealName()
}

このセクション最後の機能の実装です。残された「Cancel」ボタンにキャンセル機能を加えます。

この最後の機能は今存在しているViewControllerを終了させるだけです。

メソッドを必ず覚えておく必要があります。

Main.storybaardでMealViewControllerを選択して、「Cancel」ボタンを「Ctrl + ドラッグ」して「// MARK: Navigations」の下でリリースします。

いつものようにコネクション用のダイアログが出現して、今度は「Action」にして、名前を「Cancel」、senderは「UIBarButtonItem」に変更して、@IBAction枕詞つきのメソッドを作ります。

今回覚えなければならないメソッドは1つ!

dismissViewControllerAnimated()

タイプしていると補完機能が働いて、欲しいメソッドが出てくるので、こうやっていつも使うメソッドは覚えていくことにします。

@IBAction func Cancel(sender: UIBarButtonItem) {
 dismissViewControllerAnimated(true, completion: nil)
}   

たったこれだけです。

こででランしてみてください。新しいミールデータの登録ができるしキャンセルもできます。しかし、まだ編集ができないので次は編集の話です。

次に進む→Swiftで遊ぼう! - 398 - Implement Edit and Delete Behavior - Swiftで遊ぼう! on Hatena