こんにちは!和尚です。

iOS16がリリースされて数週間、弊社でも担当させていただいているアプリのiOS16対応を続々と行っております。

iOS16は私目線曲者で、対応しなければ特定の画面が全く使えなくなってしまうようなアプリもあって結構手間取っているといった状況です。iOS14、15はそんなに影響がなかったアプリでもiOS16では大きく影響が出てしまっててんやわんや…

そんなこんなで今回は私が今日までいくつかのアプリをiOS16対応していく中で得た知見や、バグの回避策、仕様変更の対応策などを紹介していきたいと思います。

 

目次

  1. デバイスの向きを強制変更するメソッドの修正と代替メソッドについて
  2. 合成音声のVoice IDの変更とバグについて
  3. NavigationViewが原因で発生するクラッシュについて
  4. 終わりに

 

デバイスの向きを強制変更するメソッドの修正と代替メソッドについて

動画配信アプリのように一部の画面でのみ横画面になるようなアプリの場合、多くはフルスクリーンボタンが用意されていてそのボタンを押下することで端末表示を強制的に横画面に変更するという実装がされています。そのフルスクリーンにするために用いられていたメソッドUIDevice.current.setValue()がiOS16から利用できなくなりました。

Apple曰くもともとこの関数を使用する際に画面が回転してしまうのは想定していなかった動作であり、ドキュメントも用意していない非公開関数ということもあって、無慈悲にも新OSでは修正されてしまいました。

ただ焦る必要はありません。このように画面を強制的に横画面にする関数は完全に失われたわけではなく、新たに回転させるための関数requestGeometryUpdate(_:errorHandler:)がiOS16から登場しました。

 

使い方はこちら

func switchPortrait() {
  if #available(iOS 16.0, *) {
    windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait)) { error in
      print(error)
    }
  } else {
    UIDevice.current.setValue(1, forKey: "orientation")
  }
}

func switchLandscapeRight() {
  if #available(iOS 16.0, *) {
    windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .landscapeRight)) { error in
      print(error)
    }
  } else {
    UIDevice.current.setValue(3, forKey: "orientation")
  }
}


また、AppDelegate のapplication(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) を使ってアプリの回転を制御している場合、このメソッドの返却を変更した後にUIViewControllerにiOS16から新しく登場したメソッドsetNeedsUpdateOfSupportedInterfaceOrientations() を呼び出さないと即時反映されないようになったので、注意しましょう。SwiftUIを利用している場合は、UIApplicationから画面最上部のUIViewControllerを取得して対応を行ってください。

 

合成音声のVoice IDの変更と出力時の注意

AVFoundationにも変更がありました。

まずは合成音声のVoice IDの変更です。あるアプリでは使用していたVoice ID com.apple.ttsbundle-Samantha-compactがiOS16では適用されず、日本語設定した端末では日本語の合成音声が流れてしまうというバグが発生しました。

いくつか原因を調べてみたところVoice IDに変更が入ったことが判明し、これまで利用していたIDがiOS16からは適用されなくなっていました。

全てのIDが変更になったかは不明なのですが、もし音声がiOS15と違うなと感じたらIDがiOS16では異なっていないかを確認してください。


音声出力時にAVSpeechSynthesizerのインスタンスを音声出力が完了するまで保持し続けることが必須になりました。

AVSpeechSynthesizerのドキュメントに記載されているノートにはシステムはAVSpeechSynthesizerを音声が完了するまで保持し続ける必要があると記載されています。

Note

The system doesn’t automatically retain the speech synthesizer so you need to manually retain it until speech concludes.

これまではローカル変数として保持していても音声が出力されていましたが、iOS16からはインスタンス変数などで保持し続ける必要が出てきたため音声が出ないというバグが発生した際は改めてノートに記載されているものが守られているかをご確認ください。

