Oberflächen für das iPhone Duo: vom Layout zum Code
Zwölf Kapitel, SwiftUI und UIKit zur selben Lage. Jedes Kapitel liefert ein prüfbares Urteil, zuletzt warum die Tabs unten bleiben.
Mach die Oberfläche zuerst bei jeder Breite gut. Die Klapp-Hardware gibt danach Punkte, sie ersetzt das Layout nicht. Das iPhone Duo hat ein Außendisplay, ein Innendisplay und ein Scharnier. Es ist kein neues idiom, und es darf keine zweite App hervorbringen. Der Text folgt der Lernkurve: Jedes Kapitel liefert genau ein Urteil, das du prüfen kannst. Zuerst siehst du die Schreibweise, die bricht. Dann kommt, warum das Design so sein muss. Danach stehen SwiftUI und UIKit für dieselbe Situation nebeneinander.
Die Signaturen sind gegen das iOS 27.1 SDK von Xcode 27.1 abgeglichen. Das Verhalten stammt aus Designing for iPhone Duo, Preparing your app for iPhone Duo und Three steps. Der Fließtext gibt den Stoff wieder. Er ist keine wörtliche Abschrift der Originalseiten.
So liest du. Jedes Kapitel beginnt mit „Danach kannst du“. Danach kommen das Beispiel, das bricht, die richtige Schreibweise und „Wann du es benutzt / Wann du es nicht benutzt“. Die Kontrollpunkte beantwortest du zuerst selbst. Die Antworten stehen am Ende des Textes. Die Kapitel 0–6 fragen den Falz nicht ab, benutzen kein Arrangement und lesen den Scharnierwinkel nicht. Das sind spätere Kapitel.
Kapitel 0 · Sechs Haltungen sind die Abnahme, nicht sechs Oberflächen
Danach kannst du. Siehst du ein Duo zugeklappt, aufgeklappt, halb gefaltet und gedreht, sagst du, nach welcher Breite die Oberfläche denken soll. Du schreibst nicht für jede Haltung eine eigene Fassung.
Das Beispiel, das bricht
Organisiere das Produkt nicht so: zugeklappt hochkant, zugeklappt quer, aufgeklappt hochkant, aufgeklappt quer, halb gefaltet wie ein Buch, halb gefaltet auf dem Tisch, und für jede Variante ein eigener View-Baum. Einmal auf- oder zuklappen, und die Navigation wechselt, die Buttons wechseln, die Scrollposition ist weg.
SwiftUI verzweigt nach Haltung:
enum Pose { case closed, open, half }
struct Root: View {
let pose: Pose
var body: some View {
switch pose {
case .closed: PhoneHome()
case .open: TabletHome()
case .half: BookHome()
}
}
}
UIKit verzweigt nach Haltung:
func installRoot(for pose: String) {
let root: UIViewController
switch pose {
case "closed": root = PhoneHomeController()
case "open": root = TabletHomeController()
default: root = BookHomeController()
}
// 三套控制器,三份选中项
}
Warum Apple nur zwei Breiten vorgibt
Das Duo hat ein Außendisplay und ein Innendisplay. Das Scharnier kann schließen, vollständig öffnen oder in der Mitte stehen bleiben. Menschen halten es hochkant, quer, wie ein Buch geöffnet oder aufrecht auf dem Tisch. Das sind fortlaufende Änderungen innerhalb derselben Nutzung. Die HIG verlangt: Das Außendisplay nutzt die Breite compact, das Innendisplay die Breite regular, und das vorhandene Layout wächst mit dem verfügbaren Raum. Bau die App nicht für jede Haltweise neu. Du entwirfst weiterhin ein iPhone. Die Layout-, Leisten- und Safe-Area-Regeln von iOS gelten weiter.
| Was du siehst | Was es ist | Wie du beim Entwerfen denkst |
|---|---|---|
| Zugeklappt | Außendisplay. Breiter und flacher als ein übliches iPhone | Breite compact. Im Hochformat bleibt die Höhe regular |
| Voll aufgeklappt | Großes Innendisplay, regular × regular | Wie eine natürliche Fortsetzung des iPad-Layouts, aber es bleibt eine iPhone-App |
| Halb gefaltet | Das Innendisplay wird vom Falz in zwei Seiten geteilt | Immer noch dieselbe Hierarchie. Dieses Kapitel behandelt den Falz noch nicht |
| Drehen, Split View | Verschiedene Container auf demselben Gerät | Die Breite durchläuft Zwischenwerte, nicht nur die zwei Stufen Außendisplay und volles Innendisplay |
Zur Richtung der Leisten merkst du dir zuerst einen Satz: Das Hochformat des Innendisplays ist die einzige Haltung, die die horizontalen Leisten oben und unten behält. Auf dem Außendisplay und im Querformat des Innendisplays wandern die Systemleisten an die Seite. Im Split View liegt die Leiste jeder Seite an ihrer eigenen Außenseite. Diese Details werden erst in Kapitel 5 zu Code.
Die richtige Struktur
Eine Oberfläche. Der Zustand liegt außerhalb des Layouts. Ändert sich die Breite, wechselst du nur die Anordnung, nicht die Daten.
struct Root: View {
@State private var selection: Item.ID?
var body: some View {
Home(selection: $selection)
}
}
final class RootController: UIViewController {
let model = HomeModel()
// 开合时只让同一个控制器重新布局
}
Begriffe, die du aus diesem Kapitel mitnimmst
Es gibt noch keine Duo-API. Benutz die Wörter zuerst richtig, sonst schließt du später falsch an.
Außendisplay / Innendisplay. Zugeklappt siehst du das Außendisplay, aufgeklappt das Innendisplay. Die Hauptszene wechselt das Display von selbst. Wann du es benutzt: wenn du die Hardware beschreibst und bei der Abnahme sagst, welche Seite du prüfst. Wann du es nicht benutzt: Behandle die beiden Seiten nicht als zwei Leinwände, die du selbst verwaltest.
Haltung. Zugeklappt, aufgeklappt, halb gefaltet, und dazu die Ausrichtung. Wann du es benutzt: Im Device Hub klickst du sie einzeln durch und schaust, ob dieselbe Oberfläche noch da ist. Wann du es nicht benutzt: Bau kein
switch pose, um das Layout zu wählen.compact / regular. Das Außendisplay entwirfst du in der Breite compact, das Innendisplay in der Breite regular. Wann du es benutzt: Du entscheidest, ob du eine Ebene auf einmal siehst oder zwei gleichzeitig. Wann du es nicht benutzt: Versteh regular nicht als „das ist ein iPad“. Wie du die Werte liest, steht in Kapitel 3.
Horizontale Leiste / vertikale Leiste. Das Hochformat des Innendisplays behält die Leisten oben und unten. Die übrigen üblichen Haltungen ziehen Navigation, Symbolleiste und Tab-Leiste an die Seite. Wann du es benutzt: Wenn du Leisten zeichnest, zeichnest du beide Richtungen. Wann du es nicht benutzt: Zeichne keine unechte Seitenleiste, um das nachzuahmen.
Kontrollpunkt
Liegen die Systemleisten im Hochformat des Innendisplays oben und unten oder an der Seite?
Wie viele Oberflächen sollst du für die sechs Haltungen bauen?
Wird das Duo nach dem Aufklappen zu einer iPad-App?
In diesem Kapitel noch nicht. Schreib kein if isDuo, lies den Scharnierwinkel nicht, frag den Falz nicht ab. Du musst jetzt nur ablehnen, dass jede Haltung eine eigene UI bekommt.
Quelle: HIG · Designing for iPhone Duo, Tech Talk 111466.
Kapitel 1 · Wird es breiter, leg die nächste Ebene frei, statt Sätze auf volle Breite zu ziehen
Danach kannst du. Bekommst du einen Bereich, der breiter wird, entscheidest du, ob eine Spalte dazukommt, eine weitere Spalte, oder ob nur die bisher schmale Oberfläche auseinandergezogen wird. Du kannst auch sagen, was über den Falz hinweg darf und was nicht.
Das Beispiel, das bricht
Die Einstellungsseite ist auf dem Außendisplay eine angenehme Spalte. Nach dem Öffnen des Innendisplays zieht sich jede Zeile derselben Liste fast über den ganzen Bildschirm. Titel und Schalter sitzen an den beiden Enden, dazwischen eine große leere Fläche. Die andere kaputte Variante: Der Hauptbutton, ein QR-Code oder ein Gesicht liegt genau in der Mitte des Bildschirms und wird beim halben Falten vom Falz zerschnitten.
Vier Prinzipien, die du in ein Review mitnehmen kannst
Funktion und Zustand bleiben durchgängig. Was zugeklappt möglich ist, ist aufgeklappt noch da. Scrollposition, Auswahl und der Inhalt, den jemand gerade bearbeitet, dürfen durch Auf- und Zuklappen nicht zurückgesetzt werden. Was sich ändert, ist, wie viele Ebenen du auf einmal siehst.
Die zusätzliche Breite legt Hierarchie frei. Mail ist auf dem Außendisplay „Liste, und erst nach dem Tippen der Brief“. Auf dem Innendisplay können Liste und Brief nebeneinander stehen. Das ist eine weitere sichtbare Ebene, nicht eine Briefzeile von 900 pt Breite.
Der Falz ist wie ein Buchrücken. Hintergründe, Verläufe und Artikel, die scrollen, dürfen darüber hinweggehen. Buttons, Sätze, Gesichter, QR-Codes und Ziehgriffe dürfen nicht auf der Mitte reiten. Dieses Kapitel baut nur dieses Urteil auf. Wie du den Falz abfragst, ist Kapitel 7.
Ein Steuerelement bleibt bei dem Inhalt, auf den es wirkt. Buttons, die eine Liste steuern, bleiben über der Liste. Nur weil an der Seite eine vertikale Leiste steht, schiebst du sie nicht alle weit weg.
Ein Spiel füllt die aktuelle Haltung. Änder zuerst das Seitenverhältnis des Bildes, statt oben und unten oder links und rechts schwarze Balken zu lassen. Die physische Größe von Text und Buttons hältst du möglichst stabil.
Dieselbe Einstellungsseite, zwei Breiten
Gib langem Text und Formularen eine lesbare Obergrenze. Den übrigen Raum lässt du der Ebene daneben, nicht längeren Sätzen.
Form {
Toggle("提醒", isOn: $reminders)
Text("说明保持在容易扫读的宽度里。")
}
.frame(maxWidth: 700)
.frame(maxWidth: .infinity)
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let readable = view.readableContentGuide.layoutFrame
formWidth.constant = min(readable.width, 700)
}
Reicht die Breite dieses Bereichs schon für „Liste“ und „Detail“ gleichzeitig, macht das nächste Kapitel das mit der System-Split-Ansicht. Schreib jetzt keine Seitenleiste von Hand als HStack, und vergrößere die Schrift nicht um eine Stufe, nur weil es breiter wurde.
Fragen fürs Review, noch keine API
Frag, bevor du verbreiterst. Dauerhafte Navigation, ergänzendes Detail, Inspektor, Vorschau, mehr Steuerelemente, höhere Informationsdichte. Wann du es benutzt: Gehst du von compact zu sehr breit, suchst du in dieser Reihenfolge die Struktur, die gleichzeitig sichtbar werden kann. Wann du es nicht benutzt: Vergrößere nicht zuerst Abstände und Schrift, um die Leere zu füllen.
Buchrücken-Test. Stell dir in der Mitte eine Naht vor, die sich nicht zuverlässig treffen lässt. Darüber hinweg darf: ein fotografischer Hintergrund, eine Textur, eine Liste oder ein Artikel, die durchgehend scrollen. Nicht darüber hinweg: Buttons, ein kurzer Satz, den man zu Ende lesen muss, ein Gesicht, ein QR-Code, ein Eingabefeld, ein kleines Symbol.
Nahe beieinander gruppieren. Häufige Aktionen bleiben neben dem Block, den sie betreffen. Wann du es benutzt: Der Filter einer Liste bleibt an der Liste. Das Teilen eines Details bleibt am Detail. Wann du es nicht benutzt: Wirf die Hauptaktion nicht in eine ferne Ecke, nur damit es symmetrisch aussieht.
Spiele füllen die Fläche. Ändert sich die Haltung, änderst du das Seitenverhältnis des Bildes. Wann du es benutzt: Du darfst Hochformat oder Querformat sperren, die aktuelle Haltung muss aber voll sein. Wann du es nicht benutzt: Flick die Lücken nicht mit schwarzen Balken. Musst du doch einen Rand lassen, füll ihn mit Bild, nicht mit einer flächigen Farbe.
Kontrollpunkt
Woran liegt das Problem, wenn du das Einstellungsformular auf dem Innendisplay auf 900 pt Breite ziehst?
Was darf beim halben Falten in der Mitte bleiben: der Verlauf im Hintergrund oder der Button „Bezahlen“?
Soll „Alle auswählen“ in der Mailliste mit der Liste gehen oder mit der Systemleiste ganz rechts?
In diesem Kapitel noch nicht. Bau die ganze Seite nicht für das halbe Falten neu, und schreib jetzt noch keinen Code, der den Falz abfragt. Du markierst im Entwurf nur die Elemente, die nicht auf der Mitte reiten dürfen.
Kapitel 2 · Welches SDK du zum Kompilieren verwendest, bestimmt, wie voll die Fläche wird
Danach kannst du. Du unterscheidest Deployment Target und Kompilier-SDK und beurteilst, warum ein Paket, das sich auf dem Duo schon öffnen lässt, trotzdem schwarze Ränder behält und die Leiste noch oben liegt.
Das Beispiel, das bricht
Ein mit dem iOS 26 SDK gebautes Paket lässt sich installieren. Nach dem Öffnen des Innendisplays sitzt das Fenster in der Mitte, ringsum ist leer. Zugeklappt bleibt der Inhalt links von Statusleiste und Kamera stehen. Manche schreiben daraufhin einen zweiten Patch: „Wenn es ein Duo ist, zieh das Frame auf volle Größe.“
Drei SDK-Stufen, kein eigener Duo-Schalter
Volle Fläche und vertikale Leiste kommen mit dem Kompilier-SDK, nicht mit einem Booleschen Wert in der Info.plist. Das Deployment Target darf auf einem älteren System bleiben, damit sich die App auf älteren Geräten weiter installieren lässt. Soll das Paket auf dem Duo die Fläche füllen und die Systemleisten in die vertikale Form bringen, muss es mit dem iOS 27.1 SDK oder einem neueren SDK kompiliert sein. Bevor du ein 27.1-Paket auslieferst, prüf, dass der Inhalt noch treffbar ist, nachdem die Leisten vertikal geworden sind.
| SDK, mit dem du kompilierst | Innendisplay beim Öffnen | Leisten |
|---|---|---|
| iOS 26 oder älter | Zentriert, ringsum leer. Der Inhalt des Außendisplays liegt links von Statusleiste und Kamera | Deine bisherigen horizontalen Leisten bleiben |
| iOS 27 | Füllt den größten Teil des Innendisplays, weicht der Statusleiste rechts aber noch aus | Die Symbolleiste bleibt oben |
| iOS 27.1 oder neuer | Nutzt die Anzeigefläche voll | Symbolleiste und Tab-Leiste stehen unter der Statusleiste als vertikale Leiste |
Das Deployment Target darf niedriger bleiben, Aufrufe brauchen eine Schranke
Die nächsten beiden Abschnitte rufen noch keine Klapp-API auf. Sie legen nur die Gewohnheit fest, die jedes spätere Kapitel hält: Symbole von 27.1 stehen in einer Verfügbarkeitsprüfung. Auf Mac Catalyst ist ein bekanntes Problem von Xcode 27.1, dass diese 27.1-Symbole sich nicht kompilieren lassen und noch einmal getrennt werden müssen.
var body: some View {
Home()
.modifier(DuoReadyChrome())
}
struct DuoReadyChrome: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 27.1, *) {
content // 第 5 章才把垂直栏调优放进来
} else {
content
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
if #available(iOS 27.1, *) {
// 同一个控制器。不要在这里换 root。
}
}
#if !targetEnvironment(macCatalyst)
// 27.1 专有调用放这里,避免 Catalyst 编不过
#endif
Fakten, die mit dem Kompilieren zusammenhängen
Deployment Target
IPHONEOS_DEPLOYMENT_TARGET. Wann du es benutzt: Es bestimmt das älteste System, auf dem sich die App installieren lässt. Wann du es nicht benutzt: Es auf 27.1 anzuheben ist nicht die Voraussetzung dafür, den Bildschirm zu füllen. Ob die Fläche voll wird, entscheidet das Kompilier-SDK.iOS 27.1 SDK. Der Schalter für volle Fläche plus vertikale Leiste. Wann du es benutzt: Wenn du auf dem Duo die vollständige Form ausliefern willst, kompilierst du mit diesem SDK. Wann du es nicht benutzt: Liefer das 27.1-Paket nicht aus, bevor die vertikalen Leisten angepasst sind.
#available(iOS 27.1, *). Liegt das Deployment Target unter 27.1, müssen neue Symbole eingepackt sein. Wann du es benutzt: bei jedem Aufruf einer 27.1-API ab Kapitel 5. Wann du es nicht benutzt: Pack damit keine gewöhnliche Size Class ein. Die gibt es auf früheren Systemen schon.Mac Catalyst
#if !targetEnvironment(macCatalyst). Wann du es benutzt: Lassen sich 27.1-eigene APIs für das Catalyst-Ziel nicht kompilieren, trennst du diese Aufrufe ab. Wann du es nicht benutzt: Streich deshalb nicht die Anpassung auf dem iPhone. Das Kamera-Zubehör selbst ist auf Catalyst nicht verfügbar.
Kontrollpunkt
Das Deployment Target ist weiter iOS 18, kompiliert wird aber mit dem 27.1 SDK. Erscheint die vertikale Leiste?
Das Paket startet auf dem Duo schon, ringsum ist leer. Ist der nächste Schritt, das Gerätemodell zu erkennen und das Frame aufzuziehen, oder das SDK zu wechseln?
Warum darf ein neuer Aufruf von 27.1 nicht direkt im Codepfad von iOS 18 stehen?
In diesem Kapitel noch nicht. Lies keine Bildschirmgröße, um die Fläche zu füllen. UIScreen.main ersetzt erst das nächste Kapitel. Die Regeln für Einträge der vertikalen Leiste sind auch noch nicht dran.
Quelle: Three steps to make your app shine on iPhone Duo. Xcode 27.1 braucht macOS Tahoe 26.6 oder neuer. Der Simulator klappt, dreht und faltet im Device Hub.
Kapitel 3 · Frag, wie breit dieser Bereich ist, nicht ob das ein Duo ist
Danach kannst du. Layout-Verzweigungen, die nach Gerät, Ausrichtung oder UIScreen.main gehen, rechnest du nach Size Class und der aktuellen Containergröße neu.
Das Beispiel, das bricht
Dasselbe Duo kann auf dem Außendisplay, im Vollbild des Innendisplays und im Split View des Innendisplays erscheinen. Safe Area und Leisten ändern sich zusätzlich. Liest du den Bildschirm einmal beim Start, ist der Wert nach einem Falten veraltet. Das Innendisplay ignoriert auch die Ausrichtung, die du in Info deklarierst.
let tallPhone = UIScreen.main.bounds.height == 844
let scale = UIScreen.main.scale
if UIDevice.current.userInterfaceIdiom == .pad
|| horizontalSizeClass == .regular {
IPadOnlyLayout()
}
let window = UIWindow(frame: UIScreen.main.bounds)
if UIDevice.current.orientation == .landscape { wide() }
if view.window?.windowScene?.effectiveGeometry
.interfaceOrientation.isLandscape == true { wide() }
let width = view.bounds.width
- view.safeAreaInsets.left * 2
Zwei Fakten, die man leicht vertauscht
Die Breite regular ist nicht mehr gleich iPad. Das Innendisplay ist regular breit und regular hoch. Code, der früher bei regular auf eine iPad-eigene Seite ging, läuft jetzt auf dem iPhone. Apple verlangt: Das Innendisplay soll wie eine natürliche Fortsetzung deines iPad-Layouts aussehen. Reparier dieses Layout, statt es mit einem weiteren idiom-Urteil abzuschalten.
Die Ausrichtung ist nicht die Form. Ob es jetzt breiter als hoch oder höher als breit ist, vergleichst du an Breite und Höhe des Bereichs, den du besitzt. Das Außendisplay im Hochformat ist compact in der Breite und regular in der Höhe. Das Außendisplay im Querformat ist in beiden Richtungen compact. Das Innendisplay ist in beiden Richtungen regular. Eine iPhone-App kann von hier an jede Kombination von Size Classes treffen.
Die Größe ändert sich mitten in der Sitzung. Merk dir in viewIsAppearing nicht einen Wert und benutz ihn dann bis zum Schluss.
Neu rechnen nach dem Container
struct Stage: View {
@Environment(\.horizontalSizeClass) private var widthClass
@Environment(\.verticalSizeClass) private var heightClass
@Environment(\.displayScale) private var displayScale
@State private var size: CGSize = .zero
var body: some View {
content
.onGeometryChange(for: CGSize.self) { proxy in
proxy.size
} action: { newSize in
size = newSize
}
}
private var isWiderThanTall: Bool { size.width > size.height }
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let bounds = view.bounds
let wider = bounds.width > bounds.height
let scale = traitCollection.displayScale
let usable = bounds.inset(by: view.safeAreaInsets)
layout(for: usable.size, wider: wider, scale: scale)
}
override func viewWillTransition(
to size: CGSize,
with coordinator: UIViewControllerTransitionCoordinator
) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate { _ in self.view.setNeedsLayout() }
}
let window = UIWindow(windowScene: scene)
Brauchst du wirklich ein UIScreen, holst du es in dem Moment von view.window?.windowScene?.screen und speicherst es nicht als Property. Das Fenster erzeugst du aus der Scene. Nimm die Bounds des Hauptbildschirms nicht als Frame.
API
horizontalSizeClass. SwiftUI:@Environment(\.horizontalSizeClass). UIKit:traitCollection.horizontalSizeClass. Wann du es benutzt: um die Breite compact von regular zu unterscheiden, zusammen mit der vertikalen Size Class. Wann du es nicht benutzt: Übersetz.regularnicht in „iPad-Seite“.onGeometryChange. SwiftUI bekommt bei einer Größenänderung dieCGSizedes Containers. UIKit entsprichtviewDidLayoutSubviewsundviewWillTransition(to:with:). Wann du es benutzt: um Breite und Höhe zu vergleichen oder lokal am aktuellen Container zu messen. Wann du es nicht benutzt: Lies damit nichtUIScreen.main.bounds.displayScale. SwiftUI:@Environment(\.displayScale). UIKit:traitCollection.displayScale. Wann du es benutzt: wenn du das Pixelverhältnis brauchst. Wann du es nicht benutzt: Lies nicht mehrUIScreen.main.scale. Der Verweis auf den Hauptbildschirm wird verworfen.UIWindow(windowScene:). Erzeugt das Fenster aus der Scene. Wann du es benutzt: als Ersatz fürUIWindow(frame: UIScreen.main.bounds). Wann du es nicht benutzt: Die Layoutrechnung sieht weiter die Bounds der View oder des Fensters, nicht den ganzen physischen Bildschirm.
Kontrollpunkt
Kann
horizontalSizeClass == .regularjetzt auch auf dem iPhone zutreffen?Das Innendisplay liegt quer. Ist
interfaceOrientationzuverlässig, um zu urteilen „jetzt ist es breiter“?Warum verrechnet
safeAreaInsets.left * 2die nutzbare Breite?
In diesem Kapitel noch nicht. Schalte mit einer Breitenschwelle if width > 700 nicht zwischen zwei Navigationsbäumen um. Das nächste Kapitel löst „schmal: Push, breit: nebeneinander“ mit einem Systemcontainer.
Quelle: Step 2 von Three steps und die Hinweise zum Resizing in Preparing. Tech Talk 111461.
Kapitel 4 · Lass den Systemcontainer über Nebeneinander oder Push entscheiden
Danach kannst du. Mit einem Split-Container behältst du die Auswahl, mit einer Mindestbreite der Einträge bestimmst du die Spaltenzahl des Gitters, und lokal ordnest du neu nach „welche Anordnung passt“, nicht nach „auf welchem Gerät bin ich“.
Das Beispiel, das bricht
compact benutzt einen NavigationStack, regular eine von Hand geschriebene Seitenleiste als HStack. Die Auswahl liegt in beiden getrennt. Das Gitter heißt „Duo drei Spalten, sonst eine“. Eine Button-Gruppe zerfällt mit if width > 700 in zwei völlig verschiedene Views für horizontal und vertikal.
Reihenfolge
Kannst du einen System-Navigationscontainer nehmen, nimm den Container. Wiederholte Karten bekommen ein adaptives Gitter. Lokal, wo „horizontal passt es nicht mehr, also vertikal“, nimmst du ein Layout, das den restlichen Raum ausprobieren kann. Die Size Class kommt danach. Genaue Geometrie bleibt bis zum Schluss. Die Informationsarchitektur der Split-Ansicht ist „Sammlung, Auswahl, Detail“. Schmal schiebt die Liste das Detail per Push. Breit stehen sie nebeneinander. Die Auswahl muss diese Änderung überleben. Die zusätzliche Breite legt diese Ebene frei. Langer Text bleibt in der Breite begrenzt.
Liste und Detail
NavigationSplitView {
List(items, selection: $selection) { item in
Text(item.title)
}
} detail: {
if let selection {
Detail(id: selection)
.frame(maxWidth: 700)
} else {
Text("选择一项")
}
}
let split = UISplitViewController(style: .doubleColumn)
split.setViewController(list, for: .primary)
split.setViewController(detail, for: .secondary)
// 选中项放在 list 与 detail 共用的 model 上
// preferredPrimaryColumnWidth 用 min / ideal / max
// 不要写死侧栏点数
Kartenraster: Mindestbreite, nicht Spaltenzahl
private let columns = [
GridItem(.adaptive(minimum: 220))
]
LazyVGrid(columns: columns, spacing: 12) {
ForEach(cards) { Card($0) }
}
func makeLayout() -> UICollectionViewLayout {
UICollectionViewCompositionalLayout { _, env in
let width = env.container.effectiveContentSize.width
let count = max(1, Int(width / 220))
let item = NSCollectionLayoutItem(
layoutSize: .init(
widthDimension: .fractionalWidth(1 / CGFloat(count)),
heightDimension: .estimated(160)))
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: .init(
widthDimension: .fractionalWidth(1),
heightDimension: .estimated(160)),
subitem: item, count: count)
return NSCollectionLayoutSection(group: group)
}
}
Schneidet dich der Split View auf eine mittlere Breite, fällt die Spaltenzahl von selbst auf 2, statt von 3 abrupt auf 1 zurückzuspringen. Soll die Spaltenzahl beim halben Falten gerade sein, ist das Kapitel 7. Schreib das jetzt nicht.
Dieselbe Gruppe von Views wechselt die Anordnung, nicht die Identität
ViewThatFits(in: .horizontal) {
HStack { Preview(); Inspector() }
VStack { Preview(); Inspector() }
}
let layout = isWiderThanTall
? AnyLayout(HStackLayout())
: AnyLayout(VStackLayout())
layout {
Chart()
Legend()
}
UIKit hat kein ViewThatFits. Der entsprechende Gedanke: Beim Layout fragst du „passt es horizontal noch?“, dann änderst du die Achse desselben UIStackView. Zerstöre die Kind-Controller nicht und erzeuge sie nicht neu, sonst geht der Text verloren, der gerade bearbeitet wird.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let horizontal = stack.bounds.width
> preview.intrinsicContentSize.width
+ inspector.intrinsicContentSize.width
stack.axis = horizontal ? .horizontal : .vertical
}
API
NavigationSplitView. UIKit:UISplitViewController. Wann du es benutzt: Sammlung, Auswahl, Detail. Schmal per Push, breit nebeneinander. Wann du es nicht benutzt: Ersetz es nicht durch eine von Hand geschriebene Seitenleiste alsHStack. Es ist nicht die Zwei-Inhalte-Anordnung aus Kapitel 8.GridItem(.adaptive(minimum:)). UIKit: Teile die Containerbreite durch die Mindestbreite eines Eintrags und du bekommst die Spaltenzahl. Wann du es benutzt: Karten, Alben, Dashboards, also wiederholter Inhalt. Wann du es nicht benutzt: Schreib nichtisDuo ? 3 : 1.ViewThatFits. Probiert entlang der gegebenen Achse, welcher Teilbaum passt. Wann du es benutzt: Button-Gruppen, Metadaten, Vorschau plus Inspektor, also eine lokale Wahl. Wann du es nicht benutzt: Mach damit nicht die Navigation der ganzen App.AnyLayout. Tauscht den Layout-Container und behält die Identität der Kind-Views. Wann du es benutzt: Diagramm und Legende, Vorschau und Eigenschaften. Die Anordnung ändert sich, es bleiben dieselben zwei Views. Wann du es nicht benutzt: Schreib die Kind-Views nicht je einmal in zweiif-Zweige.
Kontrollpunkt
Wo soll die Auswahl liegen, wenn du von nebeneinander zurück auf eine Spalte faltest?
Nach dem Split View reicht die Breite nur für zwei Karten von 220 pt. Woher kommt die Spaltenzahl?
Beantwortet
ViewThatFits„welches Gerät“ oder „welche Anordnung passt“?
In diesem Kapitel noch nicht. Ob die Tab-Leiste zur Seitennavigation wird, bleibt beim nächsten Kapitel. Zwei Inhalte, die dem Falz ausweichen sollen, bleiben bei Kapitel 8. Führ jetzt kein Arrangement ein.
Kapitel 5 · Systemleisten stellen sich selbst an die Seite, nachgebaute nicht
Danach kannst du. Navigation, Symbolleiste und Tab-Leiste bleiben in der horizontalen und in der vertikalen Form erkennbar, und Nebenaktionen übergibst du dem Überlaufmenü des Systems.
Das Beispiel, das bricht
Oben auf der Seite steht ein selbst gezeichneter HStack, oder eine einzeln mit new erzeugte UINavigationBar, UIToolbar, UITabBar. Das System nimmt diese Inhalte nicht in die vertikale Leiste auf. Ein anderes häufiges Problem: Der Teilen-Button hat nur den Titel "Share". Die vertikale Leiste zeigt nur das Symbol. Ein Eintrag ohne Symbol stellt sich überhaupt nicht senkrecht. Zeichnest du selbst noch ein Menü „…“, kämpft es mit dem Überlaufmenü des Systems um den Platz.
Wann die Leiste vertikal ist, und wer hinein darf
Nach dem Kompilieren mit dem 27.1 SDK zieht das Außendisplay Navigationsleiste, Symbolleiste und Tab-Leiste zu einer Leiste an der Seite zusammen. Das Querformat des Innendisplays und manche Views an der Position leading oder trailing tun das ebenfalls. Das Hochformat des Innendisplays benutzt weiter die horizontale Leiste. In einer Split-Ansicht nimmt nur die Detailspalte an der vertikalen Leiste teil. Seitenleiste und Inhaltsspalte bleiben horizontal. Der Inspektor bleibt horizontal.
Die Leiste richtet sich an der Hardwarekante aus. Eine Sprache von rechts nach links legt sie nicht auf die andere Seite. Im Split View liegt die Leiste der linken App links und die Leiste der rechten App rechts.
Ein Eintrag braucht gleichzeitig Titel und Symbol. Die vertikale Form benutzt das Symbol. Die horizontale Form bevorzugt das Symbol und darf den Titel benutzen. Im Überlaufmenü erscheinen Symbol und Titel zusammen. Eine eigene View bleibt standardmäßig in der horizontalen Leiste, außer du erklärst, dass sie vertikal stehen darf. Ein reiner Textbutton bleibt in der horizontalen Leiste. Der Edit-Button des Systems wechselt genau so zwischen Symbol und Text.
Oben in der vertikalen Leiste bleiben Zurück oder Schließen, danach eine auffällige Aktion wie Done. Die übrigen Einträge behalten ihre bisherige Gruppierung. Der Überlauf sammelt standardmäßig von unten nach oben. Eine Seite vom Typ Navigation behält zuerst die Tab-Leiste. Eine Seite, auf der man etwas erledigt, behält zuerst die Werkzeugeinträge.
Dieselbe Gruppe von Aktionen, beide Leisten können sie benutzen
SwiftUI hängt im NavigationStack:
NavigationStack {
DetailView()
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Close", systemImage: "xmark") { dismiss() }
}
ToolbarItem(placement: .topBarPinnedTrailing) {
Button("Done", systemImage: "checkmark") { save() }
}
ToolbarItem(placement: .bottomBar) {
Button("Share", systemImage: "square.and.arrow.up") { share() }
}
.visibilityPriority(.high)
ToolbarOverflowMenu {
Button("Duplicate", systemImage: "plus.square.on.square") {
duplicate()
}
}
}
.toolbarVerticalCompressionBehavior(.automatic)
}
UIKit hängt am Kind-Controller im Navigationscontroller:
let close = UIBarButtonItem(
title: "Close", image: UIImage(systemName: "xmark"),
target: self, action: #selector(closeTapped))
let done = UIBarButtonItem(
title: "Done", image: UIImage(systemName: "checkmark"),
target: self, action: #selector(doneTapped))
let share = UIBarButtonItem(
title: "Share", image: UIImage(systemName: "square.and.arrow.up"),
target: self, action: #selector(shareTapped))
share.visibilityPriority = .high
share.axisBehavior = .automatic
navigationItem.leadingItemGroups = [
UIBarButtonItemGroup.fixedGroup(items: [close])
]
navigationItem.pinnedTrailingGroup = UIBarButtonItemGroup.fixedGroup(items: [done])
toolbarItems = [share]
navigationItem.verticalBarCompressionBehavior = .automatic
Den Zurück-Button setzt der Navigationscontroller selbst. Ein eigenes Schließen kommt auf die Seite leading, Done in die pinnedTrailingGroup. Wenn die Suche den Raum füllt, gehen die festgesteckten Einträge als letzte in den Überlauf. additionalOverflowItems nimmt dein eigenes „Mehr“ in das Systemmenü auf. Die Auslassungspunkte bleiben dem Überlauf. Andere Menüs bekommen ein anderes Symbol.
Einträge, deren Symbol und Text hin und her wechseln, setzt du auf .horizontalOnly: Gibt es keine horizontale Leiste, werden sie ausgeblendet, statt sich in die vertikale Leiste zu drängen. Hat eine eigene View wirklich eine vertikale Darstellung, nimm .verticalPreferred. Elastischer Leerraum hat auf der vertikalen Achse die Größe 0. Gruppen bildest du mit ToolbarItemGroup / UIBarButtonItemGroup. Schieb keinen festen Abstand von Hand dazwischen. Für eine Markierung nimm badge. Schreib die Zahl nicht in den Titel, sonst wird dieser Eintrag kaum zu einem reinen Symbol.
Die vertikale Leiste zu verlassen ist eine stabile Ausnahme
NavigationStack {
CalculatorView()
.toolbarVerticalBehavior(.disabled)
}
override var preferredVerticalBarBehavior: UIVerticalBarBehavior {
.disabled
}
Der Standard ist .automatic. Verlass die vertikale Leiste nur auf diesen Oberflächen: Vollbildvideo, ein Layout über die ganze Breite, das nicht scrollt, wie ein Taschenrechner, und ein Sheet mit nur einem Schließen-Button. Das ist eine stabile Wahl. Schalte in viewWillAppear nicht nach dem Seitenzustand hin und her. Soll nur eine Seite die Leiste verstecken, nimm die Sichtbarkeits-API, statt das vertikale Verhalten abzuschalten. Hat sich die UIKit-Präferenz wirklich geändert, ruf setNeedsUpdateOfVerticalBarConfiguration() auf. Der Navigationscontroller überlässt die Entscheidung standardmäßig der obersten Seite im Stapel. Die Tab-Leiste überlässt sie der aktuell ausgewählten Seite.
Wissen, auf welcher Seite die Leiste steht, und wie der Hintergrund darunter weiterläuft
@Environment(\.toolbarVerticalEdge) private var verticalEdge
// HorizontalEdge?;这个上下文从不用垂直栏时为 nil
Image("hero")
.backgroundExtensionEffect()
let edge = traitCollection.verticalBarEdge
// .leading / .trailing / .unspecified
// 表示系统偏好的边,不表示栏此刻是否可见
// 从不用垂直栏的上下文返回 .unspecified
let backdrop = UIBackgroundExtensionView()
backdrop.contentView = hero
Diese Kante in UIKit beobachtest du so: Übergib an registerForTraitChanges den Wert UITraitCollection.systemTraitsAffectingVerticalBarEdge. Auch wenn sich das Trait ändert, kann die Kante dieselbe bleiben.
Einstieg der ersten Ebene: Das Innendisplay kann zur Seitennavigation werden
Auf dem iPhone bleibt .sidebarAdaptable allein die Leiste unten. Soll sie dort, wo das unterstützt wird, zur Seitennavigation werden, kommt zusätzlich defaultTabBarPlacement(.sidebar) aus iOS 27 dazu. Benutz nicht defaultAdaptableTabBarPlacement aus iOS 18. Das ist für das iPad. UI, die zur Seitenleiste gehört, liest isTabViewSidebarAvailable. Rat die Size Class nicht noch einmal.
TabView {
Tab("Home", systemImage: "house") { HomeView() }
Tab("Library", systemImage: "books.vertical") { LibraryView() }
}
.tabViewStyle(.sidebarAdaptable)
.defaultTabBarPlacement(.sidebar)
tabBarController.sidebar.preferredPlacement = .sidebar
Das Produkt darf die untere Leiste auch ausdrücklich behalten und den zusätzlichen Raum der Split-Ansicht im Inhalt lassen. Beides gilt. Zeichne nicht selbst nach der Breite eine unechte Seitenleiste.
API
toolbarVerticalBehavior(iOS 27.1)..automatic/.disabled. UIKit: überschreibpreferredVerticalBarBehavior, TypUIVerticalBarBehavior. Wann du es benutzt: Taschenrechner, Vollbildvideo, Sheet mit einem einzigen Schließen-Button. Wann du es nicht benutzt: Schalte es nicht mit der Navigation oder dem Zustand einer View hin und her.axisBehavior(iOS 27.1). SwiftUI:.automatic/.horizontalOnly/.verticalPreferred. UIKit:UIBarButtonItem.axisBehavior, dieselben drei Werte. Wann du es benutzt: Einträge, die zwischen Symbol und Text wechseln, bekommen horizontalOnly. Eine eigene View mit einer echten vertikalen Darstellung bekommt verticalPreferred. Wann du es nicht benutzt: Ein gewöhnlicher Button mit Titel und Symbol bleibt automatic.visibilityPriority(iOS 27.0). SwiftUI:.automatic/.low/.high, sowieinit(lowerThan:)/init(higherThan:). UIKit:.standard/.low/.high. Wann du es benutzt: Gib zuerst der ganzen Gruppe eine Priorität, dann den einzelnen Einträgen in der Gruppe. Häufig Genutztes und Einträge mit Markierung gehen als Letzte in den Überlauf. Wann du es nicht benutzt: Bau nicht selbst ein zweites Menü „…“.ToolbarOverflowMenuundadditionalOverflowItemssind das Systemmenü.additionalOverflowItemsin UIKit gibt es seit iOS 16.Komprimierungsverhalten (iOS 27.1). SwiftUI:
.automatic/.prefersToolbarItems/.prefersTabBar. UIKit:navigationItem.verticalBarCompressionBehaviormit.automatic/.prefersBarItems/.prefersTabBar. Wann du es benutzt: Der Navigationstyp nimmt den Standard (zuerst die Werkzeugeinträge einsammeln, die Tabs behalten). Der Aufgabentyp sammelt zuerst die Tab-Leiste ein. Wann du es nicht benutzt: Schreib nicht zwei Leisten, um die Komprimierung nachzustellen.topBarPinnedTrailing(iOS 27.0). Die UIKit-PropertypinnedTrailingGroupgibt es seit iOS 16. Schließen nutzt.cancellationAction, UIKit legt es inleadingItemGroups. Wann du es benutzt: eine Hauptaktion wie Done, die nahe bei Zurück bleiben muss. Wann du es nicht benutzt: Nebenaktionen steckst du hier nicht fest. Sie sollen in den Überlauf dürfen.toolbarVerticalEdge(iOS 27.1). SwiftUI:HorizontalEdge?, nil, wenn dieser Kontext nie eine vertikale Leiste benutzt. UIKit:traitCollection.verticalBarEdge, Werte leading, trailing oder unspecified. Es gibt die bevorzugte Kante wieder, auch wenn die Leiste gerade nicht sichtbar ist. Wann du es benutzt: wenn eigener Inhalt sich an dieser Leiste ausrichten soll. Wann du es nicht benutzt: Versteh unspecified nicht als „die Leiste wurde gerade versteckt“. Es heißt, dieser Kontext benutzt keine vertikale Leiste.backgroundExtensionEffect()(iOS 26). UIKit:UIBackgroundExtensionView. LegcontentViewin den sicheren Bereich, die Ränder füllst du mit der Erweiterung. Wann du es benutzt: Ein Hauptbild oder ein Hintergrund soll unter die vertikale Leiste reichen. Wann du es nicht benutzt: Tippbarer Vordergrund soll sich damit nicht aus dem sicheren Bereich herausbohren.defaultTabBarPlacement(iOS 27.0)..sidebarAdaptableplus.sidebar. UIKit:sidebar.preferredPlacement = .sidebar. Die UmgebungswerteisTabViewSidebarAvailableundtabBarPlacementsind ebenfalls iOS 27. Wann du es benutzt: Bereiche der ersten Ebene, denen auf einem breiten Layout eine dauerhafte Navigation steht. Wann du es nicht benutzt: BenutzdefaultAdaptableTabBarPlacementnicht für das Duo. Diesen Modifier gibt es seit iOS 18, er wirkt auf dem iPad.
Kontrollpunkt
Kommt ein Button mit nur einem Titel und ohne Symbol in die vertikale Leiste?
In den drei Spalten einer Split-Ansicht: Die Leiste welcher Spalte wird vertikal?
Reicht der Platz auf einer Seite vom Typ Navigation nicht: Sammelt der Standard zuerst die Werkzeugeinträge oder zuerst die Tab-Leiste ein?
Warum stellt sich eine selbst gezeichnete
UIToolbarnicht an die Seite?
In diesem Kapitel noch nicht. Frag den Falz nicht ab, um zu entscheiden, ob die Leiste links oder rechts steht. Die Kante der Leiste legt das System in Trait und Environment. Auf welcher Seite ein Sheet liegt, ist das nächste Kapitel.
Quelle: HIG · Vertical controls; Preparing · Optimize bars. Tech Talk 111462.
Kapitel 6 · Vier Kanten getrennt rechnen, das Sheet sagt, an welcher Seite es anliegt
Danach kannst du. Du wählst die Position eines Sheets, rechnest den nutzbaren Bereich nach einer unsymmetrischen Safe Area und hältst ein bildfüllendes Bild mit einer zum Bildschirm konzentrischen Ecke und einem Fokus.
Das Beispiel, das bricht
Den linken Inset mal zwei als gesamte Safe Area zu nehmen. Split View oder eine vertikale Leiste machen links und rechts ungleich, der Inhalt sitzt dann schief. Die andere Klasse von Problemen: selbst eine Vollbildmaske als Sheet zu bauen. System-Sheet, Warnung, Menü und Popover weichen dem Falz aus. Deine Maske nicht. Das Hauptbild mit .fill beschnitten: Das Gesicht, das auf dem Außendisplay sichtbar ist, wird auf dem Innendisplay weggeschnitten.
Die Leiste des Sheets folgt der Platzierung
Auf dem Außendisplay ist ein Sheet mit Symbolleiste standardmäßig eine vertikale Leiste. Auf dem Innendisplay ist das Sheet standardmäßig zentriert und die Leiste horizontal. Auf der Seite leading gibt es keine vertikale Leiste, auf der Seite trailing schon. Nur das Sheet beachtet die Platzierung. Bau dafür keine zweite eigene Transition. Ein Sheet mit nur einem Schließen-Button ist der Fall, in dem das vorige Kapitel die vertikale Leiste abschalten darf. Hat UIKit ein sourceView, ignoriert es preferredPlacement.
.sheet(item: $selection) { item in
ItemDetail(item: item)
.presentationPlacement(.trailing)
}
let sheet = detail.sheetPresentationController
sheet?.preferredPlacement = .trailing
// .automatic / .leading / .center / .trailing
// sourceView 非 nil 时,这个属性被忽略
Die vier Kanten der Safe Area sind unabhängig
content
.safeAreaPadding(.horizontal)
background
.ignoresSafeArea()
let usable = view.bounds.inset(by: view.safeAreaInsets)
// 分别使用 left、right、top、bottom
// 折痕不是 safe area inset
Dekoration darf aus dem sicheren Bereich herausragen. Tippbarer Inhalt und Text, den man lesen muss, bleiben darin. Kamera, vertikale Leiste und Split View machen links und rechts oder oben und unten ungleich. Eine eigene Form, die an der Bildschirmecke anliegt, benutzt ein konzentrisches Rechteck. Schreib den Radius nicht fest.
RoundedRectangle(cornerRadius: 12) // 不要写死去贴屏幕角
ConcentricRectangle()
view.cornerConfiguration = .corners(radius: .containerConcentric())
Ein bildfüllendes Bild muss das Motiv behalten
Image("cover")
.resizable()
.aspectRatio(contentMode: isWide ? .fit : .fill)
.frame(maxWidth: .infinity, maxHeight: isWide ? 420 : 280)
.clipped()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let wide = view.bounds.width > view.bounds.height
hero.contentMode = wide ? .scaleAspectFit : .scaleAspectFill
hero.clipsToBounds = true
}
Liegt das Motiv nicht im geometrischen Zentrum, gib dem Beschnitt einen Fokus, statt nur zwischen fill und fit zu wechseln. Ein Spiel füllt die Fläche und ändert das Verhältnis, siehe Kapitel 1.
API
presentationPlacement(iOS 27.0)..automatic/.center/.leading/.trailing. UIKit:UISheetPresentationController.preferredPlacement. Wann du es benutzt: Jedes Sheet siehst du zugeklappt, aufgeklappt und halb gefaltet an, bevor du entscheidest, ob es an der Kante oder in der Mitte liegt. Wann du es nicht benutzt: Popover und Warnung bekommen diesen Modifier nicht. Hat UIKit einsourceView, ignoriert es ihn.Unsymmetrischer Inset.
bounds.inset(by: safeAreaInsets). Wann du es benutzt: überall, wo du nutzbare Breite oder Höhe von Hand rechnest. Wann du es nicht benutzt: Spiegel den Inset einer Seite nicht auf die gegenüberliegende. Den Falz stopfst du auch nicht in die Safe Area.ConcentricRectangle(iOS 26). UIKit: der konzentrische Radius des Containers inUICornerConfiguration.UIView.cornerConfigurationgibt es. Wann du es benutzt: Eine eigene Fläche soll an der Ecke des aktuellen Bildschirms oder Containers anliegen. Wann du es nicht benutzt: Die kleine eigene Ecke einer Karte darf weiter einen festen Radius benutzen. Nimm das nicht, um dem Falz auszuweichen.fill / fit. Wähle nach dem aktuellen Verhältnis oder der Size Class, oder setze einen Fokus. Wann du es benutzt: Hauptbild, Videocover. Vom schmalen Außendisplay bis zum breiten Innendisplay muss das Motiv bleiben. Wann du es nicht benutzt: Wähl den contentMode nicht nur einmal beim Start.
Kontrollpunkt
Ist die Symbolleiste eines trailing Sheets auf dem Innendisplay horizontal oder vertikal?
Warum bekommst du die Inhaltsbreite nicht mit
left * 2?Ein System-Sheet weicht dem Falz aus. Tut das eine selbst gezeichnete Vollbildmaske auch?
In diesem Kapitel noch nicht. Müssen eigene Leinwand, QR-Code und Zieh-Steuerelemente dem Falz ausweichen, fragst du den reservierten Bereich erst im nächsten Kapitel ab. Rat die Koordinaten des Falzes jetzt nicht.
Kapitel 7 · Falz und Kamera sind zwei reservierte Bereiche, kein sicherer Bereich
Danach kannst du. In einem eigenen Layout findest du den aktiven Falz und die Kamera und schiebst nur den verdeckten Block weg, statt die ganze Seite neu zu bauen.
Das Beispiel, das bricht
Schon bei halb gefaltet auf einen anderen Oberflächenbaum umschalten. Oder festschreiben „der Falz liegt bei 50 Prozent der Breite“. Nach Split View und Drehen ist dieses Verhältnis nicht mehr die Mitte deiner View. Das andere Ende: jeden Absatz eines Artikels vom Falz wegschieben. Scrollender Inhalt kann über diese Naht hinwegrollen.
Zwei Bereiche, die Systemkomponenten meist schon meiden
Ein reservierter Bereich ist ein Rechteck, das etwas anderes in deinem Koordinatensystem belegt. frame enthält margins. Jeder Eintrag hat eine Art, ob er aktiv ist, und eine Kennung. Die Abfrage liefert die Bereiche, die die aktuelle View schneiden. Belegt deine View nur die Hälfte des Innendisplays, nimm nicht an, du sähest den Falz des ganzen Bildschirms.
division, Teilung. Beim halben Falten teilt das Scharnier einen großen Block in zwei Seiten. Liegt das Gerät ganz flach, ist dieser Bereich nicht aktiv, die Breite ist 0.
occlusion, Verdeckung. Die Frontkamera des Außendisplays ist immer da und dehnt sich noch zur Dynamic Island. Die Frontkamera des Innendisplays liegt sonst unter dem Display. Nur während der Nutzung belegt sie einen Block und lässt die Oberfläche ausweichen.
Warnung, Menü, Sheet und Split-Ansicht passen sich selbst an. Eigene Leinwand, QR-Code, Gesicht, ein kurzer Satz, der zusammenhängend bleiben muss, und ein Objekt, das gerade gezogen wird: Dafür liest du diese Rechtecke. Schieb nur das verdeckte Element weg, und nimm die zugehörigen Steuerelemente mit. Schick es nicht ans andere Ende des Bildschirms. Einen scrollenden Artikel und eine Liste schiebst du nicht als Ganzes weg.
Zuerst die gerade wirksamen herausfiltern, dann eigenen Inhalt setzen
reservedRegions in SwiftUI kann zusätzlich layoutDirectionBehavior: bekommen, Standard ist .mirrors. Nur wenn du selbst gespiegelt hast, übergibst du .fixed, sonst spiegelst du zweimal. Die physische Position der Kamera kippt nicht mit der Sprache. SwiftUI spiegelt die Geometrie standardmäßig zuerst für dich. Der Proxy von onGeometryChange kann dasselbe abfragen.
GeometryReader { proxy in
let folds = proxy.reservedRegions(kind: .division)
.filter(\.isActive)
let cameras = proxy.reservedRegions(kind: .occlusion)
.filter(\.isActive)
CanvasPage(avoiding: folds + cameras)
}
override func layoutSubviews() {
super.layoutSubviews()
let folds = reservedRegions(kind: .division)
.filter(\.isActive)
let cameras = reservedRegions(kind: .occlusion)
.filter(\.isActive)
placeControls(avoiding: folds + cameras)
}
Eine Strukturentscheidung wie eine gerade Spaltenzahl musst du auch sehen können, wenn das Gerät flach liegt und der Teilungsbereich noch nicht aktiv ist. Erst dann übergibst du .includeInactive, und isActive entscheidet weiter, ob du die Steuerelemente sofort wegschiebst.
let divisions = proxy.reservedRegions(
kind: .division,
options: .includeInactive)
let evenColumns = !divisions.isEmpty
let divisions = reservedRegions(
kind: .division,
options: .includeInactive)
let evenColumns = !divisions.isEmpty
API
reservedRegions(kind:options:)(iOS 27.1). In SwiftUI aufGeometryProxy. UIKit:view.reservedRegions(kind:options:), Rückgabe[UIView.ReservedRegion]. Wann du es benutzt: eigener Inhalt, den ein Systemcontainer nicht für dich wegschiebt. Wann du es nicht benutzt: gewöhnliche Liste, Split-Ansicht, Sheet, Warnung. Behandle das Ergebnis nicht als Inset der Safe Area.ReservedRegion(iOS 27.1).id,kind,frame,margins,isActive. Die Arten sind.divisionund.occlusion. Wann du es benutzt: Mitframesetzt du die Position.frameenthält den Rand schon. Wann du es nicht benutzt: Eine inaktive division schiebt keine Buttons weg. Liegt das Gerät flach, darf sie die Tippposition nicht ändern..includeInactive(iOS 27.1).ReservedRegion.QueryOptions. Wann du es benutzt: eine Entscheidung, die nicht davon abhängt, ob gerade halb gefaltet ist, etwa „sobald ein Falz existiert, nutzt das Gitter eine gerade Spaltenzahl“. Wann du es nicht benutzt: Schieb einen Button nicht schon weg, nur weil du einen inaktiven Bereich gefunden hast.layoutDirectionBehavior..mirrorsoder.fixed. Wann du es benutzt: Behalt den Standard, damit automatisch angeordnete Kind-Views und die Bereiche dieselbe Richtung benutzen. Wann du es nicht benutzt:.fixedübergibst du nur, wenn du von Hand spiegelst.
Kontrollpunkt
Ist division nach dem vollständigen Flachlegen noch aktiv? Wie groß ist die Breite?
Soll ein scrollbarer Artikel jeden Absatz vom Falz wegschieben?
Belegt die Kamera des Innendisplays occlusion im Alltag?
In diesem Kapitel noch nicht. Gepaarten Inhalt wie Player plus Wiedergabeliste übergibst du im nächsten Kapitel an Arrangement. Entscheide mit dem Scharnierwinkel nicht, ob du dem Falz ausweichst.
Quelle: ReservedRegion; Preparing · Adapt to reserved regions. Tech Talk 111463.
Kapitel 8 · Bilden zwei Inhalte ein Erlebnis, lässt du den Container sie umstellen
Danach kannst du. Du urteilst, ob ein Paar von Views split oder overlay benutzt, und lässt die Navigation außerhalb dieses Containers.
Das Beispiel, das bricht
Mit Arrangement TabView oder NavigationSplitView ersetzen. Oder Arrangement in eine List, eine ScrollView oder eine Spalte der Split-Ansicht legen. Dann rollt die Hälfte des Inhalts nicht heraus und lässt sich nicht treffen.
Sieht es schon wie einer von zwei Stapeln aus, mappe es dorthin
Arrangement hat nur eine Haupt-View und eine Neben-View. Es sieht Raum, Seitenverhältnis, Size Class und den Teilungsbereich. Es sieht nicht, ob du ein Duo bist. Der Navigationscontainer liegt außen. Apples eigenes Beispiel legt ein Arrangement in einen Navigationsstapel. Verschachtel keine Split-Ansicht innerhalb von Arrangement.
split. Entspricht dem bisherigen
HStack/VStack, oder einer Haupt-Neben-Beziehung, in der niemand den anderen verdecken soll. Ist es breiter als hoch, teilt es links und rechts. Ist es höher als breit, stapelt es oben und unten. Erscheint ein Teilungsbereich, passt es beide Seiten an und weicht dem Falz aus. Beschränkst du es auf eine Achse und diese Achse lässt sich nicht nutzen, bleibt nur die Haupt-View..split.axes(.horizontal)zeigt nur den Hauptinhalt, wenn auf der horizontalen Achse nichts mehr passt.overlay. Entspricht dem bisherigen
ZStack. Gibt es keinen aktiven Teilungsbereich, liegt die Haupt-View auf der Neben-View (zugeklappt oder vollständig geöffnet). Beim halben Falten geht die Haupt-View auf die hintere oder untere Seite des Falzes, die Neben-View auf die vordere oder obere.
Player und Liste: split
NavigationStack {
ArrangementView {
Player()
} secondary: {
Queue()
}
.arrangementViewStyle(.split)
}
let arrangement = UIArrangementViewController()
arrangement.setViewController(player, for: .primary)
arrangement.setViewController(queue, for: .secondary)
arrangement.updateArrangement(.split)
navigationController.setViewControllers([arrangement], animated: false)
Nur links und rechts erlaubt, im Hochformat bleibt nur der Player:
.arrangementViewStyle(.split.axes(.horizontal))
arrangement.updateArrangement(.split.axes(.horizontal))
Steuerelemente liegen auf dem Bild: overlay
ArrangementView {
TransportControls()
} secondary: {
Stage()
}
.arrangementViewStyle(.overlay)
arrangement.setViewController(controls, for: .primary)
arrangement.setViewController(stage, for: .secondary)
arrangement.updateArrangement(.overlay)
Verhältnis und Ebene haben noch diese Regler, alle iOS 27.1: splitArrangementLayoutRatio, splitArrangementLayoutSize, splitArrangementFixedLayoutSize, overlayArrangementEdge. Die Umgebungswerte splitArrangementAxis und overlayArrangementZIndex sagen der Kind-View, in welcher Anordnung sie gerade steht. UIKit liest mit state(for:) die Werte zIndex, splitAxis, isHidden. Den Größenbereich von Haupt- und Neben-View hält UISplitArrangement.ViewProperties, die Verdeckungskante UIOverlayArrangement.ViewProperties.edge.
API
ArrangementView(iOS 27.1).initnimmt primary und secondary. UIKit:UIArrangementViewController, die Position vonsetViewController(_:for:animated:)ist.primary/.secondary. Wann du es benutzt: Player und Warteschlange, Editor und Inspektor, Leinwand und Eigenschaften. Die beiden Blöcke sind ein Erlebnis. Wann du es nicht benutzt: die Navigation der ersten Ebene der App. Leg es nicht in eine Split-Ansicht, eine Liste oder eine Scroll-View..split(iOS 27.1).arrangementViewStyle(.split),.axes(.horizontal)oder.vertical. UIKit:updateArrangement(.split.axes(...)). Wann du es benutzt: Beide Seiten brauchen eigenen Raum, niemand deckt den anderen ab. Wann du es nicht benutzt: Steuerelemente, die von vornherein auf dem Inhalt schweben sollen. Das ist overlay..overlay(iOS 27.1). Ohne Teilungsbereich liegt die Haupt-View oben. Beim halben Falten geht die Haupt-View nach hinten oder unten, die Neben-View nach vorn oder oben. Wann du es benutzt: Vordergrund und Hintergrund, die sich im Alltag verdecken dürfen. Wann du es nicht benutzt: Mail mit zwei Spalten. Das ist die Split-Ansicht aus Kapitel 4 oder hier split.updateArrangement(_:animated:)(iOS 27.1). Wechselt split / overlay oder ändert nur die Achse. Wann du es benutzt: Das Produkt will eine Achse ausdrücklich sperren oder zwischen zwei Stilen wechseln. Wann du es nicht benutzt: Rechne bei jeder Drehung nicht selbst das Frame neu und ruf es dann auf. Der Standardstil folgt dem Container schon.
Kontrollpunkt
Vorher war es ein
ZStack, beim halben Falten soll es auf beide Seiten des Falzes. split oder overlay?Soll Arrangement die Navigation umschließen, oder soll die Navigation Arrangement umschließen?
Was passiert, wenn du es in eine
ScrollViewlegst?
In diesem Kapitel noch nicht. Lies den Scharnierwinkel nicht, um zwischen split und overlay zu entscheiden. Der Winkel ist die Eingabe für Interaktion in Kapitel 10.
Quelle: die SwiftUI- und UIKit-Beispiele in Preparing; HIG · Arrangement views. Die Signaturen sind gegen ArrangementView und UIArrangementViewController im iOS 27.1 SDK abgeglichen.
Kapitel 9 · Auf- und Zuklappen ist eine Layoutänderung, kein Neustart der App
Danach kannst du. Navigation, Scrollen, Auswahl, Bearbeitung und Wiedergabe bleiben über dem Layout, und du kennst die Grenze von Split View und neuem Fenster auf dem Duo.
Das Beispiel, das bricht
Zugeklappt hat die Liste den dritten Brief ausgewählt. Aufgeklappt beginnt ein anderer View-Baum beim ersten Brief, der Entwurf ist auch leer. Die andere kaputte Variante: nur Vollbild des Außendisplays und Vollbild des Innendisplays geprüft. Auf halbem Weg im Split View überlappen sich die Leisten, Text wird beschnitten. Und die Annahme „aufgeklappt ist es bestimmt sehr breit“.
Eine durchgehende Breite, plus zwei Grenzen der Hardware
Auf dem Innendisplay nimmt jede App an Split View teil, Video kann sich auch über eine App legen. Die vertikale Leiste jeder App liegt an ihrer eigenen Außenseite. Deine Leiste kann also links oder rechts sein. Beide Seiten musst du prüfen.
Ein sehr breites Layout muss auf eine mittlere Breite zurückkönnen und weiter auf compact. Zustand — Navigationspfad, Scrollen, Auswahl, Editor, Wiedergabefortschritt, ein ungesichertes Formular — liegt über der Schicht, die einspaltig oder zweispaltig entscheidet.
Das Duo ist das erste iPhone mit mehreren Szenen. Eine App, die auf dem iPad ein neues Fenster öffnet, tut das hier auch. Ein neues Fenster lässt sich auf dem Außendisplay nicht erzeugen. Schlägt die Anforderung fehl, bleibst du im aktuellen Fenster. Der Konfigurations-Closure von UIWindowScene.ActivationAction muss eine ActivationConfiguration zurückgeben. Änder kein Anforderungsobjekt. Unterstützt die Plattform keine mehreren Fenster, nimmst du das alternate, das du bereitstellst. Nimm nicht an, ein Tipp auf dem Außendisplay öffne bestimmt ein zweites Fenster. UIRequiresFullScreen gilt weiter, beim Auf- und Zuklappen des Geräts ändert die Oberfläche trotzdem die Größe.
Die Auswahl lebt außerhalb des Layouts
struct Mailbox: View {
@State private var path: [Message.ID] = []
@State private var selection: Message.ID?
@State private var draft = ""
var body: some View {
NavigationSplitView {
MessageList(selection: $selection)
} detail: {
Editor(text: $draft, messageID: selection)
}
}
}
let action = UIWindowScene.ActivationAction(
title: "新窗口",
image: UIImage(systemName: "plus.rectangle")
) { _ in
let activity = NSUserActivity(activityType: "mailbox.message")
return UIWindowScene.ActivationConfiguration(userActivity: activity)
}
Die Scrollposition speichert die System-Scroll-View selbst, oder eine Kennung, die im Model liegt. Beim Zuklappen kein scrollTo zurück nach oben. Die Wiedergabe benutzt dasselbe Player-Objekt. Ersetz es bei einer Größenänderung nicht mit replace.
Fakten, die du prüfen musst
Zustand nach oben heben. Auswahl, Pfad, Entwurf und Player liegen außerhalb des adaptiven Containers. Wann du es benutzt: jede Seite, die zwischen compact und regular wechselt. Wann du es nicht benutzt: Bau nicht je ein Zustandsobjekt für Außendisplay und Innendisplay.
Split View. Die Leiste liegt an der Außenseite dieser App. In der linken Hälfte liegt die Leiste links, in der rechten Hälfte rechts. Wann du es benutzt: Im Device Hub legst du dich einmal nach links und einmal nach rechts. Wann du es nicht benutzt: Nimm nicht an, die vertikale Leiste liege immer an trailing.
UIWindowScene.ActivationAction(iOS 15). Der Closure gibtUIWindowScene.ActivationConfiguration(userActivity:)zurück. Unterstützt die Plattform keine mehreren Fenster, nimmst dualternate. Wann du es benutzt: Die App unterstützt auf dem iPad schon mehrere Fenster, und das Innendisplay des Duo soll dasselbe können. Wann du es nicht benutzt: Erzwing auf dem Außendisplay keine neue Szene, ohne das Fehlschlagen zu behandeln. Ein neues Fenster lässt sich auf dem Außendisplay nicht erzeugen.UIRequiresFullScreen. Die Vollbildanforderung wird weiter beachtet. Wann du es benutzt: Brauchst du den Bildschirm ohnehin allein, behältst du die bisherige Einstellung. Wann du es nicht benutzt: Glaub nicht, es halte die Größenänderung durch Auf- und Zuklappen auf. Das Layout muss weiter mitgehen.
Kontrollpunkt
Aus welchem Zustand soll der Entwurf wiederkommen, nachdem das Innendisplay geöffnet ist?
Auf welcher Seite liegt die vertikale Leiste wahrscheinlicher, wenn die App in der rechten Hälfte des Split View steht?
Lässt sich ein neues Fenster auf dem Außendisplay erzeugen?
In diesem Kapitel noch nicht. Setz die Navigation nicht mit dem Scharnierwinkel zurück. Auf- und Zuklappen ist schon durch Größe und reservierte Bereiche ausgedrückt. Den Winkel lässt du nur für echte physische Interaktion.
Quelle: die HIG zur Kontinuität über die Displays und zu den äußeren Leisten im Split View; die Testanforderung in Preparing für mehrere Größen. Tech Talk 111464.
Kapitel 10 · Der Winkel steuert Effekte, nicht Leisten und Spaltenzahl
Danach kannst du. Gibt es ein Scharnier, machst du aus dem Winkel Tonhöhe, Kamerahaltung oder Spielmechanik, und ohne Scharnier oder wenn die Interaktion die Hierarchie verlässt, gehst du auf einen sicheren Wert zurück.
Das Beispiel, das bricht
if hinge.angle > someRadians { showSidebar = true }
Flachliegen, Split View und ein gewöhnliches iPhone liefern den Winkel nicht so, wie du es erwartest. Ob eine Seitenleiste erscheinen soll, haben Kapitel 3 und Kapitel 4 schon mit dem Raum beantwortet.
Das Layout sieht den Raum, nur die Interaktion sieht das Scharnier
Zum Winkel passt: ihn als physische Eingabe zu nehmen, eine Spielmechanik, ein Instrument, die Kamerahaltung, eine Interaktion auf dem Tisch, oder ein Effekt, der ausdrücklich am Auf- und Zuklappen hängt. Nicht dazu passt: Seitenleiste, Spaltenzahl, Anzahl der Gitterzellen, ob die Navigation eingeklappt ist.
In SwiftUI heißt DeviceHingeContext.hinge gleich nil: Dieses Gerät hat kein Scharnier. Es heißt nicht „zugeklappt“. Zugeklappt ist .closed. angle ist ein Angle. status ist .closed, .partiallyOpen, .fullyOpen.
nil in UIKit bedeutet etwas anderes: Verlässt die Interaktion die Hierarchie, die Scharnieraktualisierungen liefern kann, ist update.hinge nil. Der Winkel ist im Bogenmaß. UIHinge.Status hat zusätzlich .unknown. Frequenz und Genauigkeit der Aktualisierung bestimmt das System. Verlass dich nicht auf eine bestimmte Bildrate. Interessiert dich nur zu, halb offen oder ganz offen, lies status.
Im halb offenen Zustand wird der Winkel zur Tonhöhe, sonst zurück auf null
.onHingeChange { _, context in
if let hinge = context.hinge, hinge.status == .partiallyOpen {
pitch = bend(angle: hinge.angle)
} else {
pitch = 0
}
}
let interaction = UIHingeInteraction { [weak self] _, update in
guard let self else { return }
guard let hinge = update.hinge, hinge.status == .partiallyOpen else {
self.pitch = 0
return
}
self.pitch = self.bend(radians: hinge.angle)
}
view.addInteraction(interaction)
isEnabled von onHingeChange(isEnabled:_:) ist standardmäßig true. UIKit benutzt interaction.isEnabled: Solange es aus ist, gibt es keinen Callback, und verpasste Aktualisierungen werden nicht in eine Warteschlange gelegt. Schaltest du es wieder ein und die aktuelle Hierarchie hat noch ein Scharnier, wird der aktuelle Zustand sofort noch einmal geliefert. Dieser Closure entkommt. Nimm [weak self], damit kein Zyklus entsteht.
API
onHingeChange(iOS 27.1).(isEnabled:_:), die Parameter sind der alte und der neueDeviceHingeContext. Wann du es benutzt: Ein Effekt muss dem Scharnier fortlaufend folgen. Wann du es nicht benutzt: dem Falz ausweichen, Split-Ansicht, Gitter. Dafür gibt es reservierte Bereiche und Arrangement.DeviceHinge(iOS 27.1).statusplusangle: Angle.hingeauf dem Kontext ist optional. Wann du es benutzt: Bei nil setzt du den Effekt auf null, du stürzt nicht ab. Wann du es nicht benutzt: Versteh nil nicht als „zugeklappt“. Zugeklappt ist.closed.UIHingeInteraction(iOS 27.1).init { interaction, update in }.update.hingeist ein optionalesUIHinge,angleim Bogenmaß, der Status enthält.unknown. Wann du es benutzt: an die View hängen, die das Scharnier beobachten soll. Das offizielle Beispiel ruftaddInteractioninviewDidLoadauf. Wann du es nicht benutzt: nil heißt hier, die Interaktion hat die Hierarchie verlassen, die Aktualisierungen liefert. Es heißt nicht zwingend „dieses Telefon hat kein Scharnier“.Aktualisierungsfrequenz. Der Header sagt es ausdrücklich: Rate und Granularität ändern sich mit dem Systemzustand. Wann du es benutzt: Behandle den Winkel als fortlaufende Eingabe und toleriere ausgelassene Frames. Wann du es nicht benutzt: Kalibrier oder animier das Layout nicht mit „es müssen N Mal pro Sekunde sein“.
Kontrollpunkt
Darf die Entscheidung, ob die Seitenleiste erscheint,
hinge.angleansehen?Was bedeutet
hinge == nilin SwiftUI? Und nil in UIKit?Willst du nur wissen, ob es halb offen ist: Winkel oder status?
In diesem Kapitel noch nicht. Behandle das Außendisplay nicht als zweites Fenster, das du selbst platzierst. Das ist das nächste Kapitel, und es gilt nur beim Aufnehmen.
Kapitel 11 · Zusätzlicher Inhalt auf dem Außendisplay erscheint nur beim Aufnehmen, und das System zeigt ihn
Danach kannst du. Du erklärst, wie die Hauptszene zwischen den beiden Displays wechselt, und wann du auf dem Außendisplay überhaupt ergänzenden Inhalt ablegen darfst.
Das Beispiel, das bricht
Beim Öffnen die Oberfläche in zwei UIWindow zerlegen, eines zeichnet das Innendisplay, eines das Außendisplay. Das System lässt deine zweite Leinwand nicht auf der zugeklappten Seite liegen. Im Simulator gibt es auch keine Kamera. Solchen Code kannst du im Device Hub nicht prüfen.
Das Haupterlebnis folgt der einzigen Hauptszene
Beim Auf- und Zuklappen wechselt die Hauptszene von selbst auf die andere Seite. Alle entscheidenden Buttons bleiben auf der Hauptoberfläche. Ergänzender Inhalt auf dem Außendisplay darf interagieren, aber er soll wenig sein.
Der einzige unterstützte Weg ist das Kamera-Aufnahme-Zubehör: Das Gerät ist geöffnet, die App ist im Vordergrund und das Innendisplay im Vollbild, und es gibt eine laufende Aufnahmesitzung. Erst dann kann das System es auf dem Außendisplay zeigen. Das System entscheidet wann und wo, und es kann es jederzeit zurücknehmen. Zum Abschalten nimmst du isEnabled. Schalte nicht durch Abmelden. Der Zustand läuft über dasselbe Model.
Der Simulator hat keine Kamera. Dieser Abschnitt braucht ein echtes Gerät. Die Blickrichtung der Kamera kann sich nach Auf- und Zuklappen und nach dem Drehen umkehren. Wählst du eine Kamera, nimm „auf welche Seite sie zeigt“, siehe Choosing a camera by the direction it faces.
ExternalNonInteractiveAccessory ist ein externer Bildschirm und AirPlay, nicht das Außendisplay des Duo.
Teleprompter: Die Hauptoberfläche kann weiter aufnehmen
CameraCaptureAccessory ist im SDK als nicht verfügbar für macOS, tvOS, watchOS, visionOS und Mac Catalyst gekennzeichnet. sceneAccessory und registerSceneAccessory(_:) gibt es seit iOS 27.0. cameraCapture(sceneConfiguration:) und die Szenenrolle .windowCameraCaptureAccessory sind iOS 27.1. Musst du dem Szenen-Delegate einen Kontext übergeben, nimm die Fabrikmethode mit userInfo und holst ihn in UISceneConnectionOptions.sceneAccessoryUserInfo wieder.
CameraView(model: model)
.sceneAccessory {
CameraCaptureAccessory(isEnabled: $model.prompterEnabled) {
Prompter(model: model)
}
.onAvailabilityChange { model.prompterAvailable = $0 }
}
let configuration = UISceneConfiguration(
name: "Capture Accessory",
sessionRole: .windowCameraCaptureAccessory)
let accessory = UISceneAccessory.cameraCapture(
sceneConfiguration: configuration)
registration = registerSceneAccessory(accessory)
// 配件场景的 delegate 画提词器。
// 快门仍在主场景里。
API
CameraCaptureAccessory(iOS 27.1).init(isEnabled:content:), hängt insceneAccessory.onAvailabilityChangemeldet, ob das System es noch zeigen will. Wann du es benutzt: Teleprompter, Countdown, damit die aufgenommene Person das Bild sieht. Die Registrierung sitzt auf derselben View wie die Aufnahme-UI. Wann du es nicht benutzt: Auslöser, Fokus und Einstellungen auf dem Hauptpfad. Das Außendisplay kann jederzeit verschwinden.registerSceneAccessory(iOS 27.0). UIKit:UISceneAccessory.cameraCapture(sceneConfiguration:), Rolle.windowCameraCaptureAccessory. Die zurückgegebene Registration dientunregisterSceneAccessory. Wann du es benutzt: eine ergänzende Szene, solange die Aufnahmesitzung besteht. Wann du es nicht benutzt: Melde dich nicht ab, nur um vorübergehend zu verstecken. Nimm den Aktivierungszustand.ExternalNonInteractiveAccessory. Der angeschlossene Bildschirm oder AirPlay. Wann du es benutzt: Du willst ohnehin einen externen großen Schirm unterstützen, und der Inhalt braucht keine Interaktion. Wann du es nicht benutzt: das Außendisplay, das beim Zuklappen des Duo sichtbar wird.Simulator. Der Device Hub kann aufklappen, zuklappen, drehen und falten. Eine Kamera hat er nicht. Die meisten App-Erweiterungen laufen im Duo-Simulator ebenfalls nicht. Wann du es benutzt: Layout, Leisten, Sheet, Split View. Wann du es nicht benutzt: Erklär im Simulator nicht, das Aufnahme-Zubehör sei schon geprüft.
Kontrollpunkt
Kannst du ein Dashboard auf dem Außendisplay festnageln, wenn gerade nicht aufgenommen wird?
Wo soll der Auslöser sein, wenn der Teleprompter nicht verfügbar ist?
Warum darf dieser Abschnitt nicht nur im Simulator enden?
Bis hierhin. Hat das Produkt keine Kamera, musst du in diesem Kapitel keinen Code schreiben. Die Kapitel 0–9 decken den Hauptpfad vom Entwurf bis zum adaptiven Layout schon ab.
Quelle: Preparing · Improve your app’s camera handling. Tech Talk 111465.
Anhang · Gegenüberstellung, rote Flaggen und Antworten
Die ersten zwölf Kapitel bauen das Urteil der Reihe nach auf. Hier liegen die drei API-Schichten, die Schreibweisen, die nicht vorkommen sollen, und die Antworten der Kontrollpunkte beieinander. Nach einem Kapitel kannst du hierher zurückkommen und gegenlesen.
Drei Schichten, nicht überspringen
| Schicht | Wann du sie schreibst | SwiftUI | UIKit |
|---|---|---|---|
| 1 · jede Größe | Kapitel 3–6. Davon profitiert jedes Apple-Fenster | NavigationSplitView, TabView, GridItem(.adaptive), ViewThatFits, AnyLayout, Size Class, onGeometryChange, System-.toolbar |
UISplitViewController, UINavigationController, UITabBarController, Containerbreite, Trait, layoutSubviews, viewWillTransition |
| 2 · Falz und vertikale Leiste sind bekannt | In den Kapiteln 5–8, wenn der Systemcontainer nicht reicht | reservedRegions, ArrangementView, toolbarVerticalBehavior, axisBehavior, visibilityPriority, presentationPlacement, toolbarVerticalEdge |
reservedRegions(kind:options:), UIArrangementViewController, preferredVerticalBarBehavior, axisBehavior, verticalBarEdge, preferredPlacement |
| 3 · nur die Hardware selbst | Kapitel 10–11, wenn das Produkt es wirklich braucht | onHingeChange, CameraCaptureAccessory, sceneAccessory |
UIHingeInteraction, UISceneAccessory.cameraCapture, Rolle .windowCameraCaptureAccessory |
Die Regeln für Einträge der vertikalen Leiste stehen in Kapitel 5, weil das 27.1 SDK die Systemleisten aufstellt, nicht weil du den Falz abfragen sollst. Die Abfrage des Falzes beginnt erst in Kapitel 7.
Verfügbarkeit, damit sich die Stufen nicht vermischen
| System | Was es erst in dieser Stufe gibt |
|---|---|
| iOS 26 | ConcentricRectangle, backgroundExtensionEffect(), UIBackgroundExtensionView, UICornerConfiguration |
| iOS 27.0 | visibilityPriority, ToolbarOverflowMenu, topBarPinnedTrailing, presentationPlacement, defaultTabBarPlacement, isTabViewSidebarAvailable, sceneAccessory / registerSceneAccessory |
| iOS 27.1 | Verhalten und Kante der vertikalen Leiste, axisBehavior, Komprimierungsverhalten, reservierte Bereiche, Arrangement, Scharnier, CameraCaptureAccessory |
leadingItemGroups, pinnedTrailingGroup und additionalOverflowItems in UIKit gibt es seit iOS 16. Auf dem Duo sorgen sie für Reihenfolge und Überlauf in der vertikalen Leiste, die Properties selbst erscheinen aber nicht erst mit 27.1.
Die Sichtbarkeitspriorität in SwiftUI ist .automatic / .low / .high. Die entsprechenden Werte in UIKit sind .standard / .low / .high. Auch das Komprimierungsverhalten unterscheidet sich in einem Namen: SwiftUI hat .prefersToolbarItems, UIKit hat .prefersBarItems.
Such das zuerst, wenn du eine Datei öffnest
Für gewöhnliches Layout gehört das geändert:
UIScreen.main.bounds,UIScreen.main.scale, ein gespeichertesUIScreenUIWindow(frame: UIScreen.main.bounds)userInterfaceIdiom == .padoder.phoneUIDevice.current.orientation,statusBarOrientation,interfaceOrientation, einschließlicheffectiveGeometry.interfaceOrientationhorizontalSizeClass == .regularführt auf eine Seite „nur iPad“bounds.height == 844oder eine Tabelle bekannter GerätehöhensafeAreaInsets.left * 2if isDuo,if isFolded, der Scharnierwinkel entscheidet Seitenleiste oder Spaltenzahlein festgeschriebenes großes Frame, eine festgeschriebene Breite der Seitenleiste, eine festgeschriebene Spaltenzahl des Gitters
eine selbst gezeichnete obere Leiste, oder einzeln erzeugte
UIToolbar/UINavigationBar/UITabBarein Symbolleisten-Button mit nur einem Titel und ohne Symbol; ein selbst gezeichnetes Menü „…“
entscheidender Inhalt liegt zentriert auf dem Falz; compact und aufgeklappt speichern je einen Zustand
ein Hauptbild mit
.fillohne Fokus und ohne Wahl zwischen fill und fit nach dem VerhältnistoolbarVerticalBehavior(.disabled)außerhalb der im Text genannten Ausnahmen, oder hin- und hergeschaltet nach dem Seitenzustand
Haltungen, die du im Device Hub mindestens gesehen hast
| Haltung | Erwartung |
|---|---|
| Zugeklappt, Hochformat | Außendisplay, Breite compact plus Höhe regular, vertikale Leiste |
| Zugeklappt, Querformat | Zweimal compact, vertikale Leiste |
| Aufgeklappt, Hochformat | Innendisplay regular × regular, horizontale Leiste. Das ist die einzige übliche Haltung, die die Leisten oben und unten behält |
| Aufgeklappt, Querformat | regular × regular, vertikale Leiste |
| Halb gefaltet | Dieselbe Hierarchie. Präziser Inhalt reitet nicht auf dem Falz. Scrollender Inhalt wird nicht als ganze Seite umgeleitet |
| Split View auf dem Innendisplay, du links und du rechts | Die Leiste liegt an der eigenen Außenseite. Der Zustand ist noch da |
Jedes Sheet und jedes Popover öffnest du in diesen Haltungen einmal. Screenshots für den Store richten sich nach den Screenshot specifications in App Store Connect. Eine übliche Ordnung ist Außendisplay 1398×2034, Innendisplay 2007×2853, im Querformat tauschst du die beiden Seiten. Diese Zahlen schreibst du nicht ins Layout. Steht im Material „ab April 2027 braucht eine Einreichung Duo-Screenshots“, gilt als genaues Datum die Ankündigung von Apple zu diesem Zeitpunkt.
Antworten der Kontrollpunkte
Kapitel 0. Die Systemleisten im Hochformat des Innendisplays liegen oben und unten. Die sechs Haltungen bleiben eine Oberfläche. Nach dem Aufklappen ist es weiter eine iPhone-App, nur wird das Innendisplay zu regular × regular.
Kapitel 1. Ein Einstellungsformular von 900 pt Breite zieht die Sätze auf volle Breite. Beschriftung und Schalter werden von der leeren Fläche auseinandergerissen. Die Breite gehört begrenzt, der übrige Raum der nächsten Ebene der Hierarchie. Der Verlauf darf in der Mitte bleiben, der Bezahlen-Button nicht. „Alle auswählen“ der Liste folgt der Liste, nicht der Systemleiste ganz rechts.
Kapitel 2. Das Deployment Target darf weiter iOS 18 sein. Nach dem Kompilieren mit dem 27.1 SDK erscheint die vertikale Leiste. Ist ringsum leer, wechselst du das Kompilier-SDK. Du erkennst nicht das Gerätemodell, um das Frame aufzuziehen. Neue Symbole gibt es auf dem alten System nicht, deshalb müssen sie in #available stehen.
Kapitel 3. Die Breite regular trifft auf dem Innendisplay des Duo zu, deshalb ist sie nicht mehr gleich iPad. Das Innendisplay ignoriert die deklarierte Ausrichtung. Ob breit oder hoch, vergleichst du Breite und Höhe des Containers. Der linke und der rechte Inset dürfen ungleich sein. left * 2 verrechnet die andere Seite.
Kapitel 4. Die Auswahl liegt in demselben Zustand außerhalb der Split-Ansicht. Die Spaltenzahl kommt aus Containerbreite geteilt durch die Mindestbreite eines Eintrags, hier fällt sie auf 2. ViewThatFits beantwortet, welche Anordnung passt.
Kapitel 5. Ein Button mit nur einem Titel kommt nicht in die vertikale Leiste. In der Split-Ansicht wird nur die Leiste der Detailspalte vertikal. Eine Seite vom Typ Navigation sammelt standardmäßig zuerst die Werkzeugeinträge in den Überlauf und behält die Tab-Leiste. Eine einzeln erzeugte UIToolbar ist keine Leiste, die der Navigationscontainer bereitstellt. Das System stellt sie nicht auf.
Kapitel 6. Die Symbolleiste eines trailing Sheets ist auf dem Innendisplay vertikal. Die Safe Area links und rechts ist unsymmetrisch, du darfst sie nicht spiegeln. Eine selbst gezeichnete Vollbildmaske weicht dem Falz nicht so aus wie ein System-Sheet.
Kapitel 7. Nach dem vollständigen Flachlegen ist division nicht aktiv, die Breite ist 0. Einen scrollbaren Artikel schiebst du nicht absatzweise weg. Die Kamera des Innendisplays belegt occlusion im Alltag nicht, nur während der Nutzung.
Kapitel 8. ZStack mappt auf overlay. Arrangement wird von der Navigation umschlossen, nicht umgekehrt. In einer ScrollView wird einer der beiden Blöcke unerreichbar.
Kapitel 9. Der Entwurf kommt aus demselben Zustand zurück, nicht aus einem anderen View-Baum. Die vertikale Leiste in der rechten Hälfte des Split View liegt rechts, also an der Außenseite dieser App. Ein neues Fenster lässt sich auf dem Außendisplay nicht erzeugen.
Kapitel 10. Die Seitenleiste sieht den Scharnierwinkel nicht. nil in SwiftUI heißt: Dieses Gerät hat kein Scharnier. nil in UIKit heißt: Die Interaktion hat die Hierarchie verlassen, die Aktualisierungen liefern kann. Willst du nur wissen, ob es halb offen ist, lies status.
Kapitel 11. Ohne Aufnahme kannst du kein Dashboard auf dem Außendisplay festnageln. Der Auslöser bleibt auf der Hauptoberfläche. Der Simulator hat keine Kamera. Das Aufnahme-Zubehör braucht ein echtes Gerät.
Offizielle Einstiege
ReservedRegion, sowie
UIView.ReservedRegionin UIKitArrangementView, sowie
UIArrangementViewControllerTech Talks: 111466 Entwurf, 111461 Vorbereitung, 111462 vertikale Leisten, 111463 Haltung und Arrangement, 111464 mehrere Displays und Szenen, 111465 Kamera
Die Seite Preparing nennt außerdem WWDC 2026 · 278 und WWDC 2025 · 282
Bewährte Praxis · Die unteren Einstiege bleiben, die Breite geht an den Inhalt
Danach kannst du. Du hältst diese App gegen die Regeln von oben und trennst, was schon nach Apples Art gelandet ist, was eine Produktwahl ist, und was du noch nicht als geprüft behandeln darfst.
Diese App hat keine Duo-Fassung. Das Deployment Target bleibt iOS 18. Erst das Kompilieren mit dem 27.1 SDK füllt die Fläche und lässt die Systemleisten vertikal werden. Auf- und Zuklappen wechselt weder die Route noch das Seitenmodell. Die zusätzliche Breite regular geht in die linke und rechte Spalte innerhalb von „Heute“ und „Atmen“, nicht darin, die drei Einstiege der ersten Ebene zu einer Seitenleiste zu machen. Scharnierwinkel, Kamera auf dem Außendisplay und mehrere Fenster haben keinen Produktgrund, deshalb werden sie nicht geschrieben.
Gegenüberstellung
| Urteil in der Anleitung | Vorgehen dieser App |
|---|---|
| Kapitel 0–2: eine Oberfläche, das SDK ist der Schalter | Keine Verzweigung nach Gerätenamen. Neue APIs stehen in #available, auf dem alten System bleibt HStack / VStack derselben Beziehung |
| Kapitel 3–4: nach der Containerbreite die nächste Ebene freilegen | Eine Hülle compact bleibt auch bei mehr Breite eine Spalte. regular ist bei 800 pt noch einspaltig, ab 801 pt stehen links und rechts nebeneinander. Erst wenn die größte Hauptspalte von 480 pt plus das lesbare Detail von 720 pt Platz haben (zusammen 1201 pt), kommt eine luftigere Stufe. 1024 pt ist weiter nebeneinander, nicht Sätze auf volle Breite gezogen |
| Kapitel 5: Systemleisten, die Seitenleiste ist keine Pflicht | .sidebarAdaptable plus defaultTabBarPlacement(.tabBar), die drei Tabs bleiben unten |
| Kapitel 6: Das Sheet erklärt die Position; eine selbst gezeichnete Maske weicht dem Falz nicht aus | Das System-Sheet geht auf regular nach der Absicht auf trailing oder center. Die Bezahlschicht auf compact bleibt eine Maske an der Wurzel und weicht dem Falz nicht von selbst aus |
| Kapitel 7–8: nur die zwei gepaarten Inhaltsblöcke wegschieben | Atmen, die Querzeile der Bezahlseite und die gleich breite Haupt-Neben-Spalte benutzen split von ArrangementView. Die scrollende Startseite und das Detail ändern wegen des Falzes nicht den Weg |
| Kapitel 9–11: Zustand bleibt durchgängig; Hardware-APIs sind optional | Auswahl und Route liegen außerhalb des Layouts. Kein onHingeChange, kein Aufnahme-Zubehör, keine mehreren Fenster |
Die Wurzel-Navigation behält drei Einstiege
Das Innendisplay kann zur Seitenleiste werden. Dieses Produkt wählt das nicht. „Heute“, Trend und Atmen bleiben die Tabs unten. Einstellungen schiebst du von „Heute“ aus per Push. Der iPad-Pfad benutzt defaultAdaptableTabBarPlacement(.tabBar), iOS 27 sagt zusätzlich ausdrücklich defaultTabBarPlacement(.tabBar). Nach dem Öffnen des Innendisplays erscheint der zusätzliche Raum innerhalb der Seite: Heute links die Karte und rechts das Detail, Atmen links der Modus und rechts die Übung.
Nebeneinander verlangt zwei Dinge gleichzeitig: Die Hülle dieser Seite ist regular, und die aktuelle horizontale Size Class ist ebenfalls regular. So reißt der schmale Baum die gemeinsame Auswahl nicht an sich. Ist die Hülle compact, bleibt die Stufe eine Spalte, auch wenn der Container sehr breit ist.
TabView(selection: adaptiveSectionSelection) {
// 三个 Tab
}
.tabViewStyle(.sidebarAdaptable)
.defaultAdaptableTabBarPlacement(.tabBar)
// iOS 27:content.defaultTabBarPlacement(.tabBar)
adaptiveShellRole == .regular && horizontalSizeClass == .regular
Arrangement beschreibt nur eine Beziehung, die es schon gibt
Die Navigation bleibt außen. Das Layout der älteren Version und das Arrangement von 27.1 sind dieselbe Beziehung, nicht eine zweite Seite.
| Situation | Bisherige Beziehung | 27.1 |
|---|---|---|
| Atemübung | Das Hauptbild liegt oben, die Steuerelemente unten | .split. Hoch: oben und unten, breit: links und rechts, und dem Falz ausweichen. Keine Achse gesperrt, und der Scharnierwinkel wird nicht gelesen |
| Bezahlseite quer | Hauptbild und Kauf stehen nebeneinander | .split. Auf dem alten System bleibt es ein HStack |
| Gleich breite Haupt- und Nebenspalte | Beide Spalten brauchen eigenen Raum | .split.axes(.horizontal), Verhältnis 0.5. Der Hintergrund darf über den Falz hinweg, tippbarer Inhalt liegt innerhalb von Arrangement |
Was das System übernimmt, und was noch nicht als fertig zählt
Die Safe Area liest du an vier Kanten. Bilder in scrollenden Listen und Details verschieben sich wegen des halben Faltens nicht. System-Sheet: compact nimmt .automatic, regular wählt nach der Absicht trailing oder center.
Ausdrücklich nicht: Den Scharnierwinkel nicht benutzen, um Atmen oder die Seitenleiste zu setzen. Keine Kamera, also kein Aufnahme-Zubehör registrieren. Auf dem Außendisplay kein neues Fenster erzeugen. Das Seitenmodell nicht für sechs Haltungen kopieren. Erst wenn das Produkt Auf- und Zuklappen wirklich als Eingabe nehmen will, oder eine Aufnahmeoberfläche erscheint, kommt das wieder in Betracht.
Noch nicht als fertig: Die sechs Haltungen müssen im Device Hub noch mit der Hand angesehen werden. Öffentliche Simulatorbefehle können nicht aufklappen, zuklappen oder halb falten. Die Bezahlmaske auf compact ist kein System-Sheet. Ob ein Erklär-Sheet mit nur einem Schließen-Button die vertikale Leiste abschaltet, ist noch offen. Die feste obere Leiste für den Übergang gemeinsamer Elemente liegt noch außerhalb der Skalierungsschicht. Dass es kompiliert, heißt nicht, dass Falz und vertikale Leiste schon angesehen wurden.
So schließt du eine neue Seite an: Zuerst in den vorhandenen Tab oder Navigationsstapel, der Zustand bleibt außen. Dann entscheidet die Containerstufe einspaltig oder nebeneinander, langer Text bleibt bei ungefähr 720 pt. Arrangement nimmst du erst, wenn zwei Inhaltsblöcke ohnehin die Plätze tauschen sollen, und mit #available(iOS 27.1, *) bleibt das alte Layout. Zuerst schreibst du die System-Toolbar, die Size Class und die lesbare Breite. Danach reservierte Bereiche und Arrangement. Scharnier und zweites Display schreibst du weiter nicht, bis das Produkt sie braucht.
Mit dieser App selbst prüfen
Sollen die drei Tabs nach dem Breiterwerden des Innendisplays zur Seitenleiste aufsteigen oder unten bleiben?
Soll „Heute“ bei einem regular-Container von nur 800 pt einspaltig oder nebeneinander sein?
Warum benutzen Kugel und Steuerelemente der Atemübung
.splitund nichtonHingeChange?Liegt die Bezahlmaske auf compact beim halben Falten auf dem Falz: Darfst du sagen „das System-Sheet weicht aus“?
Antworten. Die drei Tabs bleiben unten. 800 pt ist weiter einspaltig, erst 801 pt stehen nebeneinander. Kugel und Steuerelemente sind die Beziehung oben und unten derselben Übung. Das übernimmt split. Der Winkel entscheidet das Layout nicht. Die Bezahlmaske ist kein System-Sheet und weicht dem Falz nicht von selbst aus.
Weiterlesen
Diese Ideen in Stress HRV ausprobieren
Im App Store ladenDiese Notizen teilen Produktgedanken für Wellness. Sie sind keine medizinische Beratung.