ひまネタNEWS(^o^) | スキマ時間にちょっとしたネタを提供スキマ時間に、話題のまとめを一気読み 全サイト自動巡回中
HIMA NETA ORIGINAL READER
本文だけモード Zenn IT・ガジェット 約80分で読めます

iPhone Duoに備える(SwiftUI/UIKit対応)

読みやすさ この端末に設定を保存します
書体
文字
背景

広告・ランキング・関連記事を除き、配信元の安全な文字装飾だけを残しています。

この記事について

Apple Developer の Tech Talks に、iPhone Duo 対応に関するセッションが6本公開されました。この記事は、その6本を一次情報として内容を整理したものです。

ID タイトル
111461 Prepare your app for iPhone Duo
111462 Raise the bar with iPhone Duo
111463 Strike a pose with adaptive layouts on iPhone Duo
111464 Leverage multiple displays and scenes on iPhone Duo
111465 Build a great camera experience for iPhone Duo
111466 Design for iPhone Duo

各章の冒頭に、内容のもとになったセッションへのリンクを示しました。

各セッションのページには Apple による公式コードサンプルが掲載されており、これも一次情報として参照しています。API の綴りや引数の形は、原則としてこのサンプルで確認しました。

あわせて、Human Interface Guidelines の Designing for iPhone Duo も参照しています。

セッションで言及された API については、次の2つと照合しています。

  1. Apple Developer Documentation の索引(SwiftUI・UIKit・AVFoundation・AVKit の全31,243項目、2026-09-10 取得)
  2. ローカルの iOS 27.0 SDK(Xcode 27.0 の .swiftinterface とヘッダ)

コード例は、公式コードサンプルか、上記のいずれかで宣言を確認できた API についてのみ記載しています。

iPhone Duo 向けに追加される API の多くは、公式コードサンプルには登場するものの、公開ドキュメントの索引にも iOS 27.0 SDK にも含まれていません。iOS 27.1 で追加されるものと考えられます。この記事では、そうした API についてサンプルで確認できた綴りと使い方をそのまま記載し、都度の断り書きは省いています。完全なシグネチャや値の一覧は、正式なドキュメントの公開を待つ必要があります。 セッションで言及されていても確認できなかった API は、本文でその旨を明記し、コード例を書いていません。セッションでは iOS 27.1 SDK で利用可能になる API も説明されているため、「確認できなかった」は、この2つの範囲に見当たらなかったという意味であり、存在しないことを示すものではありません。


第1部 総論:何が変わるか

もとにしたセッション: Prepare your app for iPhone DuoDesign for iPhone Duo

ディスプレイ構成

対応する HIG: AnatomyDevice poses

iPhone Duo は複数のディスプレイを持ちます。閉じた状態で見える外側ディスプレイは、従来の iPhone より横長の比率になります。開くと内側ディスプレイが現れます。

iPhone Duo の6つの姿勢を示したイラスト
出典: Apple, Human Interface Guidelines — Designing for iPhone Duo

外側ディスプレイと内側ディスプレイでは、ヒンジと前面カメラの位置が異なります。

外側ディスプレイにおけるヒンジと前面カメラの位置を示した図
出典: Apple, Human Interface Guidelines — Designing for iPhone Duo

内側ディスプレイにおけるヒンジと内側前面カメラの位置を示した図
出典: Apple, Human Interface Guidelines — Designing for iPhone Duo

iPhone のアプリは以前から変化に適応してきました。新しい画面サイズ、画面形状、Dynamic Island のような特殊な領域がその例で、iPhone Duo も新しい画面形状とカメラ位置という形でその延長線上にあります。あわせて iOS 27 では、Mac の iPhone ミラーリングによって、これまでより大きなサイズへアプリをリサイズできるようになりました。開閉によるリサイズもこれと同じ仕組みです。

閉じた状態でも開いた状態でも、アプリは iPhone アプリのまま動作します。開閉に伴って発生するのはサイズ変更であり、その過程で size class の境界をまたぐ場合があります。

size class

各ディスプレイと向きに対応する size class は次のとおりです。

状態 水平 垂直
外側ディスプレイ・縦向き compact regular
外側ディスプレイ・横向き compact compact
内側ディスプレイ regular regular

デザイン面でも、外側ディスプレイは compact width、内側ディスプレイは regular width を基準にレイアウトを設計することが案内されています。

Apple は、レイアウトの大小を判断する際に size class を使うよう案内しています。あわせて、user interface idiom から画面サイズやデバイス機能を推定すること、および interface orientation で判定することを避けるよう案内しています。

// SwiftUI
struct ContentView: View {
    @Environment(\.horizontalSizeClass) private var horizontalSizeClass

    var body: some View {
        if horizontalSizeClass == .regular {
            WideLayout()
        } else {
            NarrowLayout()
        }
    }
}

出典: Raise the bar with iPhone Duo @ 10:36

// UIKit: registerForTraitChanges は iOS 17.0+
// traitCollectionDidChange(_:) は iOS 17 で非推奨になっている
final class ContentViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (self: Self, _: UITraitCollection) in
            self.applyLayout(for: self.traitCollection.horizontalSizeClass)
        }
    }
}

WideLayoutNarrowLayoutapplyLayout(for:) は、この例のための独自の型とメソッドです。

画面を直接参照する代わりに、environment、trait collection、scene の bounds を使うことが案内されています。画面の情報が必要な場合は window scene から動的に取得します。2画面のデバイスでは main screen の参照が曖昧になるため避けるよう案内されており、将来のリリースで非推奨になると述べられています。

セッションでは API 名までは示されていませんが、該当するのはいずれも既存の API です。公式ドキュメントで確認できるものを挙げます。

用途 SwiftUI UIKit
size class EnvironmentValues.horizontalSizeClass(iOS 13.0+) UITraitCollection.horizontalSizeClass
表示領域のサイズ GeometryProxy.size UIWindowScene.effectiveGeometry(iOS 16.0+)
画面の動的な取得 UIWindowScene.screen(iOS 13.0+)

避けるよう案内されている main screen の参照は UIScreen.main にあたります。公式ドキュメント上、すでに非推奨として扱われています。

// UIKit: 画面が必要な場合は window scene から取得する
let screen = window?.windowScene?.screen

出典: Prepare your app for iPhone Duo @ 4:16

画面のスケールのように、画面を経由して取得していた値は trait collection から取れます。公式コードサンプルでは次の置き換えが示されています。

func updateThumbnail(from image: UIImage) {
    // 変更前
    let screenScale = UIScreen.main.scale
    // 変更後
    let screenScale = traitCollection.displayScale
    // ...
}

出典: Prepare your app for iPhone Duo @ 9:25

safe area の非対称性

対応する HIG: Dynamic layouts

iPhone Duo では safe area と layout margins が非対称になります。layout margins が非対称になるのは、垂直方向のボタンやステータスバーに前景コンテンツを近づけつつ、反対側のマージンは保つためだと説明されています。向かい合う辺のインセットが等しいと仮定しないよう案内されています。

標準バーは safe area の外側に配置され、ステータスバーやカメラを自動的に避けます。横方向のバーは上下方向のインセットを、縦方向のバーは左右方向のインセットを提供します。

配置の指針として、操作可能な要素と前景コンテンツは safe area の内側に置き、背景はその外側やツールバー・サイドバーの背後まで広げることが案内されています。SwiftUI では既定でコンテンツが safe area 内に配置されるため、意識する必要があるのは背景を外へ広げる場合です。

// SwiftUI: 背景だけを safe area の外側まで広げる
ZStack {
    Color.accentColor
        .ignoresSafeArea()
    ContentLayer() // safe area の内側に残る
}
// UIKit: Auto Layout では safe area layout guide に制約する
NSLayoutConstraint.activate([
    contentView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
    contentView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
])
// 背景はビューの bounds を基準に配置する
backgroundView.frame = view.bounds

手動で配置する場合はビューの safe area insets を参照し、各辺のインセットを個別に処理することが案内されています。公式コードサンプルでは、前景を safe area に合わせる書き方と、左右のインセットが等しい前提を避ける書き方が示されています。

