iOSプログラミング:iOS11のStoryboardの「Present Modally」Segueを深掘り

今回は、iOS11のStoryboardの画面遷移で用意されているSegueの種類の違いついて掘り下げたいと思います。
iOSプログラミング:iOS11のStoryboardのSegueの使い方(その1)では、古いSegueがDeprecated(非推奨)になっており、今のiOS11でのほぼノンコーディングでの簡単な書き方を紹介しました。
iOSプログラミング:iOS11のStoryboardのSegueの使い方(その2)では、Navigation Controllerを追加すると自動的にNavigation Barと戻るボタンが作成されて、画面遷移をノンコーディングで作れるやり方を紹介しました。この時は、

  • Show (e.g. Push)
  • Show Detail (e.g. Replace)
  • Present Modally
  • Present As Popover

の4種類のSegueのうち「Show (e.g. Push)」を使いました。じゃあ、他は何が違うのよというところを調べたのでご紹介します。
サンプルとして前回のアプリをSegueの種類が試せるように以下のように改造しました。

Storyboardでみると千手観音のようになります。

このStoryboardをみると一番上の「Show (e.g. Push)」以外のSegueではUINavigationControllerを使っていてもNavigation Barが自動的に追加されていないことがわかります。用途として「Show (e.g. Push)」は元の画面の延長となる画面に遷移する場合、他はそうではない場合を想定した設計思想になっているように思えます。
 
「Show (e.g. Push)」以外ではSafeAreaに注意
NavigationBarが自動的に追加されないので、自由にレイアウトできますが、かわりにSafeAreaを気にする必要があります。気にしないとiPhone Xの切り欠きやその他のiPhoneのステータスバーに被ります。

SafeAreaについては、iOS8対応の古いアプリをiPhone Xに対応する手順で説明していますので、古くないアプリの場合もそちらを参照してください。
 
Show Detail (e.g. Replace)
基本的にはNavigation Barが自動的に追加されない以外は、「Show (e.g. Push)」のSegueと違いはないようです。しかしNavigation Controllerなしで「Show (e.g. Push)」のSegueを使った時と同様に下から上にポップアップして画面が遷移します。右から左に遷移しない理由はわかりませんが、iOS11ではそのようになりました。またその挙動を変えるようなプロパティも見当たりません。ひょっとしてiPadでは違うのかなと思い検証したのですが変化なかったです。

 
Present Modally
デフォルトだと「Show Detail (e.g. Replace)」のSegueと見分けがつかない画面遷移をします。しかし、「Present Modally」のSegueにはTransitionという遷移の時の挙動(アニメーション)を変えるプロパティが用意されています。

  • Default
  • Cover Vertical
  • Flip Horizontal
  • Cross Dissolve
  • Partial Curl

の5つで「Default」は、iOS11では「Cover Vertical」と同じ挙動でした。「右から左に滑り込む」のがないのが不思議です。ユーザーインタフェースの設計思想でしょうか。
 
Cover Vertical
下から上にポップアップします。「Show Detail (e.g. Replace)」のSegueと見た目の挙動は同じです。
 
Flip Horizontal
画面がひっくり返って、裏側の次の画面が表示されます。

 
Cross Dissolve
画面がフェードアウトして、次の画面がフェードインします。
フェードアウト・フェードインのスピードはかなり速くて、スクリーンショット取れませんでした。
 
Partial Curl
画面がめくり上がって、次の画面が表示されます。

初期のiPhoneのグーグルマップでストリートビューに切り替えるので見かけましたし、見た目が面白いので自分も使いました。当時はめくり上がった後に次の画面の上にめくれが見えていて、そこをタップすると元の画面に戻ったのですが、フラットデザインになったiOSからこの次の画面で見えている「めくれ」がなくなりました。依然として「めくれ」が以前にあったところをタップすると戻れたのですが、わかりづらい。それで私は順次使うのをやめています。
 
iPhoneシミュレータのバグ?
この検証をしているときに遭遇したのですが、挙動を観察するためにシミュレータの「Debug」の「Slow Animations」にした時に、「Flip Horizontal」だと、遷移した後にボタンをタップしてもアプリが反応しなくなります。その状態で「Slow Animations」を解除すると反応する時と、依然としてしない時があります。シミュレータのバグだと思います。
 
