「Start Developing iOS Apps (Swift)」でFoodTracker Appを実際に作ってみる その8[ナビゲーションを実装する]

「Start Developing iOS Apps (Swift)」でFoodTracker Appを実際に作ってみる その8[ナビゲーションを実装する]

公式にある「Start Developing iOS Apps (Swift)」を使って実際にFoodTracker Appを作ってみる、というシリーズの第八回目です。前回はこちら

「Start Developing iOS Apps (Swift)」でFoodTracker Appを実際に作ってみる その7[テーブルビューを作る]
公式にある「Start Developing iOS Apps (Swift)」を使って実際にFoodTracker Appを作ってみる、と...

前回はテーブルビュー表示のsceneを起動画面に設定し、アプリに追加した料理全体をリスト表示できるようにしました。ただし、それによって、最初に導入したview controllerが表示できなくなりました。今回はnavigation controllerと「segue」を使って、FoodTrackerにナビゲーション機能を追加します。

この記事を書いている時点でのXcodeのバージョンは8.1です。

目次

ナビゲーションの実装

いつもの学習目的(Learning Objectives)ですが、今回のパートで

  • ストーリーボード上で既存のview controllerをnavigation controllerの中に組み込む
  • 2つのview controller間を繋ぐsegueを作る
  • ストーリーボード上でattributes inspectorを使い、segueの特性を編集する
  • prepare(for:sender:)メソッドを使ってview controller間でデータをやり取りする
  • Unwind segueを実装する
  • Stack viewを使った堅牢で柔軟なレイアウトを作る

ことが出来るようになることが目標です。最後の「堅牢で柔軟な」という表現は抽象的ですが、stack viewを使ったレイアウトに慣れるくらいのニュアンスでも良いかと思います。

この回が終わった後、アプリの起動画面は

Xcode: シミュレータでnavigation barを確認
こんな感じになります。右上のプラスボタン(+)をタップして、料理追加画面に行くと

Xcode: 料理名を入力するまではSaveボタンを無効にする
このようになっているはずです。

画面を遷移するためのsegueを追加する | Add a Segue to Navigate Forward

Segueって何?

Segueは2つのscene(view controller)間の遷移を操作するための機能です。日本語では「セグエ」と書かれていることが多いですが、発音は「セグウェイ(seg-way)」と呼ぶようなので、ここでは英語表記segueを使います。「遷移」という表現を別の英語にすると「transition」でしょうか。

前回テーブルビューを実装したことで、料理のリスト表示が可能になりましたが、個別の料理表示が出来るsceneを表示することが出来なくなりました。そこで、

料理のリスト表示から個別の料理表示に遷移(ナビゲーション)する

機能を実装しよう、というのが今回の目標です。この画面遷移(の機能)をsegueと呼んでいます。

Navigation Controllerって何?

画面遷移機能を実装するには、view controllerのサブクラスであるnavigation controllerが必要です。

Navigation controllerとは、その名前の通り、複数のview controller間の画面遷移をコントロールするためのものです。この後出てくるので、言葉の定義をすると、

navigation stack
ある特定のnavigation controllerの管理下にある(複数の)view controller

root view controller
navigation stackの最初のview controller。これはnavigation stackから削除出来ない

です。

料理リストのsceneにnavigation controllerを追加する

先ずnavigation controllerをテーブルビューのsceneに追加してみます。

ストーリーボードを開き、テーブルビューのscene dockをクリック

Xcode: Table view controllerをクリックした状態
ストーリーボード(Main.storyboard)を開くと2つのsceneがありますが、テーブルビューのscene dockをクリックして選択した状態にします。Scene dockはsceneの一番上にあるツールバーのような部分です(上の画像で一番上のアイコンが3つ並んでいるバー)。

Navigation controllerを追加する

Table view controller(テーブルビューのscene)が選択された状態で、Xcodeのメニューから

Editor > Embed In > Navigation Controller

を選択してnavigation controllerをストーリーボードに追加します。

Xcode: Navigation controllerをストーリーボードに追加
Navigation controllerが追加されると、ストーリーボードは上の画像のようになっているはずです。

Storyboard entry pointはnavigation controllerを指し(一番左の矢印)、navigation controllerからtable view controllerへ矢印(真ん中の矢印)が出ています。この矢印アイコンが