// 前景を safe area に合わせる
foreground.frame = view.bounds.inset(by: view.safeAreaInsets)

出典: Prepare your app for iPhone Duo @ 6:52

// 向かい合う辺のインセットが等しいと仮定しない
// 避ける書き方
let width = view.bounds.width - view.safeAreaInsets.left * 2
// 各辺を個別に扱う
let width = view.bounds.inset(by: view.safeAreaInsets).width

出典: Prepare your app for iPhone Duo @ 7:30

HIG は、開発者向けの参照先として SwiftUI の GeometryProxy.safeAreaInsetsEdgeInsets を返します。iOS 13.0+)と UIKit の UIView.safeAreaInsetsUIEdgeInsets を返します。iOS 11.0+)を挙げています。どちらも4辺の値を個別に持つため、非対称なインセットをそのまま扱えます。

UIKit にはこのほか UIView.LayoutRegion(iOS 26.0+)があります。safe area、layout margins、readable content の各領域を、画面の丸い角への追従を指定したうえで取得できます。layoutGuide(for:) でレイアウトガイドとして、edgeInsets(for:)directionalEdgeInsets(for:) でインセットとして受け取れます。

// 角への追従を水平方向に指定して safe area のガイドを得る
let guide = view.layoutGuide(for: .safeArea(cornerAdaptation: .horizontal))
NSLayoutConstraint.activate([
    contentView.leadingAnchor.constraint(equalTo: guide.leadingAnchor),
    contentView.trailingAnchor.constraint(equalTo: guide.trailingAnchor),
])

// インセットとして読み取る
let insets = view.directionalEdgeInsets(for: .margins(cornerAdaptation: .vertical))
// SwiftUI: 各辺のインセットを個別に読み取る
// SwiftUI は既定でコンテンツを safe area 内に配置するため、
// ここで得た値をそのまま padding として足すと二重に効く点に注意する
GeometryReader { proxy in
    let insets = proxy.safeAreaInsets
    // insets.leading と insets.trailing は非対称になりうる
    ContentLayer()
}
// UIKit: 手動レイアウトでは4辺を個別に参照する
let insets = view.safeAreaInsets
contentView.frame = view.bounds.inset(
    by: UIEdgeInsets(top: insets.top, left: insets.left,
                     bottom: insets.bottom, right: insets.right)
)

出典: Prepare your app for iPhone Duo @ 7:30

画面の角に合わせる

画面の角に UI を合わせるために、iOS 26 で導入された Concentricity の API を使うことが案内されています。

// SwiftUI: ConcentricRectangle は iOS 26.0+
ConcentricRectangle()
    .fill(.green)
    .padding(8)

出典: Prepare your app for iPhone Duo @ 4:30

UIKit では UICornerConfiguration が対応し、UIView.cornerConfiguration に設定します。半径は UICornerRadius で表し、fixed(_:) で固定値、containerConcentric(minimum:) でコンテナに対する同心の半径を指定します。

// UIKit: 4隅すべてをコンテナと同心にする
view.cornerConfiguration = .uniformCorners(radius: .containerConcentric(minimum: 0))

// 上は固定半径、下はコンテナと同心にする
view.cornerConfiguration = .uniformEdges(
    topRadius: .fixed(16),
    bottomRadius: .containerConcentric(minimum: 0)
)

corners(topLeftRadius:topRightRadius:bottomLeftRadius:bottomRightRadius:) で隅ごとに個別指定もできます。

reserved regions

対応する HIG: Reserved regions

ヒンジと内外のカメラが占める領域は reserved regions(確保された領域)と呼ばれます。すでにリサイズ対応を進めているアプリにとっては馴染みのある考え方で、iPadOS のウインドウコントロールのように、レイアウトが以前から適応してきた領域と同じように扱えばよい、と案内されています。HIG では、この領域が3種類に整理されています。

領域 存在する条件
外側ディスプレイの前面カメラ 常に存在します。Live Activities では Dynamic Island へ広がります
内側ディスプレイの前面カメラ カメラが作動しているときだけ存在します
折り目の領域 端末が部分的に開いているときに現れ、内側ディスプレイを複数の利用可能な領域に分割します

外側ディスプレイの外側カメラ領域を示した図
出典: Apple, Human Interface Guidelines — Designing for iPhone Duo

内側ディスプレイの折り目領域と内側カメラ領域を示した図
出典: Apple, Human Interface Guidelines — Designing for iPhone Duo

アラート、コンテキストメニュー、シートなどのシステムコンポーネントは折り目に応じて自動的に移動し、分割ビューのような大きなコンポーネントは列幅とマージンを内側ディスプレイの対称性に合わせて調整します。独自のコンポーネントについては、reserved region の API で領域を避けた再配置を行います。折り目は領域を分割する division region として表され、折りたたみ時にアクティブになり、平らな状態では非アクティブで幅がゼロになります。FaceTime カメラは領域を覆う occlusion region として表され、カメラの作動状態に応じてアクティブ・非アクティブが切り替わります。

領域を取得する API は既定でアクティブな領域だけを返し、非アクティブな領域も含めて取得するオプションがあります。折り目の状態によらずグリッドの列数を偶数に保つ、といった判断に使えます。

HIG では、端末が折れたときに自動で適応するレイアウトコンテナを優先すること、グリッド状のレイアウトではコンテンツがきれいに分かれるよう列数を偶数にすること、システムが自動で移動しない場合は reserved region の API で重要な要素を中央から外すことが案内されています。

reserved regions には、ヒンジやカメラを避けるだけでなく、システム UI との衝突を避けながら独自 UI を safe area の外側に配置し、使える領域を最大化する用途も説明されています。これは iOS 27.1 で導入される API で、SwiftUI では ReservedRegion、UIKit では UIViewReservedRegion と呼ばれます。独自のバーを作る場合や、画面の端から端まで使う UI を実装する場合の選択肢として挙げられました。前述の「操作要素は safe area の内側に置く」という原則に対する例外にあたります。

公式コードサンプルによると、問い合わせは SwiftUI では GeometryProxy、UIKit では UIViewreservedRegions(kind:) で行います。種別は折り目を表す .division と、カメラを表す .occlusion です。返された各領域の frame を自分のレイアウトに反映します。

// SwiftUI
GeometryReader { proxy in
    let regions = proxy.reservedRegions(kind: .division)
    let frames = regions.map(\.frame)
    // frames を避けて配置する
}

出典: Strike a pose with adaptive layouts on iPhone Duo @ 7:22

// UIKit
let regions = view.reservedRegions(kind: .division)
let frames = regions.map(\.frame)

出典: Strike a pose with adaptive layouts on iPhone Duo @ 7:03

既定ではアクティブな領域だけが返ります。非アクティブな領域も含める場合は options: .includeInactive を渡します。

let regions = proxy.reservedRegions(kind: .division, options: .includeInactive)

出典: Strike a pose with adaptive layouts on iPhone Duo @ 7:22

SDK バージョンによる差

対応状況は SDK のバージョンによって異なります。

  • 最新 SDK でビルドしていないアプリも動作します
  • iOS 27 SDK でビルドすると、内側ディスプレイのステータスバー左側まで表示領域が広がります
  • iOS 27.1 SDK では画面端まで広がり、標準のナビゲーションやツールバーのボタンがステータスバーの下に縦配置されます

UIRequiresFullScreen は引き続き尊重されますが、開閉によるアプリのリサイズは発生します。

ただし、このキー自体は Apple Developer Documentation 上で非推奨として扱われています。セッションの説明は iPhone Duo が当面このキーを尊重するという意味であり、キーが推奨され続けるという意味ではありません。なお UIRequiresFullScreen は Info.plist のキーであり、API ではありません。

向きの扱いは、外側と内側で異なります。外側ディスプレイの interface orientation は通常の iPhone と同じように振る舞います。テントのように置いて使う人がいるため、横向きに対応するよい機会だと案内されています。一方、内側ディスプレイは、アプリが宣言した supported interface orientations に従って回転しません。宣言した向きは尊重されるものの、その結果として内側ディスプレイではアプリがスケーリングされる形になり、これは Split View multitasking でも同様です。