今回紹介した「Present Modally」のSegueはいろいろと挙動を変えられるので、ユーザーインタフェースをデザインする時に便利です。
Present As Popoverは又の機会で、今回はここまで。

iOSプログラミング:iOS11 NavigationControllerを使うと画面遷移が楽チン(2018年版)

前回では2画面間をSegueで画面遷移して、その1本のSegueを逆に戻るunwindをほぼコーディングで実装する方法を紹介しました。
でももっと楽チンな方法があるのです。
 
まず例として前回のunwindを消したアプリを作ります。

Navigation Barという画面の上にアプリ名とか画面名を表示しているアプリを見たことあると思いますが、それを使います。
ただし、下記のXcode右下のObject Libraryに表示されているのをドラッグして貼ってはいけません!
 
これダメ

これをやっちゃうと縦幅が融通きかなくて、自分で上に隙間を設けないとiPhoneのステータスバーやiPhone Xの切り欠きとオーバーラップしてしまいます。
 
隙間がダサい

このように、上の隙間がダサいし、今回の目的が達成できません。
ではどうするのかというとNavigationControllerクラスを追加します。しかし、これまた下記のXcode右下のObject Libraryに表示されているのをドラッグして貼ってはいけません!
 
これダメ

この方法だと、なぜかいらないTableViewControllerまで作られちゃいます。勉強のためにやってみていいけど、消してください。
ではどうやって足すのかというと、Storyboardで最初に表示するUIViewControllerを選んで、Xcodeの「Editor」メニューをプルダウン、「Embed in」メニューのところにあるNavigation Controllerを選びます。

 
さぁ、やってみてください。じゃーん

左にNavigation Controllerが増えただけでなく、遷移先の右のUIViewControllerまでNavigation Barが追加されました。しかも縦幅もちょうどよく長くなり、「

iOSプログラミング:iOS11のStoryboardのSegueの使い方(2018年版)

iOS11の対応で、手持ちのアプリの従来のSegue関係のコードのところが単なるDeprecatedのウォーニングで済まずにエラーになってしまい、とりあえず昨年はエイヤで対処はしたものの、もやもやしているので整理してみました。
ネットもチェックしたのですが、Segueによる画面遷移の書き方に関して古い記述が多いのも整理した動機にあります。

これはSegueの種類を選ぶポップアップですが、下半分が古いSegueで、全滅なのがよくわかります。
それでは、例として超シンプルなアプリを紹介します。

シングルViewでプロジェクトを作成して、もうひとつUIViewController(右)を作成し、左のUIViewControllerで「次へ」がタップされると右のUIViewControllerに遷移し、「戻る」がタップされると右から左に遷移するだけのシンプルなアプリです。
 
1)親から子へ遷移
「次へ」ボタンからCtrl+ドラッグで右のUIViewControllerへドロップするとSegueができます。種類は一旦ここでは「Show e.g. Push」を選びます。これだけでノンプログラミングで左から右に遷移するようにできます。遷移だけならタップに対するActionも書かなくてOK。この程度の振る舞いであれば、以前のようにSegueのIdentifierプロパティにユニークな名前つけてメソッド呼ぶ必要はないです。Segueのイベントは暗黙でタップになります。
 
2)子から親へ戻る遷移
次に戻り方です。先ほどと同じ方法で逆方向にSegueを作ってはいけません。先ほどのSegueを逆に遷移させる、つまりunwindさせることで遷移できます。
この状態でunwindを設定するために右のUIViewControllerのexit印に「戻る」ボタンからCtrl+ドラッグで紐付けようとしても引けません。

これを紐づけるにはまず親である左のUIViewControllerにUIStoryboardSegueクラスのIBActionを定義します。
ViewController.h

- (IBAction)myUnwindAction:(UIStoryboardSegue*)unwindSegue;

ViewController.m

- (IBAction)myUnwindAction:(UIStoryboardSegue*)unwindSegue{
  NSLog( @"Unwinded" );
}