Table view controllerはnavigation controllerのroot view controllerである

ということを示しています。公式ページではこれをroot view controller relationshipと呼んでいます。Navigation controllerが手前(左側)来るのは、navigation controllerがview controllerを包含しているからです。

また、navigation controllerを追加すると、テーブルビュー上部に灰色の余白が出来ますが、これをnavigation barと呼んでいます。Navigation barには画面遷移用のコントロール(ボタン)を追加することが出来ますので、次のセクションで早速実装してみます。

シミュレータで確認

ここまでで一旦ビルドしてシミュレータを起動します。

Xcode: シミュレータでnavigation barを確認
Navigation controllerを実装したことで、テーブルビュー上部に灰色の余白が出来ましたが、これがnavigation barです。前回最後にシミュレータで確認した画面では、一番上のリストがステータスバーに被っていました。Navigation barを実装すると、その背景がステータスバーまで伸びており、テーブルセルとステータスバーの重なりが解消されているのが分かります。

Navigation Barの設定 | Configure the Navigation Bar for the Scenes

次はnavigation barの設定です。最初はテーブルビューのsceneから編集していきます。今navigation barに追加したいのは

  • 料理リスト用のタイトル
  • 新規料理追加ボタン

です。

料理リスト用のnavigation barを設定 | タイトルとボタンを追加

料理リストのタイトルを追加する

タイトルの設定は、navigation controllerでは無く、view controller(ここではtable view controller)上部にあるnavigation barで行います。

Xcode: Navigation barをダブルクリック
ストーリーボードで料理リスト用scene(テーブルビューのscene)を選択して、navigation barをダブルクリックすると、テキストを入力出来る状態になります(上の画像参照)。

Xcode: Navigation barにタイトルを入力
タイトルを入力したらreturnキーを押すと確定します。公式ページではタイトルが「Your Meals」なので、直訳すると「あなたの料理(or 食事)」となりますが、ここでは「料理一覧」としました。

新規料理追加用ボタンを設置する

次に、クリックすると新規料理を追加出来るようなボタンを設置します。

Xcode: Object Libraryでbar button itemを探す
Utility areaにあるObject Libraryを開き「Bar Button Item」オブジェクトを探します(上の画像参照)。「Bar Button Item」をドラッグして、navigation barの右端にドロップします。

Xcode: Bar Button Itemをnavigation barに追加
そうすると、上の画像のように、「Item」とかかれたUIが表示されます。このままだと「追加」という意味合いが分からないので、見た目で分かりやすい「+」ボタンを表示するように設定を変更します。

Xcode: Bar button itemでSystem ItemをAddに変更
Bar button itemを選択した状態で、attributes inspector(utility area上部の左から4つ目or右から3つ目のメニュー)を開きます。「System Item」という項目が「Custom」になっているはずですが、これを「Add」に変更します(上の画像参照)。

「System Item」変更後、ストーリーボードでの表示は

Xcode: Bar Button ItemのSystem ItemをAddに変更後
のようになっているはずです。「Item」と表示されていたボタンが「+」に変わっていることを確認します。

シミュレータで確認

シミュレータを起動して確認すると、

Xcode: Simulatorでタイトルとボタンがnavigation barに表示されていることを確認
という感じで、navigation barにタイトルと+ボタンが表示されているはずです。ボタンはクリックしても何も起きませんが、次はここを設定します。

今欲しい機能は

ボタンをクリックすると新しい料理を追加するためのsceneに遷移する

というもので、この画面遷移がまさに「segue(transition)」です。という訳で、次のセクションではsegueを実装していきます。

料理用sceneで「Add」ボタンを設定する | show segueをセットする

テーブルビューのnavigation barの+ボタン(Addボタン)を選択した状態にして、ボタンから料理用sceneの方へcontrol+ドラッグします。

Xcode: Bar Button Itemをcontrol+ドラッグで料理用sceneへ引っ張る
上の画像はcontrol+ドラッグしている最中のスナップショットです。アウトレット(outlets)アクション(actions)control+ドラッグで作成した時と似ています。

ドロップすると

Xcode: control+ドラッグを落としてAction Segue
このように「Action Segue」というポップアップメニューが出てきます。「Action Segue」では、ユーザがAddボタンをタップした際、料理リストから新規料理作成画面へどのように遷移するのかを決めることが出来ます。