いずれにせよ、レイアウトの判断に interface orientation を使うことは避け、size class を使うよう案内されています。

柔軟なレイアウトの原則そのものは以前から扱われており、直近では WWDC26 の「Modernize Your UIKit App」で取り上げられたと案内されています。iPhone Duo では、開く・閉じる・回転・折り曲げのすべての姿勢でリサイズが必要になるため、この原則がこれまで以上に効いてきます。

検証には Xcode 27.1 の Device Hub で iPhone Duo シミュレータを選び、開閉・回転・折り曲げの各状態を確認します。画面下部のコントロールで開閉・回転・折り曲げを切り替えます。Xcode 27.1 では、アプリ近代化用のスキルが App Resizability に改称され、SwiftUI と iPhone Duo にも対応します。


第2部 API 別の対応

2-1 適応レイアウト

もとにしたセッション: Strike a pose with adaptive layouts on iPhone Duo

この節では性質の異なる3つが出てきます。混同しやすいので先に整理します。

名前 何か
displacement(要素の移動) 設計パターン。専用の API はありません
arrangement(2ビューの配置) APIArrangementView などが該当します
reserved regions(確保された領域) API。displacement を自前で実装する際の土台になります

標準のコンテナや presentation を使っていれば多くの挙動は自動で得られます。独自に動かしたい場合に reserved regions の API を使う、という構成です。

displacement — 要素の移動(設計パターン)

同じアプリでも、外側ディスプレイの compact width と内側ディスプレイの regular width では構成が変わります。

外側ディスプレイでメールの本文を表示している画面
出典: Apple, Human Interface Guidelines — Designing for iPhone Duo

内側ディスプレイで、leading 側にメール一覧、trailing 側に本文を並べて表示している画面
出典: Apple, Human Interface Guidelines — Designing for iPhone Duo

端末を部分的に折りたたむと、同じ分割ビューが折り目に合わせて各ペインの幅を変えます。

内側ディスプレイを完全に開いた状態のメモアプリ。leading 側にメモ一覧、trailing 側に本文を表示している
出典: Apple, Human Interface Guidelines — Designing for iPhone Duo

内側ディスプレイを部分的に折りたたんだ状態のメモアプリ。折り目に合わせて各ペインの幅が変わっている
出典: Apple, Human Interface Guidelines — Designing for iPhone Duo

displacement は「要素を移動させる」という意味で、利用可能な空間に合わせて既存要素のフレームを調整するパターンです。ボタン1つからコンテナ全体まで適用できます。**これはパターンの名前であって、displacement という API があるわけではありません。**セッションでも一貫して「パターン」として説明されており、コードサンプルもありません。自分で動かす必要がある場合は、第1部で触れた reserved regions の API で領域を問い合わせ、その結果を自分のレイアウトに反映します。

移動のさせ方について、次の点が案内されています。

  • 独立して適応できる要素は単独で移動し、連携する要素は関係を保つため一緒に移動します
  • 移動元との視覚的な関係を弱める過度な移動は避けます
  • 記事、フィード、文書、リストなどの連続スクロールコンテンツは領域間で移動させません。これらはスクロールによって適応するため、移動は連続性を妨げる場合があります
  • 移動先は要素の目的と端末の使用状態に合わせます。複数の領域が適する場合は文脈との関係を優先します

姿勢ごとの例として、本のように折った状態ではアラートを trailing 側へ移動し、卓上に立てた状態では上側を離れて見るコンテンツ、下側を操作コントロールの配置先として使う構成が示されました。

システム側では、アクションシート、アラート、メニュー、ポップオーバーが reserved region の周囲へ自動的に再配置されます。分割ビューでは列幅と位置が調整され、50/50 の分割になります。

arrangement — 2ビューの配置(API)

対応する HIG: Arrangement viewsSplit views

arrangement view は「配置を決めるビュー」という意味で、primary と secondary の2つのビューを持つレイアウトコンテナです。ディスプレイのサイズや向き、size class、縦横比、reserved regions、アクティブな division region に応じて、各ビューの表示有無と配置を決めます。システム提供の arrangement は iOS 27.1 で利用できると説明されました。

内側ディスプレイ上の split arrangement を示した図。secondary ビューが leading 側半分、primary ビューが trailing 側半分を占める
出典: Apple, Human Interface Guidelines — Designing for iPhone Duo

内側ディスプレイ上の overlay arrangement を示した図。secondary ビューが全体を占め、下寄せの小さな primary ビューが重なる
出典: Apple, Human Interface Guidelines — Designing for iPhone Duo

  • split: 既定では横長なら水平、縦長なら垂直に分割します。分割軸を制限でき、その軸が主軸に対応しない場合は単一ビューを表示します。
  • overlay: 通常は重ね合わせ、端末を折ると横並びを優先します。secondary を折りたたむこともでき、Environment プロパティから重なり順を取得して表示内容を切り替えられます。

既存の HStack / VStack による分割には split、ZStack による重ね合わせには overlay への移行が案内されています。既存のパターンに当てはまらない場合の判断基準として、前景と背景の関係が明確なビューには overlay、主内容と詳細の関係があり双方を隠したくない場合には split を検討することが示されました。

HStack {                 // -> split arrangement の候補
    PlayerView()
    UpNextView()
}

ZStack {                 // -> overlay arrangement の候補
    PlayerView()
    UpNextView()
}

出典: Strike a pose with adaptive layouts on iPhone Duo @ 12:00

ArrangementView はナビゲーション基盤を提供しません。そのため入れ子に2つの制約があり、向きが逆なので注意が必要です。

  • ArrangementView中に NavigationSplitView などのナビゲーションコンテナを置かない
  • ListScrollView などスクロールするコンテナの中に ArrangementView を置かない

前掲のコード例が NavigationStack の中に ArrangementView を置いているのは、この制約に沿った形です。ナビゲーションは外側に置きます。

公式コードサンプルでは、SwiftUI は ArrangementView、UIKit は UIArrangementViewController を使います。

// SwiftUI: primary と secondary を渡し、スタイルを指定する
NavigationStack {
    ArrangementView {
        PlayerView()
    } secondary: {
        UpNextView()
    }
    .arrangementViewStyle(.split)
}

出典: Strike a pose with adaptive layouts on iPhone Duo @ 12:00

// UIKit
let arrangementVC = UIArrangementViewController()
arrangementVC.setViewController(playerVC, for: .primary)
arrangementVC.setViewController(upNextVC, for: .secondary)
let navController = UINavigationController(rootViewController: arrangementVC)

出典: Strike a pose with adaptive layouts on iPhone Duo @ 11:26

分割軸を絞る場合は .split.axes(.horizontal) のように指定します。指定した軸が主軸に対応しない場合は単一ビューの表示になります。

// SwiftUI
.arrangementViewStyle(.split.axes(.horizontal))

// UIKit
arrangementVC.updateArrangement(.split.axes(.horizontal))

出典: Strike a pose with adaptive layouts on iPhone Duo @ 13:07

overlay に切り替えるには .arrangementViewStyle(.overlay) を指定します。primary と secondary の渡し方は split と同じです。

// SwiftUI
ArrangementView {
    UpNextView()
} secondary: {
    PlayerView()
}
.arrangementViewStyle(.overlay)

出典: Strike a pose with adaptive layouts on iPhone Duo @ 13:26

overlay での重なり順は、SwiftUI では Environment プロパティ overlayArrangementZIndexInt)から、UIKit では state(for:) が返す状態の zIndex から読み取ります。値が 0 より大きいかどうかで、縮小表示と展開表示を切り替えられます。

// SwiftUI: 重なり順で表示を切り替える
struct UpNextView: View {
    @Environment(\.overlayArrangementZIndex) private var zIndex: Int

    var minimization: UpNextMinimization {
        zIndex > 0 ? .collapsed : .expanded
    }

    var body: some View {
        UpNextList(minimization: minimization)
    }
}

出典: Strike a pose with adaptive layouts on iPhone Duo @ 14:07

// UIKit: 配置状態から重なり順を読む
let primaryState = arrangementVC.state(for: .primary)
myModel.minimization = (primaryState?.zIndex ?? 0) > 0 ? .collapsed : .expanded

