[SwiftUI] 縦横に柔軟に配置するレイアウトを作る

SwiftUI

     
⌛️ 2 min.
SwiftUI の VStack, HStack を柔軟に適用するレイアウトの作り方を説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.3
  • Xcode 12.5
  • iOS 14.5

作りたいレイアウト

iOS のデバイスは 長方形 なので、スクリーンは、Portrait 状態では 縦に長く、Landscape 状態では、横に長くなります。

そこで、Portlait 状態では、縦にレイアウト(VStack) し、Landscape 状態であれば、横にレイアウト(HStack) してくれるレイアウトが欲しくなります。

そのようなレイアウトを実現する方法を説明します。

デバイスの向きに合わせて以下のようにレイアウトを変更します。

UIDecive.current.orientation

レイアウトを変更するためには、その時点でのデバイスの向きがわからないと変更することができません。

iOS/iPadOS では、UIDevice.current.orientation を参照することで、デバイスの向き情報を取得することができます。

Apple のドキュメントは、こちら

この orientation は、UIDeviceOrientation として enum 定義されていて 以下の7つの値をとり得ます。

  • unknown
  • portrait
  • portraitUpsideDown
  • landscapeLeft
  • landscapeRight
  • faceUp
  • faceDown

その名前からどういう状態を意味しているかは、自明ですね。

便利な関数も用意されています。

  • isPortrait: Bool
  • isLandscape: Bool
  • isFlat: Bool
  • isValidInterfaceOrientation: Bool

これもほぼ自明ですね。isValidInterfaceOrientation は、Portrait のいずれかの状態 もしくは、Landscape のいずれかの状態 だと true が返るプロパティです。

Apple のドキュメントは、こちら

デバイスの向きが変更された時の検知

次に、UIDevice.current.orientation をチェックするタイミングを考えます。

デバイスの向きが変更された時に通知される notification があります。

orientationDidChangeNotification です。

Apple のドキュメントは、こちら

この Notification は、NotificationCenter から通知されます。

Notification を publisher 経由で受ける

せっかくなので、Combine を使って、Notification を 受け取るようにします。

NotificationCenter に、新しく追加された publisher メソッドを使うことで、SwiftUI で onReceive で受け取ることができるようになります。

NotificationCenter の publisher メソッドについての Apple のドキュメントは、こちら

onReceive を使うと、以下のようになります。


Text("Hello, World!")
    .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
        // orientation is changed !
    }

これらを組み合わせることで、デバイスの向きが変わった時に、任意のコードを実行できるようになります。

OrientationAdaptiveStack

レイアウトを行うだけのビューを作っておくと、再利用しやすくて便利です。

orientation に応じて、受けとったビューを VStack を使って 縦にレイアウトしたり、HStack を使って 横にレイアウトするようにします。

以下のようなビューになります。


// (1)
struct OrientationAdaptiveStack<Content1: View, Content2: View> : View {
    // (2)
    var firstContent: Content1
    // (3)
    var secondContent: Content2
    // (4)
    @State var orientation: UIDeviceOrientation
    
    // (5)
    init(@ViewBuilder first: () -> Content1, @ViewBuilder second: () -> Content2 ) {
        self.firstContent = first()
        self.secondContent = second()
        self._orientation = State(wrappedValue: UIDevice.current.orientation)
    }

    var body: some View {
        // (6)
        ZStack {
            if orientation == .landscapeRight || orientation == .landscapeLeft {
                // (7)
                HStack {
                    firstContent
                    secondContent
                }
            } else {
                // (8)
                VStack {
                    firstContent
                    secondContent
                }
            }
        }
        // (9)
        .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
            orientation = UIDevice.current.orientation
        }
    }
}
コード解説
  1. Generics を使って、2つのビューを受け取るようにします
  2. 1つめのビューです。Portrait であれば 上側に Landscape であれば、左側に配置されるビューです
  3. 2つめのビューです。Portrait であれば 下側に Landscape であれば、右側に配置されるビューです
  4. デバイスの向きを @State 変数に保持します。デバイスの向きが変更された時に、この変数を更新することで、ビューが更新されるきっかけになります
  5. イニシャライザです
  6. .onReceive を持つ ビューが必要なので、ZStack を使いました
  7. landscape の時に、HStack を使ってレイアウトするコードです
  8. Portrait の時に、VStack を使ってレイアウトするコードです
  9. .onReceive で notificatin を受け取り、@State 変数の orientation をアップデートします

完成図

以下のように、デバイスの向きに合わせて、動的にレイアウトを変更されるようになりました。

まとめ:デバイスの向きに合わせて、動的にレイアウトを変更する方法

デバイスの向きに合わせて、動的にレイアウトを変更する方法
  • NotificationCenter からの UIDevice.orientationDidChangeNotification を受けてレイアウトを変更する
  • NotificationCenter からの通知を publisher 化して、onReceive で受け取るのが、SwiftUI 風

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

# SwiftUI2.0 が登場したことで少し古くなってしまいましたが、いまでも 定番本です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です