まだ生きてます。
前回、「UIViewをカスタマイズしたクラスをストーリーボードで指定するには、ちょっと気をつける点があるよん」と言ったきりのご無沙汰でした。
まあ、色々忙しかったんすよ。クリスタのお勉強したりして
https://twitter.com/reborn_xcc/status/990049398334746625
描いててクリスタより、4号戦車の75mm砲とバスケット内部構造に詳しくなったわ。
何事も実践が大事ですな。
てなわけで、UIViewをカスタマイズしたクラスであるMapViewをストーリーボードで指定したらどうなるか、サンプルのstoryboard-8.zipプロジェクト改造してやってみましょう。
サンプル:
http://tetera.jp/xcc/book-sample/storyboard-8.zip
まずはこいつ↑をダウンロードして、Xcodeで開く。
「ダウンロードしたプロジェクトだけど大丈夫か?」って聞かれるので「大丈夫だ、問題ない」と言ってOpenボタンクリックだ!
Xcodeをこまめにアップデートしてる人は、ワークスペースにワーニングマーク出るけど気にしなくてもOKっす。
でもでも気になる〜って人は、そのワーニングマークをクリックして修正案を実行させましょう。
今回の場合、プロジェクトのバージョンがXcodeのバージョンに合わないってワーニングなので、修正候補はプロジェクトのバージョンを上げるって修正になります。
候補案のウィンドウが開いた時点で、すでに修正案2点にチェックが入ってるので、そのままPerform Changesボタンをクリックだ!
気になる人は、この時点で適当なシミュレータ選んでRunして正常に動作してるか確認しましょう。
あんまり間が空いたんで、シミュレータの起動方法とか忘れたわーな人は「XcodeでiPhoneアプリ作成編」をYouTubeでみましょう。
↓その2 XcodeでiPhoneアプリ作成編
で、ここからstoryboard-8フォルダのプロジェクトに変更かけるわけだけど、今回はちょっとSource Control機能の再紹介をかねて、現時点からのソース変更分を比較できるようにしちゃいます。
Source Control
「キャッチして」でもちょっと触れたけど、Source Control機能てのは、大雑把にいうとプログラマが希望した時点でソースを履歴として記録する機能。
履歴を記録しておくと、ソースを変更して「やべ、変更で動作不安定になった、元に戻して〜」ってなった時に記録してるソースに戻したり、「どこをどう変更したんだっけ」と思った時に、履歴と比較して変更部分を見つけ出したりできます。
実際、現時点でもstoryboard-7からstoryboard-8にかけての変更である
「ViewControllerクラスのprepareメソッドをオーバーライドしてaltDelegateを設定」
が変更前と比較できるようになっとります。
変更されてるソースを履歴と変更するには、ワークスペースウィンドウのComparisonを選択だ!
これは、storyboard-8の変更をする前に履歴を残してたからで、今回も変更する前に履歴を残しておくことにします。
Commit
この履歴を残す作業はコミットっと呼ばれてて、Source Control>Commitメニューを選ぶことで実行できます。
メニューを選ぶとコミット管理画面が表示されるんで、コミットする理由を書いてCommit 2 filesボタンをクリックしちゃってください。
やりたければ、コミットしたいファイルや変更部分を個別に選択もできるんだけど、今回はCommit 2 filesボタンをクリックして一気にコミットしちゃう。
ボタンはコミットの理由を書いておかないとクリックできないようになってます。コミットする変更部分の、変更理由を他人が見て理解できる形で書きましょう。今回なら
「地図画面(MapViewControllerの画面)を出す前に、MapViewControllerのaltDelegateを設定した」
なんてのがいいと思うけど、手抜きして
「storyboard-8」
にした。仕事でこんなコメント入れたら信用度ガタ落ちなんで気をつけろ!
というわけで、これで準備完了。
いよいよ「UIViewをカスタマイズしたMapViewをストーリーボードで指定」してみます。
まずはナビゲーションエリアでMain.storyboardを選び、MapViewControllerのself.viewにUIView(ライブラリのリストではViewってなってる)をドロップだ!
なにそれ?な人は「今時はプリビズだよ」を読みましょう。
MapViewオブジェクトはライブラリにはないんで、MapViewクラスの派生元であるUIViewオブジェクトを使うんですな。
UIViewを適当な位置にドロップしたら、そのまま選択状態にしておいて、ユーティリティエリアでIdentity inspectorを選び、Class項目をMapViewに設定します。
これで、このUIViewオブジェクトはMapViewとして作られることになる。
ここまでできたら、一旦、Main.storyboard側の変更は保留してMapViewController.swift側の変更をする。
このままだとviewDidLoadメソッドでもう一個、MapViewオブジェクト作ることになるからね。
ナビゲーションエリアでMapViewController.swiftを選び、viewDidLoadメソッドを次のように書き換えます。
class MapViewController: UIViewController { // デリゲートの型は上記のプロトコルとする weak var delegate:MapViewControllerDelegate? // ストーリーボードから設定 @IBOutlet var mapView:MapView! ← 追加 override func viewDidLoad() { super.viewDidLoad()let mapView = MapView(frame:self.view.bounds)mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]self.view.addSubview(mapView)
↑ 削除
mapView.subview.image = UIImage(named:"map")
let swipeGestureRecognizer = UISwipeGestureRecognizer(target: self,
action: #selector(swipe))
・・・
ぱっと見、viewDidLoadメソッド内で
MapViewを作るのをやめた
MapViewの自動リサイズ設定をやめた
self.viewへの追加をやめた
てことと
viewDidLoadメソッド内でローカル変数として用意されてたmapViewが、プロパティになった
てことがなんとなくわかると思いますが
var mapView:MapView!
の前についてる@IBOutletってなんだよ?ってことになってんじゃねーでしょうか?
それ以前にプロパティとかわからん人は「そしてiPhoneアプリへ」「こんにちは世界」を、mapView:MapViewの後ろにつく「!」がわからん人は「アンラップしてチン♪」を読みましょう。
@IBOutlet
こいつは@IBActionと同類で、Interface Builderへの指示キーワードです。
@IBActionの方は、キーワードの後に続くメソッドをInterface Builderに認識させるためのもので「ストーリーボードでモーダル遷移だ」ではドラッグ&ドロップを使ってメソッドごと作成したんだけど、実は、Interface BuilderのGUIなんて使わずに、既存のメソッドに直接@IBActionキーワードを書くことでも指定できるんですな。
同じように@IBOutletはキーワードの後に続くプロパティをInterface Builderに認識させます。
なので、このキーワードをつけたプロパティ
@IBOutlet var mapView:MapView!
をMapViewController.クラス定義に書き加えた後に、再びMain.storyboardを選び、MapViewControllerを選んでからユーティリティエリアでConnections inspectorを選ぶと、Outletsの項目にmapViewが現れます。
あとは、このmapViewがどのオブジェクトを示すかを、@IBActionでやったようにmapView横の○から線を引きづり出して、canvasビュー上のMapViewまで伸ばしてドロップで指定すればOK!
これでMapViewControllerのviewDidLoadメソッドが呼び出される時点で、mapViewプロパティにはストーリーボードで作成されたMapViewが設定されてることになるんですな。
本来ならこれで準備完了、あとはRunするだけってことになるんですが、最初から言ってるようにMapViewクラスには欠陥があって、まともに動きません。
ものは試しでRunしてシミュレータで実行してみてください。「地図」ボタンをタップした途端にデバッグ状態に入っちゃいます。
じゃ、MapViewクラスのどこに欠陥があるかってーと、最初にMapViewクラスを定義した時にやったやつですよ。
「UIViewの派生でイニシャライザって…」で定義したrequired init?(coder aDecoder: NSCoder)てイニシャライザです。
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
こいつが欠陥なんですよ。
ストーリーボードを読み込んで作成されるオブジェクトは、このrequired init?(coder aDecoder: NSCoder)を使って初期化されるんですな。
override init(frame: CGRect)の方は呼ばれません。
これまではMapViewControllerのviewDidLoadメソッドでMapViewを作ってたので、override init(frame: CGRect)側が呼ばれて、required init?(coder aDecoder: NSCoder)側は呼ばれなかったんで問題なかったんですが、今回はストーリーボードを読み込んで作成されるんで、呼び出されちゃうんですよ。
で、当初、MapViewクラス定義の時にやった、とりあえず呼び出されたらエラーにしてデバッグ状態にしちゃえ〜って対応が問題になってくるわけっす。
じゃ、どう対応するかというとoverride init(frame: CGRect)と同じことします。
required init?(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")←削除 ↓追加 super.init(coder:aDecoder) subview.contentMode = .scaleAspectFill subview.frame = self.bounds subview.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.addSubview(subview) }
ただし、呼び出す派生元イニシャライザはsuper.init(frame: CGRect)の方じゃなくsuper.init(coder:aDecoder)の方です。
この派生元イニシャライザを呼び出すことでストーリーボードに設定されたUIViewとしての情報(矩形の大きさ、位置等)が設定されます。あとはMapViewとしての初期化だけど、ストーリーボード側から読み取る情報は特にないので、そのままoverride init(frame: CGRect)と同じことしちゃうわけですよ。
注意)MapView固有の情報をストーリーボード側で設定して、それを読み取る方法もあるけど、ここでは触れない。興味がある人は「親切本」の866ページだ!
これでMapViewはストーリーボードに正式に対応したことになる。
Runさせると、今度は「地図」ボタンをタップしてもデバッグ状態になりません。
MapViewをMapViewControllerのself.viewいっぱいに配置したい場合は、「ストーリーボードでAuto Layoutだ」でやったようにMapViewに拘束を追加してください。ただし、今回の場合はcontrolキーでドラッグしてメニュー出すより、エディタエリアの下にあるAdd Constraintsを使った方が簡単かも。
拘束の追加には、いろいろなやり方があって追求し始めるとキリがないんで、その都度、小出しに紹介していきます。そんなん待ちきれんわ〜な人は「ストーリーボードでAuto Layoutだ」で紹介したApple公式のドキュメントとか「親切本」を読みましょう。
サンプル:
http://tetera.jp/xcc/book-sample/storyboard-9.zip
というわけで、iOSアプリの骨格であるUIView、UIViewControllerの関係やストーリーボードの紹介も終わったし、準備はOKということで、リブートキャンプはこれくらいにして、次回からは私が、今もっとも興味のある
ARアプリ制作
に手を出していこうと思っとります。
じゃ、また。