上記のmyUnwindActionに親に戻ってきた直後の処理を書くこともできます。ここではNSLogでログだけ吐かせています。
これを書いてから先ほどのexit印に「戻る」ボタンからCtrl+ドラッグで紐付けをやろうとすると、myUnwindActionが選べるようになります。親のUIViewControllerにコードで定義して、Storyboardの子で選ぶというのがわかりずらいかも。親から複数のUIViewControllerに遷移して、親に戻ってきた直後の処理が異なる時は同じように親のUIViewControllerに異なる名前で複数追加すれば、その中から選べます。
紐づけると上記の定義以外はノンコーディングで完成。
この場合のSegueのイベントも暗黙でタップになります。
 
3)遷移直前の処理を書く
親から子に遷移する時も、子から親に戻る遷移も、遷移前のUIViewControllerのボタンにActionを追加してコードを書けば、遷移直前の処理が追加できます。AppDelegateでUIViewController間で共有しているオブジェクトを更新したり、アプリのNSUserDefaultsを更新したりといったことができます。

問題点
今回の実装方法で、ひとつおかしな挙動があります。
今回のSegueは「Show e.g. Push」という種類で、下記のアイコンのとおり右から左に画面が入り込んでくるのが正しい挙動です。

ところが、今回の実装だと何故か下から上にポップアップしてしまいます。バグなのかNavigation Barがないから下から上という仕様なのか微妙です。
 
Segueはここからまだ奥が深いので一旦終わり。
 
参考:Apple公式 View Controller Programming Guide for iOS - Using Segues

iOSプログラミング:NSUserDefaultsのsynchronizeメソッドは気安く使ってはいけない

iPhoneアプリで、アプリの設定などを保存しておくのにまず使うのがNSUserDefaultsクラス。

// オブジェクトへの参照
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];

// データの保存
[ud setObject:myNumber forKey:@"My Number"];

// データの読み出し
NSNumber *myNumber = [ud objectForKey:@"My Number"];

といった感じ。
昨年、実は自分ひとりしかユーザーがいないけどApp Storeで絶賛公開中の俺アプリ「潜水日記」というのを開発した時にも悩んだのだけど、このNSUserDefaultsにはsynchronizeメソッドというのがあります。その名の通り、アプリが高速に参照できる一時的なメモリ領域と、最終的に保管する領域を同期させるメソッドです。
synchronizeメソッドは、保存を確実にするために呼び出しを推奨するような書き込みをネットで見かけます。自分も過去に経験した他のプラットフォームで、DBのコネクションだの、ファイルアクセスだの、いろいろなAPIsynchronizeメソッド的なのがあると、お約束として呼ぶクセがあるのですが、それはiOS11ではアウトだと気付きました。
顕著にその悪影響が露呈するのが写真の保存。たとえばUIImagePickerControllerクラスで写真を撮ったり、フォトライブラリから写真を選んでアプリのUIImageに貼るだけの下記のようなアプリ。

わざわざ写真をサムネイルに縮小してからNSDataオブジェトをNSUserDefaultsに保存しても、synchronizeメソッドだけで30秒とか尋常じゃない時間がかかりました。アプリのユーザーから見るとiPhoneが固まったみたい。最初はどこかのコードでメモリリークしているのかと思いました。
原因を調べていたら、本家アップルのリファレンスマニュアルのsynchronizeメソッドにこんな注意書きがありました。

Waits for any pending asynchronous updates to the defaults database and returns; this method is unnecessary and shouldn't be used.

この記述で大事なのは2点。

  1. ペンディングされている非同期の更新を待つということは、裏を返せばNSUserDefaultsはペンディングされる理由がなければほっといても非同期で粛々と更新が動くらしいこと。
  2. synchronizeメソッドは必要ないし、使ってはいけないこと。

うーん。必要ないならDepricatedしろよっていいたくなりますねw
推奨している人のブログを読んでいると、以前のiOSはこの記述なかったみたいです。自分の記憶でもiOS10で遅い症状は確認したもののマニュアルにこのような記述があった記憶がありません。iOS12あたりでDepricatedになりそうな気もします。
しかし、synchronizeメソッドを削除したこのテストアプリでいろいろ試したところ、以下のケースでは写真が保存されないことがありました。

  1. デバッガで終了
  2. アップスイッチャーでアプリ終了