出典: Strike a pose with adaptive layouts on iPhone Duo @ 14:21

2-2 バーの垂直配置

対応する HIG: Vertical controls

もとにしたセッション: Raise the bar with iPhone DuoDesign for iPhone Duo

iPhone Duo は Apple 初の複数ディスプレイを持つ iPhone です。横長の比率によって水平方向の余地が増えるため、通常は上下にあるコントロールを側面へ移し、縦方向をコンテンツのために空けます。側面は指も届きやすい位置です。重要なのは、これらが新しい部品ではなく同じコンポーネントがレイアウトを変えているだけだという点です。

外側ディスプレイと、開いた状態の横向き表示では、バーが側面に配置され、縦方向のコンテンツ領域が確保されます。開いた状態の縦向き表示では水平バーになります。HIG では、内側ディスプレイの縦向きが例外であり、縦方向の空間が十分にあるため標準の水平バーを保つと説明されています。

外側ディスプレイの trailing 側に並ぶ Dynamic Island、ステータスバー、ツールバー、タブバーの位置を示した図
出典: Apple, Human Interface Guidelines — Designing for iPhone Duo

理解の仕方としては、水平バーを90度回転させて縦のスタックにしたものを想像するとよい、と説明されました。何が含まれるかはレイアウト次第で、Notes はツールバー、Clock はタブバー、Fitness はその両方を含む構成として挙げられました。

側面に移るのはアプリの要素だけではありません。Dynamic Island、ステータスバー、ツールバー(ナビゲーションボタンを含む)、タブバーが対象です。Split View multitasking で2つのアプリが内側ディスプレイを分け合う場合、各アプリは自分の外側の端に操作部を配置します。左側のアプリなら左端です。

内側ディスプレイで2つのアプリが Split View multitasking を使い、それぞれ外側の端に操作部を置いている図
出典: Apple, Human Interface Guidelines — Designing for iPhone Duo

操作部はハードウェアに対して位置を保つため、外側ディスプレイではカメラとの相対位置が変わらず、右から左へ記述する言語でも同じ側に留まります。

対応の前提

新しいバー動作に対応するには、最新 SDK での再ビルドと、ナビゲーションコンテナが提供するバーの利用が必要になります。UIToolbar / UINavigationBar / UITabBar を使って独自にバーを構成した場合、その内容は対象になりません。

// SwiftUI: コンテナが提供するバーを使う
TabView { /* ... */ }
NavigationStack { /* ... */ }
    .toolbar { /* ... */ }
NavigationSplitView { /* ... */ }

出典: Raise the bar with iPhone Duo @ 2:24

// UIKit: UINavigationController と UITabBarController に任せる
let navController = UINavigationController(rootViewController: viewController)
let tabBarController = UITabBarController()

出典: Raise the bar with iPhone Duo @ 2:39

UIKit では UINavigationControllerUITabBarController の利用が推奨されています。

配置の順序

垂直バーでは、上部に戻る・閉じるなどの主要ナビゲーションを置き、その後に完了などの主要アクションを置きます。残りの項目は元のグループを維持します。バーが水平と垂直に切り替わる場合でも、コントロールの配置に一貫性を持たせることが案内されています。

主要アクションの配置には、SwiftUI では ToolbarItemPlacement.topBarPinnedTrailing、UIKit では UINavigationItem.pinnedTrailingGroup が対応します。

// SwiftUI
.toolbar {
    ToolbarItem(placement: .topBarPinnedTrailing) {
        Button("完了") { save() }
    }
}

出典: Raise the bar with iPhone Duo @ 5:24

// UIKit
navigationItem.pinnedTrailingGroup = UIBarButtonItem(
    title: "完了", image: nil, target: self, action: #selector(save)
).creatingFixedGroup()

ナビゲーションコントローラを使っている場合、戻るボタンは自動的に追加されます。独自の戻る・閉じるボタンを leading 項目に置く場合、UIKit では leftItemsSupplementBackButtonfalse にします(既定値も false です)。SwiftUI では ToolbarItemPlacement.cancellationAction を使います。

// SwiftUI
.toolbar {
    ToolbarItem(placement: .cancellationAction) {
        // ...
    }
}

// UIKit
navigationItem.leftItemsSupplementBackButton = false
navigationItem.leadingItemGroups = [UIBarButtonItemGroup(/* ... */)]

出典: Raise the bar with iPhone Duo @ 5:00

縦配置の判定

水平バーは項目の高さが固定で幅が可変なのに対し、垂直バーは幅が固定で高さが可変になります。このため垂直バーは、シンボルだけで表現される項目に向いています。

表示形式の選び方そのものは水平バーと変わりません。上下のバーではアイコンが優先され、アイコンがなければテキストが表示されます。オーバーフローメニューに入るとタイトルとアイコンの両方が表示されます。垂直バーで加わったのは、その内容が垂直と水平のどちらの軸に向いているかという判断です。アイコンを持つ項目は垂直へ移り、テキストだけの項目は水平に残ります。

システムは項目の内容から配置を推定します。システムの編集ボタンは水平のままとなり、UIKit のカスタムビューや複雑なビューも既定では水平に配置されます。

Apple は、項目の情報をあらかじめ提供して表示形式をシステムに選択させること、画像で表示する項目にもオーバーフローや展開表示で使うタイトルを提供することを案内しています。HIG はこの用途で SwiftUI の Label を挙げています。Label はタイトルとアイコンの両方を保持するため、システムがどちらを表示するか選べます。

// SwiftUI: タイトルと画像の両方を渡す(iOS 14.0+)
ToolbarItem(placement: .primaryAction) {
    Button(action: share) {
        Label("共有", systemImage: "square.and.arrow.up")
    }
}
// UIKit: title と image の両方を設定する
let item = UIBarButtonItem(
    title: "共有",
    image: UIImage(systemName: "square.and.arrow.up"),
    target: self, action: #selector(share)
)

縦配置できる内容を増やすため、タイトルだけの項目や、テキストと画像を併記するカスタムビューを減らすことも案内されています。件数表示にはバッジを使い、未採用の場合は iOS 26 の badge API の採用が勧められています。UIKit では UIBarButtonItem.badgeUIBarButtonItem.Badge を設定します(iOS 26.0+)。

// SwiftUI: 件数を渡す
Button(action: showInbox) {
    Label("受信", systemImage: "tray")
}
.badge(unreadCount)
// UIKit: 件数、文字列、点のみの3種類
item.badge = .count(unreadCount)
item.badge = .string("New")
item.badge = .indicator()   // 数字を出さず点だけ表示する

// 配色やフォントも指定できる
let badge = UIBarButtonItem.Badge.count(unreadCount)
badge.backgroundColor = .systemRed
item.badge = badge

出典: Raise the bar with iPhone Duo @ 9:27

カスタムビューにテキストが必要かどうかの判断基準として、そのテキストが単にシンボルを補強しているだけか、それ自体で独立した情報を持つかを問うとよい、と案内されています。補助的なだけならシンボルだけで用が足ります。一方、買い物カゴのボタンに金額が出ている場合のようにテキスト自体が意味を持つなら、そのコントロールは水平バーに残すことが勧められています。

垂直配置が可能かどうかは、Environment プロパティまたは trait から取得できると説明されました。配置可能な場合は値が入り、不可能な場合は nil または未指定になります。

公式コードサンプルによると、配置軸の調整には axisBehavior を使います。シンボルとテキストが切り替わる独自項目を水平に固定する場合は .horizontalOnly を指定します。

// SwiftUI: 水平に固定する
.toolbar {
    ToolbarItem {
        SelectOrDoneButton()
    }
    .axisBehavior(.horizontalOnly)
}

// UIKit
item.axisBehavior = .horizontalOnly

出典: Raise the bar with iPhone Duo @ 8:36

逆に、カスタムビューが垂直表示に対応している場合は垂直を優先する指定を行います。

// SwiftUI: 垂直表示に対応したカスタムビュー
.toolbar {
    ToolbarItem {
        CompassView()
    }
    .axisBehavior(.verticalPreferred)
}

// UIKit
item.axisBehavior = .verticalPreferred

