Swiftで遊ぼう! on Hatena

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

Swiftで遊ぼう! - 367 - Developing iOS Apps (Swift) Work with View Controllers

2017年3月26日:Swift 3に対応*1

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

developer.apple.com

今回学ぶべきポイントをリストアップすると次の通りです。

  • View Controllerのライフサイクルと、viewDidLoad、viewWillAppear、viewDidAppearなどのコールバックがいつ呼ばれるるの理解
  • View Controller間でデーターの受け渡し
  • View Controllerを消す
  • イベント駆動させるジェスチャー認識を使う
  • UIView/UIControlの継承に基づいたオブジェクトの挙動
  • プロジェクトに画像を組み込むためにアセットカタログの使用

知らなければならない項目がまだまだ沢山あります。UI関連の基本的な事も網羅できていないので、かなり勉強になりました。1年以上勉強を続けてこんな基本的な事を知らないのかとお叱りをうけそうですが、1年前はOOPという概念も知らなかった人間が、このチュートリアルのコードを読めるようになっているので少しずつですが進歩してると信じています(^^;)

次の1年はiOSデベロップメントの飛躍の年にしたいです。iOSフレームワークの使い方にも慣れ、よりプログラミングの本質となるアルゴリズムを考えていきたいです。

さて、今日のポイントを最初からみていきます。

ViewControllerのライフサイクルに関してですが、これは以前少し検証しました。

yataiblue.hatenablog.com

次にMVC(Model-View-Controller)の説明が入ります。これは非常に重要な概念なのでしっかり理解する必要があります。

yataiblue.hatenablog.com

上のリンク先を読んでもMVCの説明がまとまっていないので、「MVCは何だ?」と感じる人もいらっしゃるでしょう。この概念はiOS開発の基本概念になるため、いつか内容をまとめていきます。

2050年公開予定:「オヤジでも解るMVCデザインとデリゲーションの重要な役割」

次は、ImageViewをストリーボードに設置していきます。

オブジェクト・ライブラリからimage viewとタイプして「Image View」をストーリーボードにドラッグ&ドロップするのですが、「Stack View」に設置している「Set Default Label Text」ボタンの下に設置します。

f:id:yataiblue:20170326091248j:plain

Image Viewが選択されている状態で、サイズ・インスペクタを開きます。色々な属性リストの下の方に「Intrinsic Size」があるので「Placeholder」を選びます。空のイメージの場合、コンテンツサイズが0になって場所を確保できなくなるため、その下のサイズ「Width」と「Height」を「320」にします。

f:id:yataiblue:20170326091319j:plain

まだImage Viewが選択されていますが、そのままオートレイアウト・ボタンの「Pin」メニューを選びます。画像の縦横比を一定にするために「Aspect Ratio」のチェックマークを入れて「Add 1 Constraint」を加えます。

f:id:yataiblue:20170326091423j:plain

もう一つ重要な属性を加えないといけません。Image Viewが選択された状態で、アトリビュート・インスペクタを開いて、「Interaction」ラベルのついている項目、「Use Interaction Enabledチェックボックスを選択します。

f:id:yataiblue:20170326091615j:plain

これでImage Viewはユーザーから加えられるイベントを受ける準備ができました。ここまでで次のようになりました。

f:id:yataiblue:20170326091657j:plain

次はUIImageViewにデフォルトでImageをセットする方法ですが、プロジェクト・ナビゲータのAssets.xcassetsを使います。

これも既に説明はしています。

yataiblue.hatenablog.com

下のような「640×640」ピクセルの「png」イメージを用意します*2

f:id:yataiblue:20151018162149j:plain

まずプロジェクト・ナビゲータのAssets.xcassetsを選択して、下部の「+」ボタンを押してメニューから「New Image Set」を選ぶと新しいイメージセット「Image」ができます。

f:id:yataiblue:20170327085510j:plain

「Image」という名前をダブルクリックして「defaultPhoto」に改名して、「2×」ホルダーに用意したpngイメージをドラッグ&ドロップします。