デバッガは仕方がないとして、アップスイッチャーでアプリの終了には対応したいですね。
そこでNSUserDefaultsを変更しているところのコードではsynchronizeを一切呼ばないようにして、標準でプロジェクトに生成されるクラスAppDelegate.mに以下の2点を追記しました。

- (void)applicationDidEnterBackground:(UIApplication *)application {
  NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
  [ud synchronize];
}
- (void)applicationWillTerminate:(UIApplication *)application {
  NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
  [ud synchronize];
}

いまのiOS11ではアップスイッチャーになるとapplicationDidEnterBackgroundメソッドが呼ばれるのでそこでsynchronizeメソッドを呼ぶようにしました。あとはアプリが終了直前に呼ばれるハズのapplicationWillTerminateメソッド。だけどiOS11ではアップスイッチャーで終了するケースでは呼ばれないようです。じゃあDepricatedでもないのに、いつapplicationWillTerminateメソッドが呼ばれるんだよってのはまだ把握していないけど、念のため書きました。(というようなモグラ叩きな発想はいけませんねーw)予想ではメインメモリが足りなくなってiOSに強制的に終了されるケースです。
このワークアラウンドで「潜水日記」アプリは固まらず、かつ画像が今の所100%保存されています。

iPhone XのFace IDが次々と突破されるワケ

iPhone Xが発売されてから、Face IDをこーやって突破したというニュースが飛び込んできています。
双子は簡単かもと思えますが、兄弟までも。
アップルの説明では、

Touch IDは他人が突破する確率が5万分の1、新しいFace IDは100万分の1


と格段に性能が上がっているはずですが、明らかにTouch IDより突破されやすい状況です。
それはなぜか?
この数字にはマジックがあります。
そのまま比較するとFace IDはTouch IDの20倍性能が良いはずです。

顔真似と指紋を真似るのどっちが何倍簡単か?

顔真似のほうが20倍以上簡単ではないか。

ここが語られていないので、20倍性能が良いと錯覚させられています。
ざわちんメイクで突破できそう。

Xcode9とiOS11で追加されたSafe Area Layoutのバグ

昨日まとめた「iOS8対応の古いアプリをiPhone Xに対応する手順」を見ながら手持ちのアプリを順次iPhone XおよびiOS11対応していてSafe Area Layoutのバグらしき現象を見つけました。

こんな感じでNavigation Barの縦幅ぐらいのマージン(C Card Infoラベルの上の余白)が直後のScroll Viewとの間にできています。この現象は縦置きでも置きているので実に嫌です。
発見したのはiPhone5でiOS10.3の実機で。
今回の修正でやったことといえば、
1)Segueを新たに作成し直し
SegueはKindをShow (e.g. Push)にするとNavigation Barが遷移先に適切なサイズで勝手に作成されて便利だったので。
2)Scroll Viewのマージンを設定しなおし
これでStoryboard上に表示されているマージンがSafe Areaからゼロになります。
3)画面一番最初のラベルのマージンを設定しました。
設定しないと明後日の位置になるみたいでScroll View内が真っ白だったので。
 
さらにシミュレータで確認したところ不思議なことに気づきました。
シミュレータの複数のデバイスのiOS9とiOS10で先ほどの症状を確認できましたが、シミュレータでiOS11だと正常。

iPhone6 iOS11実機でも正常でした。
iOS9とiOS10、というかSafe Area Layoutのバグっぽいです。

iOS8対応の古いアプリをiPhone Xに対応する手順

App Storeで公開中のiPhone/iPadアプリが、iPhone Xで表示がカメラに隠れてしまうことに気づき、いろいろあーだこーだした修正手順をまとめてみました。
こんな症状を修正します。画面上のナビゲーションバーのアイコンの位置だけ正しい位置なのは、いじわるのフォースを感じますねw