出典: Raise the bar with iPhone Duo @ 8:52

垂直バーがどちらの端にあるかは、SwiftUI では Environment プロパティ toolbarVerticalEdge、UIKit では traitCollection.verticalBarEdge で読み取ります。

// SwiftUI
@Environment(\.toolbarVerticalEdge) private var edge

// UIKit
switch traitCollection.verticalBarEdge { /* ... */ }

出典: Raise the bar with iPhone Duo @ 10:36

オーバーフロー

外側ディスプレイの横向き表示で、ツールバー項目がオーバーフローに収納されるようすを示した図
出典: Apple, Human Interface Guidelines — Designing for iPhone Duo

外側ディスプレイの横向き表示や、キーボードなどの UI が現れた場合、項目がオーバーフローメニューへ移ることがあります。既定ではツールバーを先に圧縮してタブバーの移動先を残します。ビューごとの設定により、タブバーを先に圧縮してアクションを残す構成にもできます。

項目は既定では下から上の順にオーバーフローし、高・低・カスタムの可視性優先度で順序を調整できます。まずグループ単位で優先度を決め、必要に応じてグループ内の項目にも設定することが案内されています。頻繁に使う操作や、バッジなどで重要な状態を示す項目は、長く表示を維持することが勧められています。

HIG では、この優先度の型として SwiftUI の ToolbarItemVisibilityPriority と UIKit の UIBarButtonItemVisibilityPriority が示されています。どちらも iOS 27.0+ です。公開されているメンバは次のとおりで、セッションで言及された「カスタム優先度」は init(higherThan:)init(lowerThan:)、および UIKit の生の値による初期化にあたります。

SwiftUI ToolbarItemVisibilityPriority UIKit UIBarButtonItemVisibilityPriority
既定 automatic standard
段階 high / low high / low
カスタム init(higherThan:) / init(lowerThan:) init(higherThan:) / init(lowerThan:) / init(rawValue:) / init(_:)

優先度は個々の項目のほか、SwiftUI では ToolbarItemGroup、UIKit では UIBarButtonItemGroup の単位でも設定できます。HIG が案内する「まずグループ単位で決める」はこの単位を指します。

// SwiftUI
.toolbar {
    ToolbarItemGroup(placement: .primaryAction) {
        Button("共有", systemImage: "square.and.arrow.up") { share() }
    }
    .visibilityPriority(.high)
}
// UIKit
let item = UIBarButtonItem(title: "共有", image: nil, target: self, action: #selector(share))
item.visibilityPriority = .high

出典: Raise the bar with iPhone Duo @ 13:21

独自のオーバーフローがある場合は、その操作を単一のシステム管理メニューへ統合することが案内されています。セッションで示されたのは SwiftUI の ToolbarOverflowMenu です。iOS 27.0 SDK には ToolbarOverflowMenu と修飾子 toolbarOverflowMenu(content:) の両方があります。UIKit では UINavigationItem.additionalOverflowItems が対応します。

// SwiftUI
.toolbar {
    ToolbarOverflowMenu {
        // 独自のオーバーフロー操作
    }
}

出典: Raise the bar with iPhone Duo @ 12:43

// UIKit
navigationItem.additionalOverflowItems = UIDeferredMenuElement.uncached { completion in
    completion([UIAction(title: "書き出す") { _ in self.export() }])
}

出典: Raise the bar with iPhone Duo @ 12:43

オーバーフローの記号については、他プラットフォームの記号を持ち込まず、省略記号をその用途に限定し、他のメニューには別の記号を使うことが案内されています。

公式コードサンプルでは、SwiftUI が toolbarVerticalCompressionBehavior(_:)、UIKit が UINavigationItem.verticalBarCompressionBehavior です。ツールバー項目を優先して残す場合は、それぞれ .prefersToolbarItems.prefersBarItems を指定します。

// SwiftUI
TabView {
    Tab("Recents", systemImage: "clock") {
        ContentView()
            .toolbarVerticalCompressionBehavior(.prefersToolbarItems)
    }
}

出典: Raise the bar with iPhone Duo @ 12:23

// UIKit
navigationItem.verticalBarCompressionBehavior = .prefersBarItems

出典: Raise the bar with iPhone Duo @ 12:23

バーの無効化とその他の挙動

下部に内容が集中する単一ページのアプリや、バー項目が閉じるボタン一つだけのシートでは、表示領域を確保するために垂直バーの無効化を検討することが案内されています。

その他、次の挙動が説明されました。

  • 分割ビューでは detail 列だけが垂直バーの対象になり、他の列は水平のままになります。展開されたインスペクタには独立した垂直バーを設けません。detail 列がすでに垂直バーを持っているため、混乱を避ける目的だと説明されています
  • 右から左へ記述する言語でも、バーは端末の同じ側に固定され、周囲のコンテンツが適応します
  • 垂直バーは水平バーと同様に既定ではスクロール端の効果を持たず、透明度を下げるアクセシビリティ設定が有効な場合に背景を表示します。可変スペーサーは縦軸では既定でサイズがゼロになり、固定スペーサーは最小サイズを維持します
  • キーボードのアクセサリバーは縦軸へ移動せず、キーボードに付随させたままにします
  • バーの向きにかかわらず、アプリ側で追加の間隔を作らないよう案内されています
  • 新しいバーのデザインへの更新や、項目を leading・trailing の端ごとにグループ化する対応がまだであれば、この機会に行うことが勧められています(この点は WWDC25 のセッションで扱われた内容です)

公式コードサンプルでは、SwiftUI が toolbarVerticalBehavior(_:).disabled を渡し、UIKit がビューコントローラで preferredVerticalBarBehavior を上書きします。

// SwiftUI
NavigationStack {
    ContentView()
        .toolbarVerticalBehavior(.disabled)
}

出典: Raise the bar with iPhone Duo @ 14:47

// UIKit
class MyViewController: UIViewController {
    override var preferredVerticalBarBehavior: UIVerticalBarBehavior {
        .disabled
    }
}

出典: Raise the bar with iPhone Duo @ 14:47

シート

シートは、外側ディスプレイでは標準で側面に操作部を配置し、内側では縦横どちらの向きでも水平バーを使います。部分的に折りたたむと折り目を避けて移動します。外側ディスプレイのシートで内容に合う場合には、縦バーを無効にする選択肢が提示されています。特にツールバーボタンが1つだけのシートが挙げられました。無効にした場合、シートは前面カメラの手前までを使う表示になり、ステータスバーも再配置されます。

UIKit 側の説明では、内側ディスプレイのシートは既定の中央配置では縦横どちらでも水平バーとなり、配置を変更した場合は左側では垂直バーなし、右側では垂直バーありとなります。シートの配置には UISheetPresentationController.preferredPlacement(iOS 27.0+)が対応します。

2-3 マルチディスプレイとシーン

もとにしたセッション: Leverage multiple displays and scenes on iPhone Duo

ヒンジ

ヒンジの状態と角度の連続的な更新を受け取る API が、SwiftUI と UIKit の両方で説明されました。SwiftUI は onHingeChange 修飾子、UIKit は UIHingeInteraction です。どちらも高レベルの状態として閉じた状態・部分的に開いた状態・完全に開いた状態の3つを提供し、あわせて角度を連続的に更新します。コンテキストが持つヒンジの値が nil の場合、アプリはヒンジのないデバイス上で動作しています。

用途の切り分けとして、インタラクションやエフェクトにはライブのヒンジデータを使い、レイアウトには arrangement と region の API を使うことが案内されています。

セッションでは、ヒンジの変化を受け取るクロージャが変更前後のコンテキストを受け取ること、部分的に開いている場合に角度から値を計算し、それ以外の場合は値をリセットする構成が説明されました。

公式コードサンプルによると、SwiftUI では onHingeChange 修飾子を使います。クロージャは変更前後のコンテキストを受け取り、コンテキストの hingenil ならヒンジのないデバイスです。hinge.status.partiallyOpen のときに hinge.angleAngle 型)から値を計算し、それ以外では戻す、という構成が示されています。