ここではメニューの一番上にある「show」を選びます。

Xcode: Show segueを追加
これで「show segue」がセットされました。上の図を見ると分かりますが、テーブルビューsceneと料理用sceneを繋ぐsegueが表示されています。Show segueを追加したので、addボタンをタップすると料理用sceneが表示されるように設定されたはずです。また、料理用scene(meal view controller)にnavigation barが追加されていることも分かります。

シミュレータで確認

シミュレータを起動して+(Add)ボタンをクリックすると、

Xcode: show segueを使ったAddボタンで画面遷移
ちゃんと新規料理追加画面に遷移します。また、show segueをセットした場合、navigation bar左側に、一つ前の画面に戻るボタンが自動的に追加されます。ボタンの名前は料理リストsceneに設定したタイトル「料理一覧」になっているのが分かります。

料理を追加しないと戻れない仕様にしたい

今設定したような、view controller間の移動が自由に出来るようなナビゲーションを「push-style navigation」または「push navigation」と表現しています。しかし、料理を追加するという操作の場合は、ユーザが必要な情報を入力するまでは、強制的に現在の画面に留まらせるような仕様が望ましいです。こういう機構をmodal operationとかmodal windowとか呼んでいます。

したがって、今実装したいsegueは「show segue」では無く「modal segue」です。公式ページの説明だと

modal segue
A segue in which one view controller presents another view controller as its child, requiring a user to perform an operation on the presented controller before returning to the main flow of the app.

と書いてあります。Model segueでは、連結したview controller間の関係が親子のようになって、子(遷移先)での処理を強制実行させることが出来ます。

Segueスタイルの変更 | Show segueをmodal segueに変更する

先程作ったsegueを消して作り直しても良いのですが、既存のsegueのスタイルを変更することも出来ます。Segueのスタイルを「modal segue」に変更するため、attributes inspectorを使います。

Show segueを選択してattributes inspectorを開く

先程作ったshow segueをクリックして選択した状態にします。Utility areaでattributes inspector(メニューの左から4つ目、右から3つ目)を開き、show segueをmodal segueに変更します。

  • Kindで「Show (e.g. Push)」を「Present Modally」に変更
  • Identifierで「AddItem」と入力

Xcode: Show segueをmodal segueに変更
Modal segueに変更すると、segueのアイコンの見た目が白抜きの四角に変わります(上の画像参照)。また、identifierに設定した名前は後で使います。

上の画像を見ると分かりますが、modal segueに変更すると、遷移先である料理sceneのnavigation barが無くなります。Navigation barが表示されなくても良いかもしれませんが、元々表示されていたnavigation barがいきなり消えるのも気持ち悪いです(公式ページでは「視覚的連続性を提供(provide visual continuity)」と言ってます)。

そこで、次は料理sceneにも独自のnavigation controllerを追加して、navigation barを表示します。

料理用sceneにnavigation controllerを追加する

料理リストのscene(table view controller)にnavigation controllerを追加した時と全く一緒です。料理用sceneのscene dockをクリックして、メニューから

Editor > Embed In > Navigation Controller

でnavigation controllerを新たに追加します。

Xcode: Meal sceneにnavigation controllerを追加する
そうすると上の画像のように、料理用sceneにもnavigation barが追加されます。次は、このnavigation barを編集して

  • タイトル
  • 2つのボタン: 「Cancel」と「Save」

を追加します。(繰り返しになるかもしれませんが)編集するのは料理用scene(view controller)のnavigation barで、navigation controllerのそれではありません。

料理用sceneのnavigation barを設定する

タイトルを追加する

先ずnavigation barにタイトルを追加します。Navigation barをダブルクリックすると、テキストフィールドが出てくるので

Xcode: Navigation barにタイトルを追加
上の画像のように「料理を追加」というタイトルにします(公式ページの直訳だと「新規料理」でしょうか)。新しく料理を追加するという意図が伝われば、タイトルは何でも良いと思います。

ボタンを追加する

次に、Object Libraryから「Button」オブジェクトを検索、ドラッグしてnavigation bar左端にドロップします。

Xcode: Navigation barにCancelボタンを追加
Attributes inspectorを開き、「System Item」で「Cancel」を選択すると、ボタン上に表示されているテキストが「Cancel」に変わります(上の画像参照)。