// iOS15まではローカル変数として保持していても音声が出力されていた。
func speak() {
  let utterance = AVSpeechUtterance(string: "Hello World")
  utterance.voice = AVSpeechSynthesisVoice(language: "en-US")
  let synthesizer = AVSpeechSynthesizer()
  synthesizer.speak(utterance)
}

// 正しい実装方法
struct Speaker {
  private let synthesizer = AVSpeechSynthesizer()

  func speak() {
    let utterance = AVSpeechUtterance(string: "Hello World")
    utterance.voice = AVSpeechSynthesisVoice(language: "en-US")
    synthesizer.speak(utterance)
  }
}

 

NavigationViewが原因で発生するクラッシュについて

あるアプリで原因不明のクラッシュが多発したのですが、原因はこれまでは問題なかったNavigationViewの配置方法でした。

弊社ではクラッシュの原因を突き止めやすくするために、基本的に全てのアプリでFirebase Crashlyticsを導入させていただいています。Crashlyticsに送られてきたエラーのスタックトレースの一番上は`static UIApplicationDelegate.main() + 104`で実際に端末をデバッグビルドしても同じエラーのスタックトレースしか見れずお手上げ状態でした。

そこでより詳細なエラーレポートを確認するために、Xcodeのメニューバー`Debug > Detach from アプリ名`からエラーを確認してみたところどうやらSwiftUIでエラーが発生していることが判明しました。

Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   SwiftUI    0x112d771b8 0x112824000 + 5583288
1   SwiftUI    0x112d77178 0x112824000 + 5583224
2   SwiftUI    0x112d771cc 0x112824000 + 5583308
3   SwiftUI    0x113705af4 0x112824000 + 15604468
4   SwiftUI    0x11396d584 0x112824000 + 18126212
5   SwiftUI    0x11396d5b0 0x112824000 + 18126256
6   UIKitCore  0x11c310760 -[UIViewController removeChildViewController:notifyDidMove:] + 124
7   UIKitCore  0x11c2631d4 -[UINavigationController removeChildViewController:notifyDidMove:] + 76
8   UIKitCore  0x11c30a2c0 -[UIViewController dealloc] + 764
9   UIKitCore  0x11c26d224 -[UINavigationController viewDidDisappear:] + 368
10  UIKitCore  0x11c3118a4 -[UIViewController _setViewAppearState:isAnimating:] + 988
11  UIKitCore  0x11c3124b8 -[UIViewController __viewDidDisappear:] + 148

ただSwiftUIの何が原因かという詳しい情報は取れなかったのでスタックトレースにあった`UINavigationViewController`という情報よりNavigationViewを調べてみたところ、NavigationViewの配置方法が原因でiOS16でクラッシュしていることが判明しました。

条件が変わった時にNavigationViewごとViewを切り替えているのがクラッシュする原因だったので、条件毎NavigationViewで囲ってしまったところクラッシュが発生しなくなりました。

// クラッシュする記述方法
var body: some View {
  if isLogin {
    NavigationVIew {
      HomeView()
    }
    .navigationViewStyle(.stack)
  } else {
    NavigationView {
       LoginView()
    }
    .navigationViewStyle(.stack)
  }
}

// 回避策
var body: some View {
  NavigationVIew {
    if isLogin {
      HomeView()
    } else {
      LoginView()
    }
  }
  .navigationViewStyle(.stack)
}

ただこの修正方法は切り替えた時、元々の画面遷移の動きとはことなる動きをする可能性があるので、あくまでクラッシュを抑えるものとして参考にしてください。

より詳細な原因もしくは対応策が判明し次第追記します。

 

終わりに

いかがだったでしたでしょうか。他にもWKWebView表示時に警告が出るものの何も支障がないといったXcodeのバグ?など他にもいくつか確認している問題があるので分かり次第更新していきたいと思います。

ではでは、よい開発ライフを!

投稿者プロフィール

和尚
和尚
'96年生まれのiOSエンジニア