GuitarView(pitchBend: pitchBend)
    .onHingeChange { _, context in
        // hinge が nil ならヒンジのないデバイス
        if let hinge = context.hinge, hinge.status == .partiallyOpen {
            pitchBend = calculatePitchBend(angle: hinge.angle)
        } else {
            pitchBend = 0
        }
    }

出典: Leverage multiple displays and scenes on iPhone Duo @ 2:17

UIKit では UIHingeInteraction が対応します。

マルチタスキングと複数ウインドウ

iPhone Duo では全アプリがマルチタスキングに参加します。iPad や iPhone ミラーリングでのリサイズにすでに対応しているアプリは、その時点で有利な状態にあると案内されています。アプリを左右に並べる配置と、動画とアプリを上下に重ねる配置は、アプリ側からは同様に扱います。レイアウト判断には、システムが提供する size class や scene のジオメトリを利用できます。

iPhone Duo は、アプリの UI を複数インスタンス表示できる最初の iPhone です。iPad でこれに対応しているアプリは iPhone Duo でも対応します。ただし新規ウインドウを作成できるのは内側ディスプレイのみで、外側ディスプレイでは作成できません。

作成可否が動的に変わるため、新規シーン要求時のエラーを処理し、作成不可時に自動で非表示になる UIWindowScene.ActivationAction / UISceneError / UISceneError.Code / UIApplication.activateSceneSession(for:errorHandler:) / UISceneSessionActivationRequest を使うことが案内されています。

セッションではエラー型の名前までは示されていませんが、該当するのは既存の UISceneError です。要求は UIApplication.activateSceneSession(for:errorHandler:)(iOS 17.0+)で行い、失敗の理由は UISceneError.Code で判別します。

// UIKit: 新規シーンを要求し、拒否された場合に備える
let request = UISceneSessionActivationRequest(
    role: .windowApplication, userActivity: nil, options: nil
)
UIApplication.shared.activateSceneSession(for: request) { error in
    guard let code = (error as? UISceneError)?.code else { return }
    switch code {
    case .requestDenied:
        // 外側ディスプレイなど、新規ウインドウを作成できない状況
        break
    case .multipleScenesNotSupported:
        // アプリが複数シーンに対応していない
        break
    default:
        break
    }
}

UISceneError.Code にはこのほか、ジオメトリ要求に関する geometryRequestUnsupportedgeometryRequestDenied(いずれも iOS 16.0+)があります。なお requestSceneSessionActivation(_:userActivity:options:errorHandler:) は非推奨で、activateSceneSession(for:errorHandler:) の利用が案内されています。

scene accessories

scene accessories は、メイン UI に付随するコンテンツを別のディスプレイへ同時に表示する仕組みです。iPhone Duo 専用ではなく iPhone と iPad の機能で、外部ディスプレイでゲームを表示しつつ iPhone をコントローラーとして使う、といった用途が例に挙げられました。利用可否はシステムが動的に管理し、既定では有効ですが随時切り替わります。システム条件の変化に追従するため、observation tracking を使って利用可否の変化に対応することが案内されています。

この API は公式ドキュメントに掲載があり、sceneAccessory(content:) は iOS 27.0+ です。渡す内容は SceneAccessoryContent に準拠している必要があります。

カメラアプリ向けには CameraCaptureAccessory が用意されました。内側ディスプレイにメイン UI を置いたまま、外側ディスプレイへ追加の UI を表示できます。利用条件は、内側ディスプレイでの全画面表示とアクティブなカメラセッションです。

セッションでは、外側ディスプレイにテレプロンプターを表示する例が示されました。アクセサリをカメラビューに登録することで、カメラビューが表示されているときだけテレプロンプターが出ます。ツールバーのトグルで有効状態を切り替え、onAvailabilityChange で利用可否を受け取って、端末を閉じたときなどにトグルを無効化します。

CameraView(model: model)
    .sceneAccessory {
        CameraCaptureAccessory(isEnabled: $model.isEnabled) {
            TeleprompterView(model: model)
        }
        .onAvailabilityChange { newValue in
            model.isAvailable = newValue
        }
    }
    .toolbar {
        TeleprompterToggle(isEnabled: $model.isEnabled)
            .disabled(!model.isAvailable)
    }

出典: Leverage multiple displays and scenes on iPhone Duo @ 6:25

TeleprompterModelTeleprompterViewTeleprompterToggle はアプリ側で用意する型です。

SceneAccessoryContent に準拠する型としては、このほかに ExternalNonInteractiveAccessory が iOS 27.0 SDK で確認できます。外部ディスプレイへ操作を伴わないコンテンツを出す用途で、公式ドキュメントではプレゼンテーションのプレビューを表示する例が示されています。

UIKit では UISceneAccessory が対応し、利用可否は UISceneAccessoryRegistration.isAvailable で確認します。

2-4 カメラ

もとにしたセッション: Build a great camera experience for iPhone Duo

2つの前面カメラ

iPhone Duo は、正方形センサーと超広角の画角を持つ2つの前面カメラを搭載します。外側ディスプレイ側と、内側のディスプレイ下埋め込み型です。

前面位置と Wide または Ultra Wide のデバイスタイプで探索すると、Virtual Front Camera が見つかります。これは端末の開閉に応じて内側と外側の物理カメラを自動で切り替える AVCaptureDevice です。

let session = AVCaptureDevice.DiscoverySession(
    deviceTypes: [.builtInWideAngleCamera, .builtInUltraWideCamera],
    mediaType: .video,
    position: .front
)

個別のカメラと Virtual Front Camera では、利用できる機能が異なります。

解像度・フレームレート 深度
個別のカメラ 内側は1080p・最大60fps、外側は最大4K・120fps 対応
Virtual Front Camera 両カメラに共通する機能のみ、最大1080p・60fps 非対応

深度が使えるのは個別のカメラにアクセスする場合だけです。内側・外側それぞれの物理カメラには専用のデバイスタイプ(.builtInInnerUltraWideCamera.builtInOuterUltraWideCamera)が用意されており、これらを指定すると各カメラの全機能を利用できます。

個別のカメラを使う場合、端末の開閉に伴う切り替えはアプリが担当します。

カメラの向き

AVCaptureDeviceposition は前面・背面・不定の3値をとる固定の属性です。

enum AVCaptureDevicePosition: Int {
    case unspecified
    case back
    case front
}

extension AVCaptureDevice {
    var position: AVCaptureDevicePosition { get }
}

出典: Build a great camera experience for iPhone Duo @ 3:00

固定の前面・背面位置だけでは、カメラが利用者の方を向いているか判断できません。アプリのビューに対するカメラの向きと、その変化を通知する direction coordinator が説明されました。両ディスプレイを同時に使用する場合、各ビューに対応する coordinator はそれぞれのビューを基準とした向きを報告するため、ビューごとに作成することが案内されています。たとえば背面カメラは、外側ディスプレイ側の coordinator からは forward-facing、内側ディスプレイ側の coordinator からは backward-facing として報告されます。カメラアプリが両ディスプレイを同時に使う仕組みには scene accessories を利用します。

この coordinator は AVKit にあり、ビューに紐づくため main actor に隔離されます。ハンドラーに渡されるのは AVCaptureDevice ではなく AVCaptureDeviceDescriptor です。これは AVCaptureDevice を main actor で安全に扱える sendable な表現で、AVCaptureDevice を生成するのに必要な情報をすべて含み、カメラ用 actor へ安全に渡せます。変更ハンドラーから AVFoundation の API を直接使用せず、descriptor をカメラ用 actor に渡してその actor から操作するよう案内されています。

生成には、アプリのビュー、監視対象のデバイスタイプ、変更ハンドラーの3つを渡します。ハンドラーは、各カメラがそのビューから見て利用者側を向いているか(forward-facing)、反対を向いているか(backward-facing)を報告します。たとえば端末を開いてビューが内側ディスプレイへ移ると、外側の前面カメラと背面カメラはいずれも backward-facing として、内側の前面カメラが forward-facing として報告されます。

directionCoordinator = AVCaptureDeviceDirectionCoordinator(
    view: view,
    deviceTypes: [
        .builtInOuterUltraWideCamera,
        .builtInInnerUltraWideCamera,
        .builtInDualWideCamera,
    ],
    changeHandler: { [weak self] map in
        self?.updateCameraSession(map)
    }
)