同様に、もう1つの「Button」オブジェクトをObject Libraryからnavigation barの右端にドロップ。Attributes inspectorの「System Item」で「Save」を選択します。

Xcode: Navigation barにSaveボタンを追加
最終的にストーリーボード上でのnavigation bar表示は、上の画像のようになっているはずです。

シミュレータで確認

タイトルとボタンを追加したら、シミュレータを起動して新規料理を追加する画面を開いてみると、

Xcode: Navigation barを料理用sceneに実装した直後
このようになっており、navigation barとタイトル、及び「Cancel」と「Save」ボタンがちゃんと実装されているのが分かります。ただし、ボタンに何のアクションも設定していないので、「Cancel」や「Save」ボタンをクリックしても何も起こりません。

そこで次のセクションでは、これらのボタンにアクションを追加します。具体的には、「Cancel」をクリックすると料理リストの画面に戻り、「Save」をクリックすると新規料理を料理リストに追加する、という機能を実装します。

Auto LayoutでUIを完成させる | Finalize the UI with Auto Layout

Simulatorを見ると分かりますが、テキストフィールドとnavigation barの間に余分な空白があるので、stack view全体を上に移動して空白を潰します。

Xcode 7.xでは「Resolve Auto Layout Issues」で自動的にstack viewとnavigation barの上下位置が調整されたようですが、Xcode 8.1ではうまくいきません。Auto layoutが賢くなって、navigation barが挿入されてもUIの再配置を自動的に行っている、のかもしれません(推測)。また、stack viewの上部constraintは明示的に指定しているので、その場合はauto layoutの自動調整で変更することが出来なくなっているのかもしれません。

Stack viewを上マージンの位置へ移動

Outline viewからstack viewを選択します。ストーリーボード上でクリックして選択することも可能ですが、難しいのでoutline viewのメニューから選択する方が楽です。

Xcode: Stack viewを上にドラッグ
Stack viewを選択したら、上にドラッグします。Navigation barに出来るだけ近づけますが、ある程度近づけるとマージンの位置で点線が出てきます(上の画像参照)。この位置でstack viewをドロップします。

Xcode: Stack viewを上に移動した直後
UIを移動すると、stack viewの上側に設定したconstraints表示が赤くなり、元々stack viewが置かれていた位置にオレンジの点線が表示されます(上の画像参照)。これは、実行時のレイアウトが設定されているレイアウトと違うという警告なので、auto layoutを使って解決します。

Resolve Auto Layout Issuesでconstraintsを自動調整

Xcode: Resolve Auto Layout Issuesでconstraintsの調整
Stack viewを選択した状態で、canvas右下にある「Resolve Auto Layout Issues」をクリックします(上向き三角のようなアイコン)。「Selected Views」項目の「Update Constrained Constants」がアクティブになっているはずなのでクリックします。

シミュレータで動作確認をすると、

Xcode: stack viewとnavigation barの余白を消した後
このようにnavigation barとテキストフィールド(stack view全体)の余白が小さくなっているのが確認できます。

Stack viewのsize inspectorや、outline viewの「Constraints」に設定されている値を直接チェックして確認することも出来ます。

次は、navigation barに設置した2つのボタンにアクションを追加します。先ずは「Save」ボタンからです。

新しい料理をリストに保存する | Store New Meals in the Meal List

Navigation bar右側に設置したボタン「Save」にアクションを実装します。「Save」という言葉から簡単に連想出来ますが、新しく実装したいのは、

「Save」ボタンをタップすると、新しい料理(入力した料理名と写真、さらに料理の評価)をリストに追加して表示する

という機能です。これをプログラミング言語的な表現に置き換えると、

  • 新しい料理情報を保持したMealオブジェクトをMealViewControllerで作る
  • 作ったMealオブジェクトをMealTableViewControllerに渡して、テーブルビューで表示する

となります。

MealプロパティをMealViewControllerに追加する

先ず必要なのは、前者のMealオブジェクトを作る部分です。後者の機能は次のセクションで実装します。

MealViewController.swiftを開き、ratingControlアウトレットの下に

// This value is either passed by `MealTableViewController` in `prepare(for:sender:)`
// or constructed as part of adding a new meal
var meal: Meal?

を追加します。オプショナル(optionals)として定義するので、途中でnilになる可能性があるようです。