私の場合は、もっとも古いのでiOS8で動くアプリです。
縦置きは、私のアプリはステータスバーのマージンをすべてとっているのでカメラに隠れず大丈夫でした。
iPhone Xスクリーン下のスリット部分も表示領域になっていませんでした。
しかし、横置きが全滅でした。
もっとも簡単な対処法は、横置きの対応をやめるというのもありだと思います。iPhoneで縦置きと横置きの両方に対応したいアプリってあまり無いと思いますので。大抵どちらかだけですよね。それを言っちゃ技術者魂が許さないので調べました。
 
Safe Area
iOS11の開発環境であるXcode9で導入された表示領域に関する新たな概念です。Safe Areaとは確実に矩形で表示できる領域で、今のところiPhone Xだけこれが大事みたいです。
古いアプリのプロジェクトをXcode9で開くとプロジェクトの設定変更を勧めてきます。自動で変更してからUIViewを選んで右側のSize Inspector(物差しアイコン)を開くと、Layout MarginsにSafe Area Relative Marginsという項目が増えています。
 
手順1)Safe Area Relative Marginsを有効にする
自動でプロジェクトの設定変更をしていたら、この項目はチェックが入っていることがあるみたいですが、その規則性はわかりません。

最初からXcode9で作成したアプリと変換した古いアプリでは違いがあり、最初からXcode9で作成したアプリにはさらにSafe Area Layout Guideという項目があります。

この項目を出現させる方法は結局わからなかったのですが、今のところ問題はありませんので次へ。
このままだとまだiPhone Xで表示が欠けてしまいます。調べたら

Safe Areaに対応できるのはiOS9以降から

だからでした。XcodeでSafe Area Relative Marginsにチェック入れて安堵していたらトラップ!
つまり

iOS8対応のアプリはiPhone Xに対応できない

まずこれを解消。
 
手順2)Project設定のDeployment TargetをiOS9.0以降に変更する
これでもだめ。新しいアプリと古いアプリのストーリーボードでUIViewをよーく比較観察すると違いを発見。 
 
元iOS8のアプリ

 
最初からXcode9で作成したアプリ

違うアプリなのでちょっとわかりにくいですがUIViewの配下にSafe Areaという要素がぶら下がっています。これがないから動かないみたいということに気づきました。知らなかったのですが

インターフェースビルダーにはプロジェクト設定とは別にDeployment Targetがある

これがiOS8のままなのが原因でした。
 
手順3)インターフェースビルダーのDeployment TargetをiOS9以降に変更する
ではどこでこれを出現させるか?というと、左側のツリーでストーリーボードを選んだ状態で、ストーリーボードのなかの要素を何も選ばないで右側のFile Inspectorのアイコンを押します。もしストーリーボードで何か選択されている場合は、一度ストーリーボードの何もないところをクリックしてFile Inspectorを表示します。
Builds For項目のDeployment TargetをiOS9.0以降にします。

 
手順4)Use Safe Area Layout Guidesを有効にする
これも自動で設定変更したらチェックが入っていると思います。この項目にチェックを入れたら手順1で書いたUIViewのSafe Area Layout Guideという項目が出現するかと思ったのですが出現しませんでした。しかし、さきほどのUIViewの下にSafe Areaの要素が出現して、iPhone Xで正しく表示されるようになりました。
 
以上でiPhone X対応が完了です。右側のマージンまで左のカメラと同じだけ削るのはちょっとどーよって思いますがディスプレイのカーブで矩形ではないからだと思います。

iOS9やiOS10など比較的新しいアプリはここまでの設定が、アプリによってプロジェクトを開いた時の自動の設定変更だけでほぼできていてるようで、前回「 iPhone Xで表示テストしてわかったこと(その2)」で書いたようにどれか1つマージンを再設定するだけで正しい表示になることもあるし、まちまちです。詳しいことはわかんないですけど。
ここまでの作業よりも大変だったのは、iOS9に変更したことによってpopoverとかUIAlertViewとかが完全にdeprecatedになってたり、segueの仕様も変わっていたり、plist.infoへの記述が厳しくなったり、iTunes connectでアップロードしたら怒られたりでその修正のほうが大変でした。(汗)
まーほとんどのアプリは自分しか使っていないんで、そもそもお前iPhone X買うのかよって話なんですけどねw