出典: Build a great camera experience for iPhone Duo @ 4:06

向きの変更時には、利用者側を向くカメラからの配信を維持するために AVCaptureSession を再構成し、カメラの向きに応じてプレビューのミラーリングを検討し、UI を更新します。背面カメラが利用者側を向く場合はミラーリングすることが案内されています。

プレビュー

プレビューは、余白に操作部品を配置する構成と、ディスプレイ全体を埋める構成を選択できます。超広角前面カメラでは正方形センサーを利用して、内側ディスプレイ向けの横長比率を選べます。

// プレビューの配置
previewLayer.videoGravity = .resizeAspectFill

// 現在のアスペクト比を読み取る(dynamicAspectRatio は読み取り専用)
let current = device.dynamicAspectRatio

// 変更は専用メソッドで行う。lockForConfiguration が必要で、
// activeFormat の supportedDynamicAspectRatios に含まれる値のみ指定できる
try device.lockForConfiguration()
device.setDynamicAspectRatio(ratio) { syncTime, error in
    // 適用完了
}
device.unlockForConfiguration()

回転については、rotation coordinator の採用が勧められています。アプリがディスプレイ間を移動した際にも更新されます。採用後は、性能改善のためカメラセンサーの向きの補正を無効にすることが案内されています。この補正は iPhone Duo の全前面カメラで有効になっています。

iPhone Duo の機能を利用するには iOS 27.1 SDK を使用し、実機でプレビューを確認することが案内されています。

rotation coordinator は AVFoundation の AVCaptureDeviceRotationCoordinator で、Swift では AVCaptureDevice.RotationCoordinator として扱います(iOS 17.0+)。デバイスとプレビュー用のレイヤーを渡して初期化し、水平を保つための回転角を読み取ります。

let coordinator = AVCaptureDevice.RotationCoordinator(
    device: device, previewLayer: previewLayer
)
previewLayer.connection?.videoRotationAngle =
    coordinator.videoRotationAngleForHorizonLevelPreview

videoRotationAngleForHorizonLevelPreview は key-value observing に対応しているため、変化を監視して反映します。

補正を無効にする設定は、公式コードサンプルによると AVCapturePhotoOutputisCameraSensorOrientationCompensationEnabled です。

photoOutput.isCameraSensorOrientationCompensationEnabled = false

2-5 デザイン原則

対応する HIG: Best practicesDevice poses

もとにしたセッション: Design for iPhone DuoPrepare your app for iPhone Duo

姿勢による変化

  • 閉じた状態の外側ディスプレイでは、アプリの操作部、ステータスバー、Dynamic Island が右側に配置されます。Live Activities の到着時には Dynamic Island が縦方向に拡張します
  • 内側ディスプレイでも操作部は側面に移動します。ただし縦向きでは水平バーを使用します
  • 部分的に折りたたむと、テキストや画像、操作要素が中央の湾曲領域を避けるように移動します。縦向きでは操作要素が下半分に移ります。この移動が組み込まれているのは対応するシステムコンポーネントであり、スクロール可能なコンテンツは湾曲領域を避ける必要がありません
  • アプリを側面へドラッグすると画面を等分して2つのアプリを並べられます。操作部は各アプリの外側の端に配置されます
  • ピクチャ・イン・ピクチャの動画を画面上部に固定すると、アプリは残りの領域に合わせて縦方向にリサイズされます。部分的に折りたたむと動画が画面の半分まで拡大します

設計方針

姿勢ごとに個別のレイアウトを設計する代わりに、2つの幅の size class を対象とし、layout margins と水平方向の safe area insets を使って自由にリサイズできる設計にすることが推奨されています。固定幅、ブレークポイント、特定の画面に結び付いた寸法は避けるよう述べられています。

機能を特定の姿勢に限定することや、外側と内側のディスプレイでアプリの階層を変えることも避けるよう述べられています。卓上で使う姿勢専用のレイアウトを設ける場合も、他の姿勢と同じ操作部と基本的な階層を維持することが推奨されています。端末の使用状態にかかわらず、コンテンツ・機能・レイアウトを利用可能に保つことが案内されています。

側面領域の扱い

側面の領域はアプリの操作部とシステム表示で共有され、空間が足りない場合、アプリの操作部は自動的にオーバーフローメニューへ収納されます。上部のツールバーボタンは側面領域の上部へ、下部のボタンは下部へ移ります。タブバーは下揃えを維持します。

テキストボタンやセグメントコントロールなど、側面領域には幅が広すぎるツールバー項目は、ナビゲーションバーに残すことが推奨されています。多くのコンテンツについては、水平方向の safe area insets に合わせ、側面の操作部に隠れない配置にすることが推奨されています。

スクロールしない没入型の視覚的な UI では、操作要素が側面の操作部に遮られないことを条件に、画面全体を基準とした中央配置が提示されています。全幅の背景とインセット付きのスクロール領域を組み合わせる場合は、すべての操作要素をそのスクロール領域内に配置するよう述べられています。

内側ディスプレイの構成例

内側ディスプレイの構成例として、階層を同時表示する分割ビュー、縦積みから2列への再配置、タブバーのサイドバー表示が紹介されました。タブバーのサイドバー表示は、情報量の多いアプリに適した選択肢とされています。

// UIKit: タブバーをサイドバーとして表示する
// UITabBarController.Sidebar.Placement は .automatic / .sidebar / .tabBar(iOS 27.0+)
tabBarController.sidebar.preferredPlacement = .sidebar

出典: Prepare your app for iPhone Duo @ 5:44

内側ディスプレイではサイドバーを選択できると説明されています。

SwiftUI 側は defaultTabBarPlacement(_:)(iOS 27.0+)が対応します。引数の AdaptableTabBarPlacement.automatic / .sidebar / .tabBar を持ち、UIKit の UITabBarController.Sidebar.Placement と対称です。

// SwiftUI: タブをサイドバーとして表示する
TabView {
    // タブの定義
}
.defaultTabBarPlacement(.sidebar)

出典: Prepare your app for iPhone Duo @ 5:44

システムコンポーネントの折り目回避

シート、アラート、メニュー、ツールバーボタンなどのシステムコンポーネントには、操作要素を折り目から移動させる動作が組み込まれています。折り目回避の動作を得るため、可能な限り対応するシステムコンポーネントを使うことが推奨されています。

標準の分割ナビゲーションは、閉じた状態では単一スタックに折りたたまれ、開いた状態では列を並列またはオーバーレイで表示します。タブも各状態に適応します。ポップオーバー、コンテキストメニュー、アラートも各状態に適応します。


第3部 移行チェックリスト

もとにしたセッション: Prepare your app for iPhone DuoRaise the bar with iPhone DuoStrike a pose with adaptive layouts on iPhone DuoLeverage multiple displays and scenes on iPhone DuoBuild a great camera experience for iPhone DuoDesign for iPhone Duo

各セッションの内容を、実行可能な手順として整理しました。

ビルドと検証環境

  • iOS 27.1 SDK でビルドする
  • Xcode 27.1 の Device Hub で iPhone Duo シミュレータを選び、開閉・回転・折り曲げの各状態を確認する
  • Xcode 27.1 の App Resizability スキルを試す
  • カメラを使うアプリは実機でプレビューを確認する

レイアウト判定の見直し

  • レイアウトの大小判定を size class に置き換える
  • user interface idiom から画面サイズやデバイス機能を推定している箇所を除去する
  • レイアウト判断に interface orientation を使っている箇所を除去する
  • レイアウト判断の画面参照を、environment・trait collection・scene の bounds に置き換える。画面の情報が必要な場合は window scene から動的に取得する
  • main screen への参照を除去する(将来のリリースで非推奨になると述べられています)
  • 固定幅、ブレークポイント、特定の画面に結び付いた寸法を除去する

safe area

  • 向かい合う辺のインセットが等しいという前提を除去し、各辺を個別に処理する
  • 操作可能な要素と前景コンテンツを safe area の内側に配置する
  • 背景を safe area の外側やバーの背後まで広げる
  • Split View multitasking で、アプリを左右両側に配置して検証する