Mealオブジェクトを作るのはUIの「Save」ボタンがタップされた時だけですが、それを判断するために「Save」ボタン用アウトレットも作ります。

SaveボタンとMealViewControllerを繋ぐ

いつものアウトレット作成ですので、手順を箇条書きにします。

  • ストーリーボードを開く
  • Assistant editorを開く(邪魔ならnavigator areaやutility areaを隠す)
  • ストーリーボード上の「Save」ボタンからcontrol+ドラッグで、MealViewController.swiftのratingControl下にドロップ
  • 出てきたダイアログで、Nameを「saveButton」にセット、残りの項目はデフォルトのまま「Connect」をクリック

Xcode: saveButtonアウトレットを追加
最終的には上の画像のように、saveButtonアウトレットが追加されているはずです。

Unwind Segueを作る | Create an Unwind Segue

次は、

MealViewControllerで作った)MealオブジェクトをMealTableViewControllerに渡す

という部分を実装したいのですが、テーブルビュー表示にするということは一つ前の画面に戻ることになります。この「戻る」という画面遷移を実行するのがUnwind Segueです。

Unwindというのは「(巻いたものを)巻き戻す」とか「解きほぐす」という意味ですので、unwind segueで「戻るためのsegue」「戻すsegue」という感じでしょうか

画面遷移時に何かしらのアクションを実行する場所が、メソッドprepare(for:sender)のようです。このメソッド(methods)を使うと、source view controller(segueの始点があるview controller)上のデータ保存等を実行することが出来るので、MealVewControllerに早速実装します。

MealViewControllerにprepare(for:sender:)を実装する

MealViewController.swiftを開き、prepare(for:sender:)を追加

MealViewControllerを編集したいので、standard editor表示に戻して、MealViewController.swiftを開きます。コメント// MARK: Actionsの上に、navigation用のmarkコメント

// MARK: Navigations

を追加し、その下にprepare(for:sender:)の外枠

// This method lets you configure a view controller before it's presented.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
}

を追加します。「prepare」とタイプしていくと、補完候補が表示されるはずです。

Xcode 7.x/Swift 2.xでは、prepareForSegue(_:sender:)というメソッドでした

このメソッドのパラメータ(parameters)は2つで、1つ目のforがsegue、2つ目のsenderはsegueの始点にあるオブジェクトです。「Save」ボタンのケースだと、このボタンがsenderに該当します。

saveButtonとsenderが同じインスタンスかどうか条件判定

先程も述べたようにsenderはsegueの始点にあるオブジェクトですから、sendersaveButtonと「同じ」かどうかいう条件判定が必要になります。それを実装すると、

if let sender = sender as? UIBarButtonItem, saveButton === sender {
}

という感じになります。

条件判定の部分が少しややこしく見えますが、