トーリーボードに設置しているUIImageViewを選択して、アトリビュート・インスペクタにある「Image」ラベルの項目から「defaultPhoto」を選択します。

f:id:yataiblue:20170327085416j:plain

トーリーボード上のImage Viewに画像が描画されImageViewのインスタンス化ができました。しかし、このままではViewControllerで制御することができません。

ImageViewをコードで繋ぐ

アシスタント・エディタを開きます。Image Viewから「Ctrl + ドラッグ」して、「// MARK: Properties」以下の一番下でリリースするとIBOutletを作成のためのダイアログが出現します。名前を「photoImageView」にしてコネクトすると次のコードができます。

@IBOutlet weak var photoImageView: UIImageView!

これでコードで表示内容を制御できるようになりました。しかし、「Ctrl + ドラッグ」だけではActionの設定ができません。同じようなUIエレメントのUITextFieldでActionの設定ができるのにどうしてUIImageViewオブジェクトはActionの設定ができないのでしょう?

初心者ならこういう素朴な疑問が生じます。クラス継承を考えなければ理解できないんです。UITextFieldもUIImageViewも同じUIViewクラスのサブクラスになりますが、UITextFieldはUIViewのサブクラスUIControlのサブクラスです。このUIControlを継承していれば無条件にユーザーのイベントに応えることができますが、継承していないUIImageViewの場合はちょっとアプローチを変更して取り組む必要があります。

UIControlクラスの機能を使用したいときにどうすればいいのか? UIControlクラスでUIButtonを継承しなおす? なんて馬鹿なことはできません。異なるクラスに共通機能を持たせるという考え方はOOPの「ポリモーフィズム」に当たります。ということはプロトコールの使用です。

Gesture Recognizerを実装したいので、必要な機能だけ実装していきます。今回はタップ機能を利用したいので、UITapGestureRecognizerを使います。オブジェクト・ライブラリの検索ボックスにtap gestureとタイプすると「Tap Gesture Recognizer」が出てくるのでドラッグしてストーリーボード上のUIImageViewの上でドロップします。

すると「Scene Dock」に「Tap Gesture Recognizer」が組み込まれます。

f:id:yataiblue:20170327101446j:plain

なんとなく違和感を感じます。UITextFieldをストリーボードにドラッグ&ドロップしても「Scene Dock」に「Tap Gesture Recognizer」は出現しないからです。UITextFiledには、タップ機能を実装する機能が既に備わっているのになぜでしょう? 

私なりに考えてみると、UIControlをスーパークラスとして保持しているからでしょう。タップ・ジェスチャーへの対応はデリゲーションを使うので、このUITextFieldクラス自身がUITextFieldDelegateを持っています。そのためUITextFieldをViewControllerに「Ctrl + ドラッグ」して繋ぐだけで、ジェスチャ機能の使用ができるんです。

しかし、UIImageViewは、そもそもジェスチャーへ対応するスーパークラスを持っていないので、デリゲーション用のプロトコールは用意されていません。独立したUIGestureRecognizerクラスのプロトコールを組み込む必要があり、オブジェクトをphotoImageViewに繋げたとしても、ストリーボード上のジェネリックなUIImageViewにControl機能は加わりません。「Scene Dock」にTap Gestureを加わることで、このプロトコールが加わったということを明示させるXcode開発環境ならではの表現法でないかと考えます。

Xcodeで明示的に表現されたのでViewControllerクラスでプロトコールの準拠*3をさせなくてもいいのです。

次にすべきことは「Scene Dock」にあるTap Gesture RecognizerをViewControllerの「// MARK: Actions」の下に「Ctrl + ドラッグ」することです。これでお馴染みのダイアログが出現します。「@IBAction」を選択して、名前に「selectImageFromPhotoLibrary」を入力して、Typeを「UITapGestureRecognizer」に変更してconnectボタンを押します。

@IBAction func selectPhotoFromImageLibrary(_ sender: 
                               UITapGestureRecognizer) {

}