ナビゲーションとバー

  • 標準のナビゲーションコンテナが提供するバーを使う(独自構成のバーは新しい動作の対象外です)
  • ツールバー構成を、主要ナビゲーション・主要アクションの順に見直す
  • 画像で表示する項目にもタイトルを提供する
  • タイトルだけの項目や、テキストと画像を併記するカスタムビューを減らす
  • 件数表示をバッジに置き換える
  • 独自のオーバーフローを単一のシステム管理メニューへ統合する
  • グループ単位で可視性優先度を決める
  • キーボードのアクセサリバーをキーボードに付随させたままにする

レイアウトの適応

  • 中央配置のレイアウトを点検し、2列化や displacement を検討する
  • 連続スクロールコンテンツを領域間で移動させていないか確認する
  • 複雑なレイアウトや、独自 UI を safe area 外に配置する必要がある場合は reserved regions の利用を検討する
  • 折り目回避が必要な箇所で、対応するシステムコンポーネントを使う

複数ディスプレイ

  • Split View multitasking に対応する
  • 新規シーン要求時のエラーを処理し、UIWindowScene.ActivationAction を使う
  • 別ディスプレイへ出す付随コンテンツがある場合、scene accessories を検討する
  • scene accessory の利用可否の変化に observation tracking で対応する

カメラ

  • 前面カメラの探索方針を決める(Virtual Front Camera か個別のカメラか)
  • 個別のカメラを使う場合、開閉時の切り替えを実装する
  • 個別のカメラを扱う場合、direction coordinator を採用する。両ディスプレイを同時に使う場合はビューごとに作成する
  • 変更ハンドラーから AVFoundation を直接呼ばず、カメラ用 actor 経由にする
  • 向きの変更時にプレビューのミラーリングを検討する
  • rotation coordinator を採用し、その後センサーの向き補正を無効にする

付録:API の確認状況

セッションで言及された API を、Apple Developer Documentation の索引(SwiftUI・UIKit・AVFoundation・AVKit、計31,243項目、2026-09-10 取得)およびローカルの iOS 27.0 SDK と照合した結果です。

宣言を確認できたもの

公開ドキュメントまたは iOS 27.0 SDK で宣言を確認できた API です。印の意味は次のとおりです。

  • New! — iOS 27.0 で追加されたもの
  • Update! — 以前からある API で、iPhone Duo に対応するよう挙動が更新・拡張されたとセッションで説明されたもの

印のないものは、以前から変わらず利用できます。

レイアウト

用途 SwiftUI UIKit
size class horizontalSizeClass UITraitCollection.horizontalSizeClass
表示領域 GeometryProxy.size UIWindowScene.effectiveGeometry
画面の取得 UIWindowScene.screen
画面スケール displayScale traitCollection.displayScale
safe area GeometryProxy.safeAreaInsets UIView.safeAreaInsets
背景を外へ ignoresSafeArea(_:edges:) UIView.bounds
角に追従 ConcentricRectangle Update! UICornerConfiguration Update!
領域に合わせた余白 safeAreaPadding(_:)scenePadding(_:) UIView.LayoutRegion
変化の監視 onGeometryChange(for:of:action:) registerForTraitChanges(_:action:)

ナビゲーション

用途 SwiftUI UIKit
分割 NavigationSplitView Update! UISplitViewController Update!
スタック NavigationStack UINavigationController
タブ TabView Update! UITabBarController Update!
サイドバー表示 defaultTabBarPlacement(_:) New! Sidebar.preferredPlacement New!
タブバー最小化 tabBarMinimizeBehavior(_:) minimizeBehavior
シート配置 PresentationPlacement New! UISheetPresentationController.preferredPlacement New!

ツールバー

用途 SwiftUI UIKit
戻る・閉じる .cancellationAction leftItemsSupplementBackButton
主要アクション .topBarPinnedTrailing New! pinnedTrailingGroup
グループ化 ToolbarItemGroup UIBarButtonItemGroup
可視性優先度 visibilityPriority(_:) New! visibilityPriority New!
オーバーフロー ToolbarOverflowMenu New! additionalOverflowItems
バッジ badge(_:) UIBarButtonItem.Badge
タイトルとアイコン Label titleimage

シーン

用途 SwiftUI UIKit
付随表示 sceneAccessory(content:) New! UISceneAccessory New!
利用可否の監視 onAvailabilityChange(perform:) New! UISceneAccessoryRegistration.isAvailable New!
新規シーン要求 openWindowsupportsMultipleWindows UIWindowScene.ActivationAction
要求のエラー UISceneError

カメラ(AVFoundation)

AVCaptureDevice.DiscoverySession / AVCaptureVideoPreviewLayer.videoGravity / AVCaptureDevice.dynamicAspectRatio / AVCaptureDevice.RotationCoordinator

公式サンプルで確認できるが、ドキュメント未収載のもの

次の API は、セッションの公式コードサンプルに登場するため綴りと使い方は確認できますが、公開ドキュメントの索引にも iOS 27.0 SDK にも含まれていません。iOS 27.1 で追加されるものと考えられます。完全なシグネチャや値の一覧は、正式なドキュメントの公開を待つ必要があります。

分類 主なシンボル
領域 reservedRegions(kind:options:).division.occlusion.includeInactive
arrangement ArrangementViewUIArrangementViewControllerarrangementViewStyle(_:).split.overlayaxes(_:)updateArrangement(_:)state(for:)overlayArrangementZIndex
ヒンジ onHingeChangeUIHingeInteraction
バー axisBehaviortoolbarVerticalEdgeverticalBarEdgetoolbarVerticalBehavior(_:)preferredVerticalBarBehaviorUIVerticalBarBehaviortoolbarVerticalCompressionBehavior(_:)verticalBarCompressionBehavior
カメラ AVCaptureDeviceDirectionCoordinator.builtInOuterUltraWideCamera.builtInInnerUltraWideCameraisCameraSensorOrientationCompensationEnabled
シーン CameraCaptureAccessory

セッションからは判断できなかったもの

  • 折り目回避の移動量や角度などの具体的な条件

Claude Code の skill として公開しています

この記事で整理した内容を、Claude Code の skill にまとめて公開しました。

d-date/iphone-duo-skill

領域ごとに参照が分かれており、作業内容に応じて必要な部分だけが読み込まれます。

ファイル 扱う内容
SKILL.md 全体の原則と、どの参照を読むかの案内
references/layout.md size class、safe area の非対称性、画面の角、reserved regions、arrangement
references/bars.md 垂直バー、項目の配置順、軸の制御、バッジ、オーバーフロー、無効化
references/scenes.md ヒンジ、マルチタスキング、複数ウインドウ、scene accessories
references/camera.md デュアル前面カメラ、カメラの向き、プレビュー、回転
references/checklist.md 既存アプリの移行チェックリスト

インストールは npx が手軽です。

npx skills add d-date/iphone-duo-skill

グローバル(~/.claude/skills/)とプロジェクト(.claude/skills/)のどちらへ入れるかを対話的に選べます。更新は npx skills update、削除は npx skills remove です。

手動で置く場合は次のとおりです。

git clone https://github.com/d-date/iphone-duo-skill.git
mkdir -p ~/.claude/skills
cp -r iphone-duo-skill/skills/iphone-duo ~/.claude/skills/

参考リンク

Human Interface Guidelines

セッション内で参照が案内された資料

いずれもセッション中に名前で案内されたものです。

次の2つは、Build a great camera experience for iPhone Duo の中で Apple Developer Documentation の記事として案内されました。ただし、公開されているページを確認できませんでした。

  • 「Choosing a Camera by the Direction it Faces」 — direction coordinator が AVKit にあると述べた直後に、詳細はこの記事にあると案内されています
  • 「Supporting Device Rotation in Your Camera App」 — rotation coordinator をディスプレイ間で一貫して使う方法を説明したあとに、詳細はこの記事にあると案内されています

いずれも扱っている API が iOS 27.1 のものであるため、記事自体がまだ公開されていない可能性があります。

Tech Talks

Discussion

元のページを確認する

必要な場合のみ、広告などを含む配信元ページをフレームで表示できます。