if let sender = sender as? UIBarButtonItem { // UIBarButtonItemにダウンキャスト
    if saveButton === sender { // saveButtonとsenderのインスタンスは同じか?
        ....

を1行にまとめているだけです。1行目のif文ではsenderUIBarButtonItemダウンキャスト(downcasting)し、2行目ではsaveButtonsenderを比較しています。

比較に使っている演算子はidentity operator===であって、equal operator==では無いので注意が必要です。今実行したい条件判定は、

saveButtonsenderが指しているインスタンスが同じかどうか

ですから===を使う必要があります。

Swift 3.0からの変更点 | Identity operatorを使う場合はAnyに対して明示的な型キャスト

Swift 3.0から、Anyに対して明示的な型キャスト(type casting)無しでidentity operatorなどの演算子を使った操作はコンパイルエラーになります。したがって、上記のコードの1行目のように、senderを明示的にUIBarButtonItemsaveButtonの型)に型キャストしています。型キャスト無しで比較しようとすると、

if saveButton === sender {
        ....
}
// Binary operator '===' cannot be applied to operands of type 'UIBarButtonItem!' and 'Any?'

という感じでコンパイルエラーになります。

アウトレットから値を取り出す

次に、アウトレットからMealオブジェクトに値を渡す部分を、if文の中に記述します

if let sender = sender as? UIBarButtonItem, saveButton === sender {
    let name = nameTextField.text ?? ""
    let photo = photoImageView.image
    let rating = ratingControl.rating
}

アウトレットはストーリーボードのUIと繋がっているプロパティ(properties)なので、これは結局ストーリーボードで入力された情報を、MealViewControllerMealオブジェクトに渡していることになります。

nameプロパティへ値を渡すコードは、

let name = nameTextField.text ?? ""

のように、nil合体演算子a ?? bを使っていますので、nameTextField.textが値(文字列)を持っていればunwrapして返し、nameTextField.textnilの場合は空白文字""を返す、という演算です。

if文で書くと分かりやすいと思いますが、以下のような構文

if let text = nameTextField.text {
    return text
} else {
    return ""
}

と等価です。

アウトレットから取り出した値をMealオブジェクトに渡す

最後に、アウトレットから取り出した値をMealオブジェクトに渡します。先程実装したコードの続き、if文の一番下に、

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

を追加します。

これでprepare(for:sender:)は、

// This method lets you configure a view controller before it's presented.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if let sender = sender as? UIBarButtonItem, saveButton === sender {
        let name = nameTextField.text ?? ""
        let photo = photoImageView.image
        let rating = ratingControl.rating

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

のようになっているはずです。

MealTableViewControllerにアクションメソッドを追加する

次に、MealViewControllerで作ったMealオブジェクトを、テーブルビュー(MealTableViewController)に渡すアクションメソッドを実装します。

[図解]Segueを使ってview controller間でデータをやり取りする
概略図を書くと上のようになります。Mealオブジェクトを作ったのはMealViewController(source view controller)ですが、これをMealTableViewController(destination view controller)に渡します。オブジェクトのやり取りをするために使うのがsegue(UIStoryboardSegue)です。

「Destination」は「行き先」や「届け先」、または「目的地」という意味で使われます

Unwind segueは「戻る」方向の遷移なので、sourceがMealViewControllerで、destinationがMealTableViewControllerになります。

メソッドunwindToMealList(_:)、segue(UIStoryboardSegue)をパラメータとして定義

先にアクションメソッド全体を示しておくと、

@IBAction func unwindToMealList(_ sender: UIStoryboardSegue) {
    if let source = sender.source as? MealViewController, let meal = source.meal {
        // Add a new meal
        meals.append(meal)

        let newIndexPath = IndexPath(row: meals.count, section: 0)
        tableView.insertRows(at: [newIndexPath], with: .bottom)
    }
}

となります。少し複雑なので1つずつ順番に説明していきます。

どこに定義しても良いと思いますが、公式ページではファイルの一番下(クラス定義最後の}の手前)に定義しています。IBAction attribute(@IBAction)は必須なので、忘れずにメソッドの頭に付けておきます。

アクションメソッドunwindToMealList(_:)はパラメータとしてsegueを取ります。

