iOS8向けアプリ開発時の注意点
先日、iOS8のGM seedが公開されました。iOS8の正式リリースも間近ですね。
ところで、iOS8対策は済んでいますか?
もし対策が済んでいたとしても、この下にある「UITextView、UITextFieldで編集メニューの表示がおかしい」だけは読んでおく事をお勧めします。
全てのUITextView、UITextFieldを使用したアプリが関係する問題です。
私自身、iOS8はベータ版の時から見ていたので、いくつか不具合が出る箇所を発見していました。
そこで、まだGMの段階ですが、備忘録を兼ねてiOS8対策に関して情報共有したいと思います。
間違いがありましたならばご指摘ください。
今回記載するのは以下の内容です。
他にもiOS8ではDeprecatedになったAPIなどがありますが、それらは他の方に任せて、緊急性が高いもの、またはウチでしかわからない様な細かい事に限定しました。
- 画面の向きにより取得できるサイズ値が変更されている
- UITextView、UITextFieldの編集メニューの表示がおかしい
- UIAlertView、UIActionSheetがDeprecated
- iPadでUIAlertControllerをUIAlertControllerStyleActionSheetで呼び出すとアプリが落ちる!?
- UISearchDisplayControllerがDeprecated
- ソースでのAutoLayoutの指定が厳密になった
- UINavigationBarのY座標がおかしいタイミングがある
- UINavigationBarに貼り付けたUIBarButtonItemの位置がおかしい
- NSUserDefaultsの値がアプリ削除後も残る!?
- Sandboxのパスが上書きインストールする度に変わる
画面の向きにより取得できるサイズ値が変更されている
iOS8より、[UIScreen mainScreen].bounds などで取得できる端末の画面解像度のサイズは、画面の向きにより変更される様になりました。
※サイズは2x端末の場合を想定しています。そのため、実際の解像度の1/2になっています。
- iOS7 … 固定サイズ。Portait(縦長表示)、Landscape(横長表示)共に縦横 568×320が取得できた。
- iOS8 … 画面の向きにより変わる。Portait(縦長表示)568×320、Landscape(横長表示)では320×568になる。
これの何が問題になるのか?
例えば、画面のサイズを見て表示位置やサイズを変更していた場合、特にLandscapeの時に今までの固定サイズではなくなるためにおかしくなります。
他にも、画面の回転時に以下のUIViewControllerのメソッドで回転イベントを取得して表示位置やサイズを変更していた場合、画面回転前に呼び出されるwillRotateToInterfaceOrientationでは回転前のサイズが取得できるので、考慮する必要があるでしょう。
※ちなみに以下の2つのメソッドはiOS8ではDeprecatedです。
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
この画面の向きで取得できるサイズが変わるという仕様は、ソフトウェアキーボードのframeサイズにも関係しています。
例えば、ソフトウェアキーボードの高さに合わせてUIViewControllerのサイズを変更していたとします。
今まではソフトウェアキーボードのframeサイズを画面の向きを判断してheight値(Portrait)を使うのかwidth値(Landscape)を使うのかを切り分けていました。
iOS8からはheight値のみを見る事になります。その意味ではスッキリしたという事でしょうかね。
この仕様変更に関しては、今までかなり特殊だったUIWindowを使用したUIの座標指定にも当てはまります。
こちらも楽になったという事でしょうかね。
参照: UIWindowを使用したUIを作成する方法と注意点
UITextView、UITextFieldの編集メニューの表示がおかしい
iPhone6 / iPhone 6 Plusでは新しい解像度が追加されました。
そして、設定が少し変更されています。
まずはAppleのサンプルコードであるUICatalogをXcode6でビルドして走らせた所を見てもらいましょう。
なお、このUICatalogはiPhone6 / iPhone 6 Plusが発表される前に公開されたものです。(WWDC2014以降)
Appleのサンプルコード「UICatalog」でもウチが味わったUIWindow関連の問題(表示領域がおかしい)が発生して楽しい(笑) Zoomポップアップの表示領域に注目です。 pic.twitter.com/qgccEM1CTE
— SkyArts.com (スカイアーツ) (@SkyArts_dot_com) 2014, 9月 12
Zoomポップアップの表示領域がおかしい事に注目してください。
左端が切れているのがわかるかと思います。
ちなみに、Zoomポップアップが非表示になった後、iOSの編集メニューが出るはずなのですが、画面上部に表示されてしまい、表示する所を確認できませんでした。
再度試した所、編集メニューの下部の一部だけが表示されるという異常な状態になりました。
最初に見た時に編集メニューが表示されないので機能自体が無くなったのかと思ってしまいました。流石にそれは無いですよね(笑)
ウチにはテキスト編集はスクラッチで開発した超高機能テキストエディタ「Wrix」があるのでUITextView自体、余り使わないもので(笑)
別の画像も見てもらいましょう。
iPhone6ではこんな感じ。 iOSの編集メニューの表示領域がおかしい事に注目です。 pic.twitter.com/MJfHkqI3e4 — SkyArts.com (スカイアーツ) (@SkyArts_dot_com) 2014, 9月 12
iOSの編集メニューの表示領域がおかしい事に注目してください。
上記の画像は表示領域が切れている問題ですが、編集メニューなどが少し離れた左上などに表示される事もあります。
ちなみに、UICatalogのUITextFieldの画面で試すと編集メニューさえも表示されない事があります。
完全に編集メニューの表示位置が明後日の方向にあるからだと考えられます。
上記のZoomポップアップ、編集メニューの表示領域がおかしい問題は、以下の環境で発生しました。
- iPhone 6シミュレータ
- iPadでiPhoneアプリとして走らせた場合
しかし、iPhone 6 PlusシミュレータとiPadでは発生しませんでした。
この様に環境によって発生する場合と発生しない場合があり、この問題の複雑さを倍増させています。
困った事に逆もまた然りなのです(苦笑)
確かにUICatalogは左端をスワイプして前の画面に戻ると不具合が発生するとか色々と問題があるサンプルコードではあります(笑)
しかし、以前のプロジェクトをきちんと対策しないでいるとこの様な問題が出る可能性がある事がわかります。
単純にXcode 6で旧プロジェクトをビルドするだけでは駄目という事です。
この問題はUITextView、UITextFieldを使用した全アプリに発生する可能性があるという事を理解して頂ければどれだけ大きな問題なのかは理解できるかと思います。
何故この問題が発生したのか?
ZoomポップアップとiOSの編集メニューはUIWindowで作成されていると考えられます。
そして、ウチのアプリでもUIWindowを使用したUIを使用してポップアップメニューなどを表示していますが、同様な表示領域が切れる問題、意味不明な位置に表示される問題に悩まされました。
特にLandscape時に発生しやすい様です。(Portraitでも発生する事もあります)
その結果判ったのは、Xcodeプロジェクトの以下の部分がきちんと設定されていない場合、UIWindowのUIは表示領域が切れる問題、意味不明な位置に表示される問題が発生するという事です。
- TARGET -> General -> Launch Images Source
- TARGET -> General -> Launch Screen File
どちらも起動画像の設定です。
Launch Images Sourceは、各解像度毎の起動画像が必要になります。
今後の事を考えると解像度が更に増えた場合に嬉しくないですね。特に次期iPadがRetina HDディスプレイになった場合などです。
ちなみに、Xcode6でプロジェクトを新規作成するとLaunch Images Sourceは未設定で、新しく追加されたLaunch Screen Fileだけが設定されています。
なお、Launch Images Sourceの設定は詳しく調べていません。空の設定で使用しているだけですのであしからず。
新しく追加されたLaunch Screen Fileは、Xcode 6で新規プロジェクトを作成すると「Base.lproj」フォルダに自動生成される「LaunchScreen.xib」ファイルが指定されています。(拡張子無しの名前を入力します。)
ただし、自動生成される「LaunchScreen.xib」ファイルは、背景は白で黒文字で大きくアプリ名と著作権表記が記載されていますので、修正した方が良いでしょう。
なお、新しく追加されたLaunch Screen Fileを設定した場合、iPhone専用アプリとしてビルドしてもiPadで走らせるとiPadアプリの様に動作してしまう様です。その点だけは注意が必要です。
ちなみに、UICatalogでは Launch Images Source だけが設定されていました。(実際の設定は空ですが)
Launch Screen File だけの場合、バイナリをアップロードする時にValidateで弾かれるかも知れません。Deployment TargetがiOS7.1以下の場合は中身が空であってもLaunch Screen Fileが無いとValidateでNGになるのかも知れません。
恐らく、Launch Screen Fileだけが許されるのはDeployment TargetがiOS8.0以上の時だけかも知れません。この辺りは詳しくは検証していません。
ウチでは今後の事を考え、空のLaunch Images SourceとLaunch Screen Fileを設定する事にしました。
今の所、iPhone専用アプリとしてビルドしてもiPadで走らせるとiPadアプリの様に動作してしまう以外は特に問題は出ていません。
勿論、自動生成された「LaunchScreen.xib」ファイルに設定されているアプリ名を消して黒画面にしましたが(笑)
ちなみに、iOS7の時からLaunch Images Sourceはありましたが、未設定でも問題なく動いていました。
あっ、勿論、UICatalogもウチのやり方で設定した所、問題なく表示される様になった事を付け加えておきます。
良い題材になるので、UICatalogで設定を色々と試してみると良いかも知れません。
なお、上で取り上げた起動画面設定に関しては、きちんと設定しないとiPhone6 Plusの売りであるRetina HDディスプレイによる3倍画像(Scale 3)が使えず、従来通りの2倍画像(Scale 2(従来どのRetinaディスプレイと同じ))で処理される事になるかと思います。
そのため、上記の編集メニューなどの表示問題以前に、起動画面はきちんと設定する必要はあるかと思います。
UIAlertView、UIActionSheetがDeprecated
影響が大きいのはこの仕様変更かも知れません。 UIAlertViewを使用するというのは普通にありますしね。
ただし、Appleも影響が大きい事を考慮したのか、TargetをiOS8にしても警告は出ません。
Swiftではビルドエラーになる様ですが。
UIAlertView、UIActionSheetは共にUIAlertControllerに置き換えます。
多分、置き換え作業はボタン押下後のコードが二重管理になる問題が出る位でそれ程問題では無いかも知れません。
ただし、UITextField付きのUIAlertViewを表示する場合はわかりにくいかも知れません。
特にplaceholderやパスワード指定を設定する場合と入力値を取得する場合です。
ここでは説明は割愛します(笑)
iPadでUIAlertControllerをUIAlertControllerStyleActionSheetで呼び出すとアプリが落ちる!?
UIAlertView、UIActionSheetがDeprecatedになり、UIAlertControllerに置き換える事は説明しました。
しかし、単純に置き換えると困った問題が出ます。
それは、iPadでUIAlertControllerをUIAlertControllerStyleActionSheetで呼び出すと以下の様なエラーを出してアプリが落ちてしまう事です。
2014-09-09 01:48:51.849 Wrix[3332:84506] *** Terminating app due to uncaught exception 'NSGenericException', reason: 'UIPopoverPresentationController (<_UIAlertControllerActionSheetRegularPresentationController: 0x7ff59d1c84a0>) should have a non-nil sourceView or barButtonItem set before the presentation occurs.' *** First throw call stack: ( 0 CoreFoundation 0x000000010c0533e5 __exceptionPreprocess + 165 1 libobjc.A.dylib 0x000000010bc40967 objc_exception_throw + 45 2 CoreFoundation 0x000000010c05331d +[NSException raise:format:] + 205 3 UIKit 0x000000010ac2773b -[UIPopoverPresentationController presentationTransitionWillBegin] + 334 4 UIKit 0x000000010a61a977 __71-[UIPresentationController _initViewHierarchyForPresentationSuperview:]_block_invoke + 1398 5 UIKit 0x000000010a61943e __56-[UIPresentationController runTransitionForCurrentState]_block_invoke + 175 6 UIKit 0x000000010a53997e _applyBlockToCFArrayCopiedToStack + 314 7 UIKit 0x000000010a5397f8 _afterCACommitHandler + 516 8 CoreFoundation 0x000000010bf88337 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23 9 CoreFoundation 0x000000010bf88290 __CFRunLoopDoObservers + 368 10 CoreFoundation 0x000000010bf7e0c3 __CFRunLoopRun + 1123 11 CoreFoundation 0x000000010bf7d9f6 CFRunLoopRunSpecific + 470 12 GraphicsServices 0x000000010e36f9f0 GSEventRunModal + 161 13 UIKit 0x000000010a516990 UIApplicationMain + 1282 14 Wrix 0x00000001075c95aa main + 106 15 libdyld.dylib 0x000000010c984145 start + 1 16 ??? 0x0000000000000001 0x0 + 1 ) libc++abi.dylib: terminating with uncaught exception of type NSException
理由は兎も角、何も考えずにiPadでUIAlertControllerをUIAlertControllerStyleActionSheetを使うとアプリが落ちます。
回避方法は、以下の通りです。
- UIAlertController.popoverPresentationController.sourceView プロパティにUIViewを設定する
しかし、これだけだと左上部にPopoverとしてActionSheetが表示されてしまいますので、以下のプロパティで細かく場所を指定した方が良いでしょう。
- UIAlertController.popoverPresentationController.sourceRect
勿論、UIAlertControllerStyleActionSheet自体を使わず、全てUIAlertControllerStyleAlertで済ませてしまう事もできるかと思います。
iOS8では3つボタンのAlertは横にボタンが表示されるようですから。(iOS7では縦に表示されました。)
UISearchDisplayControllerがDeprecated
UISearchDisplayControllerがDeprecatedになりました。
UITableViewの上部に付けたUISearchBarと連携して検索結果を表示するUIViewControllerですね。
UISearchBar + UISearchDisplayControllerは、UISearchControllerに置き換わりました。
UISearchController自体がUISearchBarを持っているため、UISearchControllerのUISearchBarを表示し、結果を表示するUIViewControllerを用意しないといけなくなりました。
結果を表示するUITableViewController、またはUITableViewを持つUIViewControllerを用意する必要があります。
結果を表示する際に細かいサイズの微調整をしないといけないと思いますが、ウチの場合は手作業で細かく調整しているため、ここでは説明を省きます。
ソースでのAutoLayoutの指定が厳密になった
ソースでAutoLayoutを指定している場合、iOS7では動いていたものがiOS8では急に動かなくなり、アプリが落ちる様になる事があります。
これは、本来間違いの指定でもiOS7では動いていたが、iOS8では厳密になり、動かなくなった、というのが正しいかと思います。
ソースでAutoLayoutを指定する場合、NSLayoutConstraintクラスの以下のメソッドを使用します。
参照: iOSのAutoLayoutの基本的な記述方法とエラーメッセージの種類
+(instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c;
constraintWithItemメソッドの引数に以下の指定がある時にアプリが落ちる様になりました。
- 引数のattributeがTop, Bottom, Left, Rightの場合、toItemがnil
- 引数のmultiplierが0
先にも触れましたが、上記指定は元々間違いです。 iOS7ではそれでも動いていた、という事です。ご注意を。
UINavigationBarのY座標がおかしいタイミングがある
UIViewControllerの表示を切り替えた際、次の画面ではUINavigationViewControllerのUINavigationBarのY座標を取得してStatusbarの高さなどを計算していたのですが、タイミングによってはStatusbarの高さを無視した値がある事がわかりました。
通常、Statusbarが表示されている場合、Y座標は20pxになります。
しかし、以下のタイミングだけは0pxを返します。
- UIViewControllerをpresentViewControllerで表示した際、次の画面(UIViewController)のviewDidLoadまでの間にUINavigationBarのframeを取得した場合
以下の場合は問題ないようです。
- UIViewControllerをpushViewControllerで表示した場合
- UIViewControllerをpresentViewControllerで表示した場合はviewDidLoadよりも後(viewWillAppear等)
まあ、UINavigationViewControllerのUINavigationBarのframe値を取得して何かを行うのは非常にレアケースだとは思います(笑)
UINavigationBarに貼り付けたUIBarButtonItemの位置がおかしい
UINavigationBarにUIBarButtonItemを貼り付ける事は良くあるかと思います。
しかし、以下の様な感じで、極端に左右両端に表示されてしまいます。
※独自ポップアップメニューに貼り付けたUINavigationBarを表示していますが、実際のUINavigationViewControllerのUINavigationBarでも同様かと思います。
Wrix for iOSの次のバージョンでは操作メニューの行頭、行端への移動、選択、削除に関して、行頭と折り返し行頭、行端と折り返し行端に分割して機能を追加する予定です。 同時にキーマクロにも機能追加予定です。 pic.twitter.com/ZdeQSOGZUA
— SkyArts.com (スカイアーツ) (@SkyArts_dot_com) 2014, 9月 9
ベータ版の時点でBugReportを出したのですが、重複でCloseされてしまいました。
そして、GMで直る事を期待していたのですが、期待を裏切り、修正はされていませんでした。
対策をするべきか悩む所ではあるのですが、iOS側の問題として説明をするしか無いのかも知れません。
どちらにしてもユーザーからはアプリのせいにされてしまうのですがね(苦笑)
NSUserDefaultsの値がアプリ削除後も残る!?
これはシミュレータだけの問題なのですが、アプリ内からNSUserDefaultsで何らかの値を設定したとします。
そして、そのアプリを削除し、再度Xcode 6でインストールします。
するとアプリ削除前に設定したNSUserDefaults値が取得できてしまいます。
この問題は実機では発生しない様ですが、シミュレータでNSUserDefaultsを使うアプリを開発している方は知って置いた方が良いかも知れません。
私の場合は間違えた値を設定してしまったので、アプリを削除してから値を入れ直したつもりが更におかしな値になったので気が付きました。
BugReportは提出したのですが、重複で私のBugReportの方はCloseになってしまいました。直に直るのかも知れません。
Sandboxのパスが上書きインストールする度に変わる
Xcode 6でアプリを上書きインストールすると毎回Sandboxのパスが変更されてしまいます。
これは実機、シミュレータ共に発生します。iOS7の実機でも発生しますので、Xcode 6の問題の様にも見えます。
※新規インストールの話ではありません。念のため。
初回インストール時と上書きインストール時にアプリ内課金のレシートのパスを取得したのですが、以下の様に「/var/mobile/Applications/」の後の一意のID部分が変更されています。
/var/mobile/Applications/E19058F8-A942-4A47-93A1-39DD09901639/StoreKit/receipt /var/mobile/Applications/788A8EE0-3235-4A30-A9DA-289991791505/StoreKit/receipt
通常のファイルなどを置いておくDocumentsフォルダ等も同様にSandboxのパスが変更されます。
今まではSandboxのパスはアプリを削除しない限り変更されなかったのですが、Xcode 6では変更されてしまいます。
これの何が問題なのか?
多くの方はフルパスで値を持つ事はほとんど無いかと思います。
それに以前のSandbox内に置いてあったファイルは一部を除き何事も無かったかの様に無事引き継がれます。
しかし、アプリ内課金のレシートは上書きインストールすると引き継がれないため、上書きインストールする度にレシートを持っていない状態になります。
つまり、上書きインストールする度に毎回レシートを取得して検証する処理を走らせないといけない、という事になります。とても面倒ですね(笑)
レシート以外にも引き継がれないファイルがあるかも知れません。例えば、一時ファイルやキャッシュファイルなどです。
「そもそもフルパスで値を保持していないよ!」という場合でも、自動的にファイルが削除されてしまうとテスト時に困る事もあるかも知れません。
シミュレータの場合はアプリのフォルダが大量に増えるという問題もありますね。
実機では直接見る事は普通にはできないですが、実際は同様かも知れません。
参照: JailBreak無しでiOS機の中を見る事のできるソフトウェア「iFunBox」
ウチの場合はWrixとWrixに内蔵している多機能ファイルマネージャー機能でフルパスで処理している部分があったため、急いで対応しました。
まとめ
iOS8もiOS7の時同様に色々な問題がある様ですね。
参照: iOS 7向けアプリ開発時の注意点
特に「UITextView、UITextFieldの編集メニューの表示がおかしい」は全てのUITextView、UITextFieldを使用しているアプリに影響がありますので、ご注意ください。
単純にXcode 6で旧プロジェクトをビルドするだけでは駄目という事です。
他にもDeprecatedになったAPIなど、iOS8対応は色々あります。
特にUIAlertView、UIActionSheetをUIAlertControllerを置き換える作業はかなり大変かも知れません。Wrixの場合は機能が多いため、300カ所以上を修正しました(苦笑)
私自身、大量にBugReportを出していて、細かい部分の問題も把握しているのですが、余りにも細かいので今回は割愛しました。
※BugReportを出しても誰からも一切感謝されないのですけれどね(笑)
加えて、新しいiTunes ConnectにアップロードするPNG形式の画像ファイルはアルファチャンネルと透過が不許可になりましたので注意してください。
もしiOSアプリ開発などで私どもが協力できそうな事がありましたならばご連絡ください。
超強力な開発者が開発のご協力をさせて頂きます(笑)
スクラッチで開発したiOS向け超高機能テキストエディタ「Wrix」もよろしくお願いします。