SwiftUI でのレイアウト

デバイスごとのレイアウトというのは非常に面倒なもので、最近はどちらもで対応できるユニバーサルなデザインも流行っているのではあるが、やはりそれぞれのポテンシャルを最大限に活かすためにはそれぞれに最適なレイアウト・UI を提供すべきだと考えます。

となれば実行しているデバイス・デバイスの傾きで UI を変更する必要があります。

それをどうやったら実現できるかについて考えてみました。

シミュレータと実機

シミュレータと実機の区別は以下のコマンドが使えます。

#if targetEnvironment(simulator)
    // Simulator
#else
    // Device
#endif
1
2
3
4
5

じゃあ#if targetEnvironment(iPhone)みたいなのがないのかという話になるのですが、残念ながらありません。

iPhone と iPad

では iPhone と iPad はどうやって区別するかということなのですが、一つの方法としてUIDevice.current.userInterfaceIdiomを使うものがあります。

Enum 意味
.phone iPhone, iPod Touch
.pad iPad
.tv Apple TV
.carPlay CarPlay
.mac macOS
.unspecified それ以外

これはUIUserInterfaceIdiomという Enum を返し、それぞれ上記の表のような種類があります。

デバイスの向き

デバイスにはそれぞれ Landscape と Portrait という二つのモードがあります。iPhone と iPad にそれぞれあるので全部で四種類の UI を用意しなければいけないわけです。

ただ、iPhone の Landscape は実装されていないアプリも多いです。Landscape に特化した(例えば動画視聴アプリのようなものを除けば)実際には三種類用意すれば十分と言えるでしょう。

となれば実装すべき組み合わせは以下の通りとなります。

.phone .pad
portrait - Required
randscape Required Required

では次に傾きを調べる方法なのですが、UIDevice.current.orientationというのがあるのですがこれは利用できないので注意しましょう。

というのも、UIDevice.current.orientationはアプリが起動してから傾きが変化するまでの間に意味不明な値が代入されているからです。

なのでUIApplication.shared.windows.first?.windowScene?.interfaceOrientationを代わりに使うようにしてください。

View を表示してみる

というわけで、切り替えられるように以下のようなコードを書いたとしましょう。

これは一見するとちゃんと動きそうなのですが、実際には動きません。何故でしょうか?

struct ContentView: View {
  var body: some View {
    // デバイスと傾きでビューを切り替える
    switch (UIDevice.current.userInterfaceIdiom, UIApplication.shared.windows.first?.windowScene?.interfaceOrientation) {
      case (.pad, .landscapeLeft), (.pad, .landscapeRight):
        padLandscapeView
      case (.pad, .portrait):
        padPortraitView
      default:
        phoneView
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

というのも SwiftUI の再レンダリングをするには変数をStateにする必要があるからです。UIDevice.current.userInterfaceIdiomUIApplication.shared.windows.first?.windowScene?.interfaceOrientation@Stateのプロパティラッパー指定がないためここの値が変わってもビューの再レンダリングが行われないというわけです。

通知を受け取れるようにする

これを解決するためには傾き等の情報が更新されたときに SwiftUI が検知できるように@Stateをつければ良いことになります。

ただし、それをいろいろビューで宣言するとややこしくなるだけなので@ObservableObjectの仕組みを利用しましょう。

@ObservableObjectは簡単に言うと@Stateをクラス化したものです。似たようなものに@StateObjectがあるのでややこしいですが、ちょっと違います。

@StateObject と@ObservableObject の違い