リブートキャンプ by Swift 目次

 

 

 

 

 まさかの癌宣告で前回から1か月以上あいちゃったけど、予告通りストーリーボードの話。

 

 

 ちなみに抗がん剤治療の副作用で、冷たいものに触ると指先がビリビリします。なので部屋を暖かくしてからMacBook Proのトラックパッドを触るようにしてる。

 

 

 

 

 

ストーリーボード

 

 

 ストーリーボードってなんだというと、「スクリプトエンジンっぽい」で作った辞書を使った脚本オブジェクトみたいなもんです。

 

 

 あの時は、アプリで表示させるビューやその動作を辞書に脚本として記述して、それを読み出して実行するプログラムを作ったわけですが、ストーリーボードも同じように、画面上にどんなビューを出したり、タップを通知したりということを記述できたりします。

 

 

 で、こいつはSwiftのプログラムコードのように、テキストエディタを使って記述するのではなく、Interface BuilderというXcodeに組み込まれた専用のエディタを使って記述するようになってます。

 

 

 でもって、その記述したものをファイルとして保存して任意のタイミングで読み込んで利用する。

 

 

 

 

 

.storyboardファイル

 

 

 それが.storyboardって拡張子のファイルなんだけど、実はXcodeのiPhoneアプリ用プロジェクトのテンプレートは全て、この.storyboardファイルを利用するようになってます。

 

 

 それがこのMain.storyboardってファイル。これまで作ったプロジェクトにも入ってて、ナビゲーションエリアのMain.storyboardを選ぶと表示されます。

 

 

 

 

 

 

 

 

 

 

サンプル:

 

 

http://tetera.jp/xcc/book-sample/storyboard.zip

 

 

 

 

 

 エディタエリアに表示されてる画面はInterface Builderのもので、左の領域をoutlineビュー、右の領域をcanvasビューと呼びます。

 

 

 outlineでストーリーボードに記述されているオブジェクトの一覧を階層構造で見せ、canvasで、それらのオブジェクトが実際のiPhoneの画面をどう表示するかを見せてるわけですよ。

 

 

 でもって、どっちのビューでもオブジェクト単位で選択ができるようになってて、互いに連動してハイライトされるようになってもいます。

 

 

 

 

 

 

 

 

 

 

 

 

 

 で、そのまた右のユーティリティエリアでは、選択されたオブジェクトの情報が表示されるという具合。

 

 

 

 

 

 

 



 んじゃ、そのMain.storyboard様には何が記述されてるかというと、ViewControllerオブジェクトの作成なんですな。

 そういうわけで、今回のプロジェクトのMain.storyboardを選んだエディタエリアには、ViewControllerオブジェクトと、そいつが持つUIView(self.view)が表示されていることになります。

 

 

 本当にそうなのか疑う人はoutlineでView Controller項目を選んで、ユーティリティエリアでIdentityインスペクタ画面を表示させてみましょう。

 

 

 classのところがViewControllerってなってることが確認できるでしょう。

 

 

 

 

 

 

 

 

 

 

 以前「こんにちは世界」で触れ、これまで説明していなかった

 

 

 

 

 

 このアプリは、起動するとViewControllerオブジェクトが1つ用意されるようになってるんですよ

 

 

 

 

 

 に対して当然湧き上がるであろう疑問、「じゃあ、なんで、アプリは起動するとViewControllerオブジェクトを用意するのか?」の仕組みがこれです。

 

 

 

 

 

 答え:Main.storyboardでViewControllerを作れって指定されてるから。

 

 

 

 

 

 でもって、プロジェクトでアプリ起動時にMain.storyboard読み込んで使えって指定してるから。

 

 

 ちなみに、どこでMain.storyboardが利用されるように指定されてるかというと

 

 

 ここ↓

 

 

 

 

 

 

 

 

 

 

 ここに指定された名前と拡張子.storyboardを組み合わせたファイルが、アプリ起動時に読み込まれて利用されるようになっている。

 

 

 今回ならMain.storyboardファイルがアプリ起動時に読み込まれて利用されるわけですよ。

 

 

 なので、例えば、このMain.storyboardでViewControllerじゃなく地図画面用であるMapViewControllerを作るように記述すれば、アプリ起動時に地図画面を表示するようにだってできます。

 

 

 そこらへんは追い追いやるとして、とりあえずは手馴しとしてViewController.swiftのviewDidLoadメソッドで記述した、タップの見張りと通知なんかをMain.storyboardで記述してみることにしましょう。

 

 

 ↓こいつね。

 

 

 