これでTapジェスチャーが使えるようになります*4。次に、タップして何をしたいのでしょう?

タップに反応するImage Pickerをつくる

PhotoLibraryから既存画像を呼び込んでアプリで使用するか、アプリからPhotoLibraryに画像を保存する機能を加えていきたいのです。この機能はiOSフレームワーク(API)で用意されています。それがUIViewControllerのサブクラス、UIImagePickerControllerです。

UIImagePickerControllerの標準機能を使ってPhotoLibraryの写真を取りこみます。

UIImagePickerControllerを使うために、ViewControllerの切り替え作業が必要になります。マルチプルViewControllerの実装法も今までに説明したように複数あります。本来ならストリーボードを使った切りかえが一般的なのですが、このデモでViewControllerの切り替えを機能を実装していくとチュートリアルの本筋から外れるので興味がある人は下のリンクを読んでください。

yataiblue.hatenablog.com

まず重要なのは、ViewControllerの切りかえではなく、UIImagePickerControllerのメソッドをViewControllerで使うための準備です。これはUITextFieldの機能を実装した時と同様です。UIImagePicekerControllerDelegateを準拠します。

もう一度プロトコールとデリゲーションの関係を説明します。

PhotoLibraryからViewControlleを介してImage Viewrに画像(データ)を渡すことを考えます。

デリゲーションという考え方は、MVCのMC間やVC間で使用するだけでなく、複数のMVC間でも使用します。ImagePickerControllerとコンテンツビューのViewControllerでデリゲーションを使ってデータをやり取りするんです。

f:id:yataiblue:20150701165553j:plain

上記にコード実装の概略を示しましたが、これは以前説明した6ステップ実装法と同じです。

yataiblue.hatenablog.com

6ステップ実装法を見ながらコーディングをしていきます。

  • ステップ1:デリゲーションさせたい機能をプロトコールとして用意するのですが、今回UIImagePickerDlegateというプロトコールを利用するのでコードする必要はありませんが、プロトコールに用意されているメソッドに、Required(必須)とOptional(オプション)メソッドがあり、Requiredメソッドは必ず実装する必要があります。利用できるドキュメントを調べてプロトコールで実装しなければならないメソッドを知る必要があります*5
  • ステップ2:これもコードする必要はありませんが、iOS API使用時に用意されているデリゲーション用のプロパティは、「delegate」と「dataSource」です。
  • ステップ3:自分でプロトコールを設計する時に一番重要かもしれません。しかし、利用する時にカプセル化して見えない部分になります。要するに利用時に知る必要のないことです。
  • ステップ4:利用時に必ず必要なステップです。デリゲーションを受けるデリゲートオブジェクトはViewControllerなので、UIImagePickerControllerの機能を受けるのでプロトコールに準拠する必要があります。

UIImagePickerControllerDelegateプロトコールだけ準拠させるのかと思ったら...

class ViewController: UIViewController, UITextFieldDelegate, 
                          UIImagePickerControllerDelegate, 
                                  UINavigationControllerDelegate {

あれ? なんでUINavigationControllerDelegateプロトコールも準拠させないといけないのか! 初心者のオヤジはこういうところで慌てふためくという訳です(^^;) 

UIImagePickerControllerの継承関係を見ると、UIImagePickerController: UINavigationController: UIViewControllerになっています、キャンセルボタンを押して元のViewControllerに戻る機能はNavigationControllerの機能を使っているので、NavigationControllerDelegateにも準拠する必要があるのかもしれません。少し確証が持てませんが(^^;)

  • ステップ5:ViewController自身をUIImagePickerControllerクラスのプロパティ(delegate)として持たせてImagePickerControllerのメソッドを使えるようにします。では、どこでこのステップを実行するのか?

photoImageViewをタップしたときにUIImagePickerControllerが生成させたいので、selectImageFromPhotoLibrary()メソッドの中です。
このメソッドを考えます。
タップされたときにnameTextFieldが入力状態かもしれません。このfirstResponderをまず消します。

nameTextField.resignFirstResponder()

次にUIImagePickerViewControllerをインスタンス化して、ソースタイプを「.photoLibrary」にします。

let imagePickerController = UIImagePickerController()
imagePickerController.sourceType = .photoLibrary

後にimagePickerVontrollerのdelegateプロパティに、ViewController自身を保持させます。

imagePickerController.delegate = self

最後にImagePickerContollerに切り替えます。

presentViewController(imagePickerController, 
             animated: true, completion: nil)

コードをまとめて書くと次のようになります。

@IBAction func selectPhotoFromImageLibrary(_ sender: 
                               UITapGestureRecognizer) {
        
    nameTextField.resignFirstResponder()
        
    let imagePickerController = UIImagePickerController()
    imagePickerController.sourceType = .photoLibrary
        
    imagePickerController.delegate = self
        
    present(imagePickerController, animated: true, completion: nil)
}
  • ステップ6:このプロトコールで実装しなければならないメソッドを実装します。
  1. func imagePickerControllerDidCancel(_ picker: UIImagePickerController)
  2. func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any])