@IBAction func unwindToMealList(_ sender: UIStoryboardSegue) {

Segueの型はUIStoryboardSegueで、名前はsenderです。

SegueからMealオブジェクトを取り出す

SegueからMealオブジェクトを取り出すのは2段階ステップです。メソッドunwindToMealList(_:)の1行目のif文を見ると分かりますが、

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

という形になっており、これを2つのif文に分割すると

if let source = sender.source as? MealViewController {
    if let meal = source.meal {

となります。

2つの構文はどちらもoptional bindingで、上記のプログラムを日本語にすると

  • Segue(sender)の持つsource view controller(sender.source)がMealViewControllerにダウンキャストできればsourceに割り当てる
  • 取り出したsourceが持つMealオブジェクト(source.meal)がnilじゃなければmealに割り当てる

という感じです。

Segueが持っているsource view controller(source)の型はUIViewControllerなので、実際の型(今回はMealViewController)にダウンキャストする必要があります。また、if文の条件判定時に割り当てたsourcemealは、どちらもif文の中でのみ有効なローカル定数です。したがって、これらをif文の外に持ち出すことは出来ません。

新しいMealオブジェクトを挿入するテーブルの位置を計算する

テーブルビューの位置(行とセクション)を指定するのに、IndexPathという構造体を使います。これは前回Part7でも出てきました。

let newIndexPath = IndexPath(row: meals.count, section: 0)

1つ目のパラメータが行数で、2つ目がセクション数です。今セクションは1つしかないので0で固定です。新しいMealオブジェクトを挿入する位置は、既存の配列(Arrays)の最後尾ですので、要素数(meals.count)をインデックスとして指定しています。

具体的に考えると分かりやすいですが、例えば5つの要素を持つ配列に新しく1つ要素を加える時、加えた6つ目のインデックスは5(0から数えた場合)で、これは既存の配列の要素数(count)と同じです。

Mealオブジェクトを配列に入れる

Segueから取り出したMealオブジェクト(meal)を、あらかじめ用意しておいた配列mealsに追加します。

// Add a new meal
meals.append(meal)

append(_:)は既存の配列の最後尾に要素を追加するメソッドです。

テーブルビューに新しい行を追加する

最後にテーブルビューに新しい行を追加します。

tableView.insertRows(at: [newIndexPath], with: .bottom)

1つ目のパラメータは追加したいインデックス(型は配列要素)、2つ目のパラメータはUITableViewRowAnimationという列挙型(enumerations)で、新しい行を追加(削除)する時のアニメーション効果を指定するものです。今回指定しているbottomというのは、挿入する行を下からスライドイン(削除する時は下方向にスライドアウト)するアニメーションを指定しています。

後でメソッドを再編集して、もっと高度な機能を追加するようですが、ここでは一旦実装完了ということにして先に進みます。

SaveボタンとアクションメソッドunwindToMealList(_:)を繋ぐ

次は、unwind segueをストーリーボード上で作って、「Save」ボタンをunwindToMealList(_:)と繋ぎます。

Xcode: SaveボタンとExitを繋ぐ
ストーリーボードを開いて、料理用sceneで「Save」ボタンから「Exit」(scene dockの一番右端に設置されているアイコン)へcontrol+ドラッグします(上の画像参照)。アイコンの上にポインタをドラッグすると「Exit」と表示されるので、簡単に見分けられると思います。

Xcode: Action segueでunwindToMealListを設定
「Save」ボタンと「Exit」を繋ぐと「Action Segue」と書かれたメニューが表示されるので、unwindToMealList:を選択します(上の画像参照)。これで「Save」ボタンをタップすると、料理リストsceneに戻りつつ、unwindToMealList(_:)メソッドを実行するようになります。

試しにシミュレータを起動して、料理リストから右上のプラス+ボタンをタップし、適当に料理名、写真、評価を入力して「Save」をタップすると、

Xcode: 新規料理を追加
確かに新しい料理(写真は適当)がリストに反映されているのが分かります。

名前が空白の場合はSaveボタンを無効にする | Disable Saving When the User Doesn’t Enter an Item Name

Mealオブジェクトの初期化子を思い出すと分かりますが、

// Initialization should fail if there is no name or if the rating is negative
if name.isEmpty || rating < 0 {
    return nil
}

のように

名前が空白、または評価値が負の場合はnil

という条件が入っています。したがって、もしユーザが新しい料理の名前をセットせず空白のまま「Save」ボタンをタップすると、Mealオブジェクトが作られず(nil)、料理用リストにも追加されないため、「Save」ボタンを押したのに何も起こらない、ということになります。

これだとアプリの挙動としてマズイので、名前が空白の場合はそもそも「Save」ボタンを無効(タップ出来ないよう)にします。「Save」ボタンを有効(タップ可能)にする条件として、

  • 名前がちゃんと入力されていること
  • 文字入力時に使ったキーボードが非表示になったこと

の2つを確認します。

Saveボタンを無効にする

MealViewController.swiftを開いて、// MARK: UITextFieldDelegateセクションに移動します(function menuを使えば素早く移動出来ます)。新たにUITextFieldDelegateのメソッド

func textFieldDidBeginEditing(_ textField: UITextField) {
    // Disable the Save button while editing
    saveButton.isEnabled = false
}

を追加します。textFieldDidBeginEditing(_:)は、テキストフィールドの編集開始時またはキーボードが表示された時に呼び出されるメソッドのようです。メソッド内部ではsaveButtonのプロパティisEnabledfalseをセットしています。これでテキストフィールドの編集中は「Save」ボタンが無効になりました。

Saveボタンを有効にするメソッドcheckValidMealName()

次に、先程実装したtextFieldDidBeginEditing(_:)の下に、

func checkValidMealName() {
    // Disable the Save button if the text field is empty
    let text = nameTextField.text ?? ""
    saveButton.isEnabled = !text.isEmpty
}

というメソッドを追加します。これは「Save」ボタンを有効にするための補助メソッドで、次のセクションでこれを別のメソッドに実装します。

このメソッドの中身では、先ずnameTextFieldからテキストを取り出し、

let text = nameTextField.text ?? ""

そのテキストが空白文字でなければ「Save」ボタンを有効化する

saveButton.isEnabled = !text.isEmpty

という処理を実行しています。

最後のコードがややこしいように見えますが、if文に変換すると

if text.isEmpty {
    // 空白文字(isEmpty = true)ならfalse
    saveButton.isEnabled = false
} else {
    // 空白文字でない(isEmpty = false)ならtrue
    saveButton.isEnabled = true
}

という感じで、結局isEnabledの真偽は常にisEmptyの逆になるので、saveButton.isEnabled = !text.isEmptyという形で簡単にまとめられています。

Saveボタンをどこで有効化するか?

先程作った補助メソッドcheckValidMealName()を、2つのメソッド

  • textFieldDidEndEditing(_:)
  • viewDidLoad()

に実装します。

textFieldDidEndEditing(_:)メソッドを

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

このように編集します。checkValidMealName()で、テキストフィールドにちゃんと文字が入力されていれば「Save」ボタンを有効化します。2行目のコードでは、テキストフィールドに入力した料理名をnavigation barのタイトルに設定しています。

viewDidLoad()には、メソッドの一番最後にcheckValidMealName()を追加します。

override func viewDidLoad() {
    super.viewDidLoad()

    // Handle the text field’s user input through delegate callbacks.
    nameTextField.delegate = self

    // Enable the Save button only if the text field has a valid Meal name.
    checkValidMealName()
}

シミュレータでSaveボタンの挙動を確認

この時点でビルドして、シミュレータを起動します。料理リストからプラスボタンをタップして新規料理追加画面に遷移します。

Xcode: 料理名を入力するまではSaveボタンを無効にする
テキストフィールドにちゃんと名前を入力しなければ、「Save」ボタンが有効化されないことを確認します。上の画像のように、デフォルトでは右上の「Save」ボタンが無効(灰色)になっているのが分かります。料理名を確定する(キーボードが非表示になる)と、navigation barのタイトルが料理名と同じになることも確認しておきます。

新規料理の追加をキャンセルする | Cancel a New Meal Addition

「Save」ボタンだけじゃなく「Cancel」ボタンにもアクションメソッドを実装します。ボタンの名前から役割は容易に想像出来ますが、

「Cancel」ボタンをタップすると、入力した内容等を何も保存すること無く料理リストsceneに戻る

という挙動を実装します。

ストーリーボードを開き、assistant editor表示にして、MealViewControllerをassistant editorに表示します。これまでアクションメソッドを追加してきた場合と同様に、ストーリーボード上の「Cancel」ボタンからcontrol+ドラッグで、MealViewController.swiftの// Navigationコメント下にドロップします。

Xcode: Cancelボタンにアクションメソッドを実装
ダイアログが表示されるので、各項目に

  • Connection: Action
  • Name: cancel
  • Type: UIBarButtonItem

とセット、それ以外はデフォルト設定のままで、「Connect」をクリックします(上の画像参照)。

そうすると

@IBAction func cancel(_ sender: UIBarButtonItem) {
}

というメソッドがMealViewControllerに追加されるので、これを

@IBAction func cancel(_ sender: UIBarButtonItem) {
    dismiss(animated: true, completion: nil)
}

と編集します。メソッドdismiss(animated:completion:)は、今表示されている画面を閉じるメソッドなので、「Cancel」ボタンタップによって料理追加画面が閉じ、元々表示されている料理用リストが表示されます。

最後に、シミュレータを起動して「Cancel」ボタンが動くかどうか確認しておきます。

まとめ

今回はnavigation controllerやsegueを導入し、画面遷移自体の実装と、view controller間のデータのやり取りを学びました。次回は、料理リストに表示されている既存のエントリーを編集したり削除する機能を実装します。

「Start Developing iOS Apps (Swift)」でFoodTracker Appを実際に作ってみる その9[編集機能を実装する]
公式にある「Start Developing iOS Apps (Swift)」を使って実際にFoodTracker Appを作ってみる、と...
スポンサーリンク
広告1
広告1

シェアする

フォローする