class ViewController: UIViewController,

    ・・・



    override func viewDidLoad() {

        super.viewDidLoad()

        self.view.addGestureRecognizer(UITapGestureRecognizer(

           target: self, action: #selector(showAsView)))

    }



 

 

 

 UITapGestureRecognizerの作成とself.viewへの登録、そしてtargetとactionの設定をストーリーボードで記述するわけです。

 

 

 

 

 まずはUITapGestureRecognizerの作成とself.viewへの登録から。

 

 

 

 

 どうやるかというと、ユーティリティエリア下にあるライブラリエリアに表示されてるTap Gesture Recognizerという項目を、エディタエリアのcanvasビューのViewにドラッグ&ドロップしてやります。

 

 

 

 

 

 

 

 

 

 1、ライブラリエリアでObjectライブラリを選択してオブジェクト群を表示させる。

 

 

 

 

 2、表示された項目からTap Gesture Recognizer項目を探し、これをcanvasビュー上のViewにドラッグ&ドロップする。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 Viewはoutlineの階層を見ると察しがつくと思うけど、ViewControllerの持ってるUIView、つまりself.viewを意味してます。これで

 

 

 

 

 

 

 

 

 

  self.view.addGestureRecognizer(UITapGestureRecognizer()))

 

 

 

 

 

 

 

 

 

と同じことを記述したことになるんですな。

 

 

 

 

 ちなみに、objectライブラリのTap Gesture Recognizer項をダブルクリックすると、どういうクラスのオブジェクトかわかるようになってます。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 で、これでUITapGestureRecognizer作ってself.viewに登録するまではできたわけですが、これだけだとtarget: self, action: #selector(showAsView)部分が記述できてないわけですよ。

 

 

 

 

 これをどうするか?

 

 

 

 

 ↓これね。

 

 

 

 

 

 

 

 

 

 

        self.view.addGestureRecognizer(UITapGestureRecognizer(

           target: self, action: #selector(showAsView)))

 

 

 

 

 まず必要なのは、ViewController.swiftにテキストエディタを使って記述したshowAsViewメソッドを、ストーリーボードを記述するInterface Builderに認識させること。

 

 

 

 

 

 

 

 

 

@IBAction

 

 

 

 

 そのために一度、ナビゲーションエリアでViewController.swiftを選び、テキストエディタでViewController.swiftのshowAsViewメソッドの前についてる@objc@IBActionに書き換えます。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

class ViewController: UIViewController,

   ・・・

    @objc func showAsView() {

 

 

 

 

 

 ↓
 

class ViewController: UIViewController,

   ・・・

    @IBAction func showAsView() {

 

 

 

 

 

 

 

 

 

 この@IBActionこそが、Interface Builderに対して、このメソッドはターゲットアクションデザインパターンのアクションとして指定可能なメソッドだよって知らせるキーワードなわけですな。ちなみに@IBActionのついたメソッドは@objc指定であることも保証します。

 

 

 

 

 ターゲットアクションデザインパターンや@objcが何かわからん人は「タップしてドン」と前回の「そして委譲へ」を読みましょう。

 

 

 

 

 書き直したら再びナビゲーションエリアのMain.storyboardを選び、outlineビューのView Controller項目を選択し、ユーティリティエリアのConnectionsインスペクタを表示させてみる。

 

 

 

 

 そうすると下の方のRecieved ActionsのグループにshowAsViewの項目が出現するんですな。これがInterface BuilderがshowAsViewメソッドをターゲットアクションデザインパターンのアクションとして指定可能なメソッドとして認識した証拠。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 で、これが出ると、このshowAsViewの項目の右にある◎からドラッグで線を引き出して、outlineビューのTap Gesture Recognizer項目に繋ぐことができるようになります。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 でもって、上の図のようになったところでマウスのボタンを放すと、これでUITapGestureRecognizerオブジェクトのtargetとしてViewControllerが、actionとしてshowAsViewメソッドが設定されることになるわけです。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 つまり、これで

 

 

 

 

 

 

 

 

 

 

        self.view.addGestureRecognizer(UITapGestureRecognizer(

          target: self, action: #selector(showAsView)))

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 と同じことが、ストーリーボードに記述できたことになるんですな。

 

 

 

 

 なので、ViewController.swiftのviewDidLoadメソッドで記述したUITapGestureRecognizerオブジェクトの作成とself.viewへの登録は不要になりました。そのままだとストーリーボード、viewDidLoadメソッドの両方でUITapGestureRecognizerオブジェクトを作っちゃってself.viewへ多重登録することになるんで、viewDidLoadメソッド側は消しちゃってください。

 

 

 

 

 ↓こいつね。

 

 

 

 

 

class ViewController: UIViewController,

    ・・・



    override func viewDidLoad() {

        super.viewDidLoad()

        self.view.addGestureRecognizer(UITapGestureRecognizer(

           target: self, action: #selector(showAsView)))

    }



 

 

 

 

サンプル:

 

 

 

 

http://tetera.jp/xcc/book-sample/storyboard-2.zip

 

 

 

 

 

 

 

 

 

 消したらRun!

 

 

 

 

 viewDidLoadメソッドの処理が無いのに、ちゃんと画面タップで地図画面が表示されるはず。

 

 

 

 

 グレート。

 

 

 

 

 

 

 

 

 

 というか、ぶっちゃけ、これくらいならviewDidLoadメソッドにテキストエディタでプログラムコード直接記述する方がよっぽど早いんすけどね。

 

 

 

 

 

 

 

 

 

 違うから!

 

 

 

 

 本来、ストーリーボードを使うメリットは、View上にボタンといったサブビューをAuto Layout使って配置する時に出てくるんだから。

 

 

 

 

 

 

 

 

 

 というわけで、次回は、ストーリーボード上でボタンを配置してタップしたら地図画面が表示されるようにしてみる。

 

 

 

 

 

 

 

AD

リブートキャンプ by Swift 目次

リブートキャンプ by Swift 目次

 

1、ラノベっぽい

 

2、ラノベっぽくない

 

3、走れマイアプリちゃん

 

4、IBMのSwiftサイト

 

5、ど〜こ〜で〜もプログラミングぅ

 

6、ループの話

 

7、ifの話

 if文、Int型からDouble型への変換

 

8、そしてiPhoneアプリへ

 

9、「こんにちは世界」

 

10、こんにちはデベロッパの世界

 

11、スクールカーストとかヒエラルキとか

 アプリで画像ファイルを利用する

 

12、または私は如何にして心配するのを止めて…

 

13、色々と脱線

 クラスオブジェクト

 

14、重箱の隅をつつくようにネチネチと進めてみる

 プロトコル、プロトコルの採用、autoresizingMask、画面レイアウト

 

15、CALayerで完璧

 

16、ダイナミックな文字

 dynamic type対応

 

17、アンラップしてチン♪

 オプショナル型、アンラップ、アンラップ済み、

 

18、ダイナミックなレイアウト

 auto layout

 

19、タップしてドン

 クロージャ、ターゲットアクションデザインパターン、通知、UITapGestureRecognizer

 

20、スクリプトエンジンっぽい

 

21、キャッチして

 Git

 

22、プレゼントせずにモーダル

 新規UI派生クラスファイルの追加

 

23、UIViewの派生でイニシャライザって…

 init、指定イニシャライザ、convenience、required

 

24、UIViewとUIViewController

 UIViewControllerの切り替え

 

25、そして委譲へ

 デリゲートデザインパターン、weak、@objc

 

26、今時はプリビズだよ

 ストーリーボード、Interface Builder

 

27、ストーリーボードでAuto Layoutだ

 ボタン、拘束

AD

リブートキャンプ by Swift 目次

 

 というわけで、画面をUIViewControllerごと切り替えるのが、iOSアプリの流儀なわけですが…

 切り替えた先でユーザーが何かを指定した場合に、どうやってそれを切り替え元に知らせるのか?

 例えば今回なら、表示した地図でユーザーに移動先を選択してもらうって利用形態(格好つけてユースケースと言ってもいい)が考えられるけど

注意)あんま普段からカッコつけてると、自分でも気づかないうちに「EC、ソーシャルメディアとリアルを融合させたオムニチャネル活用により、個々の顧客のニーズに合わせた新たなカスタマー・ エクスペリエンス…」とか喋り出してしまうようになる。

 

 その場合、地図表示担当のMapViewControllerから戻ってきたViewController側は、MapViewControllerコントロール下でユーザーが選んだ目的地をどうやって知ればいいのか?

 

 

 MapViewController側でユーザーがおこなった操作を、ある程度ViewController側も把握したいわけで、これは前に紹介したターゲットアクションデザインパターンと同じくオブジェクト間の連携問題となります。

注意)ターゲットアクションデザインパターンがわからん人は「タップしてドン」を読みなさい。

 こういったときにAppleが推奨してる作法が、デリゲートデザインパターンっす。

 

デリゲートデザインパターン

 ターゲットアクションデザインパターンが、イベントが発生した時用の

 

  連携オブジェクト(ターゲット)

  連携メソッド(アクション)

 

の2つを連携先に登録するやり方だったのに対し、デリゲートデザインパターンは、最初に

 

 イベントA発生時は、連携オブジェクトのメソッドAを呼び出す

 イベントB発生時は、連携オブジェクトのメソッドBを呼び出す

   ・・・

 

といった取り決めをしておいて

 

  連携オブジェクト(今回ならViewController)

 

だけを連携先(今回ならMapViewController)に登録するやり方です。

 この連携オブジェクトのことをデリゲート(delegate:イベントに対する動作を委譲する相手)と呼びます。

 なので、デリゲートとなる連携オブジェクトはあらかじめ取り決められたメソッドを装備してないダメってことになります。

 

 

 

 例えば次のような取り決めをしたならば

 

取り決め:

 MapViewControllerは地図画面でユーザーが目的地をタップしたら

 

  func selectDistination(name:String);

 

 というメソッドで、連携先に目的地の名前を知らせる。

 

MapViewController側から見てデリゲートとなるViewController側ではselectDestination(name:)メソッドを用意しておく必要があるわけです。

class ViewController: UIViewController {
   ・・・
  func selectDestination(name:String) {・・・}
}

 

 でもってMapViewControllerでは、デリゲートを特定するためにViewController型のプロパティを用意し外部から指定可能にしておく。プロパティの名前はなんでもいいんだけど、わざわざ関係のない名前にしてもしょうがないので普通はdelegateって名前にします。

class MapViewController: UIViewController {
    var delegate:ViewController?
  ・・・
}

注意)ViewController後ろの?の意味がわからん人は「アンラップしてチン♪」を読みましょう。

 

 こうしておいてMapViewController作った時に、次のようにViewControllerをdelegateプロパティに設定してやれば

class ViewController: UIViewController {
    ・・・
    @objc func showAsView() {
        let mapViewController = MapViewController()
        mapViewController.delegate = self
        self.present(mapViewController, //  mapViewControllerに切り替え
            animated: true)    //  切り替えはアニメーションさせる
    }

 あとはMapViewController側で地図画面タップされた時に、目的地名を決めてselectDestination(name:)を呼び出してやればいいわけですよ。

class MapViewController: UIViewController {
    var delegate:ViewController?
  ・・・
    @objc func tap(tapGestureRecognizer:UITapGestureRecognizer) {
        let location = tapGestureRecognizer.location(in: self.view)
                   ↓とりあえず、タップ位置を名前にする
        self.delegate?.selectDestination(name:"\(location)") 
    }

 

注意)UITapGestureRecognizerが何かわからん人は「タップしてドン」を読みましょう。

 

 こうするとViewControllerは、MapViewControllerの地図画面上でユーザーが指定した目的地を知らせてもらえることになるわけです。

class ViewController: UIViewController {
   ・・・
    func selectDestination(name:String) {
        print(name)  ←コンソールに名前を出力
    }
}

サンプル:

http://tetera.jp/xcc/book-sample/delegate-pre.zip

 

 

 

 ちなみに

 

     "\(location)"


というように、文字列中に  \( (バックスラッシュとオープンバーレン)と ) (クローズバーレン)で挟んでプロパティ名を書くと、そのプロパティの値が文字列として設定されます。結果として、例えばlocationのx、yに[100, 200]が設定されていたら

 

     "(100.0, 200.0)"

 

て感じの文字列になり、これがコンソールに出力されます。\(プロパティ名)は、どこでも使えるので、こんな感じでもいい

 

     "location は \(location) です"

 

この場合、

 

     "location は(100.0, 200.0)です"

 

という文字列になる。

 

 ただ〜、これだとまるで汎用性がないわけでして…

 MapViewControllerのdelegateプロパティに指定できるのはViewControllerオブジェクトか、その派生オブジェクトしかできないってことになっちゃうんですな。

 で、そこらへんに汎用性を持たせるために、Appleが提唱するデリゲートデザインパターンでは「これこれのイベントが発生したら、連携先オブジェクトの、このメソッドを呼び出す」という取り決めを、プロトコルとして表現しておき

protocol MapViewControllerDelegate {
    func selectDistination(name:String)
}

注意)プロトコルがわからん人は「重箱の隅をつつくようにネチネチと進めてみる」を読みましょう。

 

 連携先は、このプロトコルを採用するものとする。

class ViewController: UIViewController, MapViewControllerDelegate {
   ・・・
    func selectDistination(name:String) {
        print(name)
    }
}

 ということにしたんですな。

 こうすると、MapViewController側はViewControllerではdelegateプロパティとして、ViewControllerじゃなくプロトコルであるMapViewControllerDelegate型を指定することができ

class MapViewController: UIViewController {
    var delegate:MapViewControllerDelegate?
   ・・・

サンプル:

http://tetera.jp/xcc/book-sample/delegate.zip

 

 

 MapViewControllerDelegateを採用したオブジェクトなら、どれでも指定可能になるわけです。

 なので、例えば次のようにして目的地選択専用のオブジェクトを用意することだってできる。

//  MapViewControllerDelegateを採用した別クラス
class AltDelegate : MapViewControllerDelegate {
    func selectDestination(name:String) {
        print("タップ位置は" + name + "です")
    }
}

class ViewController: UIViewController, MapViewControllerDelegate {
   ・・・
    //  別のデリゲートオブジェクト
    let altDelegate = AltDelegate()
    
    @objc func showAsView() {
        let mapViewController = MapViewController()
        mapViewController.delegate = altDelegate
        self.present(mapViewController, //  mapViewControllerに切り替え
            animated: true)    //  切り替えはアニメーションさせる
    }

 self、altDelegate、どちらでもdelegateに設定可能。

 

サンプル:

http://tetera.jp/xcc/book-sample/delegate-alt.zip

 

 

 こんな感じでプロトコルとその採用を導入することで、かなり汎用性が出てくるわけです。これがデリゲートデザインパターン。

 

 あと、慣例としてデリゲート側プロパティにはweakをつけます。

class MapViewController: UIViewController {
    weak var delegate:MapViewControllerDelegate?

 これは、自分はdelegateに指定されたオブジェクトを所有しないという意思表示です。逆に言えばweakをつけてない場合は、delegateに指定されたオブジェクトはMapViewControllerオブジェクトに所有されることになり、MapViewControllerオブジェクトが存在する限り、勝手に消滅したりはできなくなります。

 これ自体は、MapViewControllerにとってありがたい事なんだけど、一点だけ懸念事項があって…

 それはdelegateで指定されるオブジェクトが、MapViewControllerオブジェクト側を同じようにプロパティで所有してた場合です。

 こうなると互いに所有しあって、いつまでもオブジェクトが消滅できなくなる可能性が出てくるんですな。

 

 

 オブジェクトは用が済めば静かに退場するのが礼儀というものなんですが、互いに所有しあってると、どちらも消滅しようとしても消滅できなくなるんですよ。

 

 

 

 オブジェクトを用意できる最大数はiPhoneの機種によって違うけど、いずれにせよ限界はあるわけで、古参がいつまでも居座り続けるのは良くないです。

 ここら辺を防ぐためにweakをつけます。

 

 

 で、weakをつけたプロパティは設定されているオブジェクトが消滅するときはnilが設定されることになるので、利用するときに毎回アンラップして使うのが安全ってことになります。

 これはすでに実装済み。

class MapViewController: UIViewController {
   ・・・
    @objc func tap(tapGestureRecognizer:UITapGestureRecognizer) {
        let location = tapGestureRecognizer.location(in: self.view)
        self.delegate?.selectDestination(name:"\(location)")
    }

 最後に、weakをつけられるプロパティには条件があって、オブジェクトでないとダメなんですな。そのためプロトコルを定義するときにAnyObjectを採用します。

protocol MapViewControllerDelegate : AnyObject {
    func selectDistination(name:String)
}

 これでプロトコルMapViewControllerDelegateはクラスでしか採用できなくなり、結果、MapViewControllerDelegate型は必ずオブジェクトが設定されることが保証されweakがつけられるようになります。

 もっとも、このプロトコルをObjective-Cでも利用できるようにしたいなら、その点を保証するためのキーワード@objcをつけることになり、このキーワードには暗黙裡にAnyObjectを継承させる指定も含まれるので

@objc protocol MapViewControllerDelegate {
    func selectDistination(name:String)
}

と書くのがよく見られる書き方ってことになります。

 

サンプル:

http://tetera.jp/xcc/book-sample/delegate-weak.zip

 

というわけで、これでAppleの3大オブジェクト連携手法が出揃ったわけですな。

 

 ターゲットアクションデザインパターン

 通知

 デリゲートデザインパターン

 

 ここら辺を理解できたら、大概のサンプルは何やってるかわかるんじゃないかと思われ。あとはストーリーボードってことになるけど、これはまた次回。

 

AD