最初のメソッドは、キャンセルボタンを押したときの挙動です。

func imagePickerControllerDidCancel(_ picker: 
                            UIImagePickerController) {
    dismiss(animated: true, completion: nil)
}
    

いま見えているViewControllerを消してしまうので下にあるViewController(元のViewController)に戻ります。
そして次は、写真を選ぶという動作が加わります。選んだ画像をどうするのかコードするところなので、次のように変更します。

func imagePickerController(_ picker: UIImagePickerController, 
            didFinishPickingMediaWithInfo info: [String : Any]) {
    guard let selectedImage = 
        info[UIImagePickerControllerOriginalImage] as? 
                                                 UIImage else {
        fatalError("Expected a dictionary containing an image, 
                         but was provided the following: \(info)")
    }
        
    photoImageView.image = selectedImage
        
    dismiss(animated: true, completion: nil)
}

これでデリゲーションが完成しました。ラン(Cmd + R)してみましょう。イメージビューをクリックしたら...

f:id:yataiblue:20170327124804j:plain

これを「SIGABRT 」シグナルというようです*6。これだけをみても分からないのでエラーメッセージが出ていコンソールウインドウを見ます。

f:id:yataiblue:20170327125053j:plain

iOS 10になってセキュリティが強化されています。アプリ間の情報のやりとりにもロックがかかっている要なので、許可を与える必要があります。アプリの許可を与える場合「Info.plistを編集するんです。

一番下にある「Add」ボタンをクリックして「Privacy - Photo Library Usage Description」を選択します。

f:id:yataiblue:20170327125843j:plain

次にString型の値「Allows you to add photos to your meals.」を入力します。

f:id:yataiblue:20170327130001j:plain

これで準備完了です。ランしてみます。今度はImage Viewをタップしてもエラーが出ません。先ほど設定したStringデータが表示されました。

f:id:yataiblue:20170327140518j:plain

許可を与えれば写真の登録もできました。

f:id:yataiblue:20170327140609j:plain

次に進む→Swiftで遊ぼう! - 371 - Developing iOS Apps (Swift) Implement a Custom Control - Swiftで遊ぼう! on Hatena

*1:2016年1月12日改訂:コメントで指摘があった箇所は修正しました。あんどうさん、ありがとうございました。2015年10月17日改訂:環境環境:Xcode7.1Swift2.1

*2:本当は320ピクセルの2倍、3倍を用意した方がいいのですが、デモなのでiPhone6/6sPlusを対象外にします

*3:クラス宣言のところで「,」を付けてプロトコールを記載するやり方。

*4:私はハマってしまいましたが、必ずphotoImageViewが選択された状態でアトリビュート・インスペクタにある「Interaciton」の「User Interaction Enabled」にチェックマークを入れて下さい。でないとタップに反応しません。

*5:この作業は気が遠くなります。しかし、ベテランiOSデベロッパーは、こういう知識がきっと豊富なんでしょう。

*6:まだデバッグの知識が乏しくよく分かりません。