Concevoir pour l’iPhone Duo, de la mise en page au code
Douze chapitres, SwiftUI et UIKit pour la même situation. Un jugement vérifiable par chapitre, puis pourquoi les onglets restent en bas.
Faites d'abord tenir l'interface à n'importe quelle largeur. Le matériel pliable n'arrive qu'ensuite, comme un plus. L'iPhone Duo a un écran externe, un écran interne et une charnière. Ce n'est pas un nouvel idiom, et l'app ne doit pas en devenir une deuxième. Le texte suit une courbe d'apprentissage : chaque chapitre ne livre qu'un jugement que vous pouvez vérifier. On regarde d'abord l'écriture qui casse, on explique pourquoi le dessin est fait ainsi, puis on donne le même cas en SwiftUI et en UIKit.
Les signatures sont vérifiées contre le SDK iOS 27.1 de Xcode 27.1. Le comportement vient de Designing for iPhone Duo, de Preparing your app for iPhone Duo et de Three steps. Le corps du texte est une reformulation, pas une copie de la source.
Comment lire. Chaque chapitre ouvre sur « À la fin de ce chapitre », puis l'exemple qui casse, la bonne écriture, et « Quand l'utiliser / Quand s'en passer ». Répondez d'abord aux points de contrôle ; les réponses sont à la fin. Les chapitres 0 à 6 n'interrogent pas le pli, n'utilisent pas Arrangement et ne lisent pas l'angle de la charnière. Cela vient plus tard.
Chapitre 0 · Six postures pour la recette, pas six interfaces
À la fin de ce chapitre. Devant un Duo fermé, ouvert, semi-plié ou tourné, vous dites selon quelle largeur penser l'interface, au lieu d'écrire une version par posture.
L'exemple qui casse
N'organisez pas le produit ainsi : portrait fermé, paysage fermé, portrait ouvert, paysage ouvert, semi-plié comme un livre, semi-plié posé sur la table, chacun avec son arbre de vues. Un seul mouvement d'ouverture remplacerait la navigation, remplacerait les boutons et perdrait la position de défilement.
SwiftUI qui bifurque selon la posture :
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 qui bifurque selon la posture :
func installRoot(for pose: String) {
let root: UIViewController
switch pose {
case "closed": root = PhoneHomeController()
case "open": root = TabletHomeController()
default: root = BookHomeController()
}
// 三套控制器,三份选中项
}
Pourquoi Apple ne donne que deux largeurs
Le Duo a un écran externe et un écran interne. La charnière peut se fermer, s'ouvrir complètement, ou s'arrêter au milieu. On le tient en portrait, en paysage, ouvert comme un livre, ou posé sur la table. Tout cela est une variation continue au cours du même usage. Le HIG demande ceci : l'écran externe en largeur compact, l'écran interne en largeur regular, et la mise en page déjà écrite grandit avec l'espace disponible. Ne refaites pas l'app pour chaque façon de la tenir. Vous dessinez toujours un iPhone. Les règles iOS de mise en page, de barres et de safe area restent en vigueur.
| Ce que vous voyez | Ce que c'est | Comment le penser au dessin |
|---|---|---|
| Fermé | Écran externe. Plus large et plus court qu'un iPhone courant | Largeur compact. En portrait, la hauteur reste regular |
| Complètement ouvert | Grand écran interne, regular × regular | Le prolongement naturel de votre mise en page iPad, mais l'app reste une app iPhone |
| Semi-plié | L'écran interne est coupé en deux par le pli | La même hiérarchie. Ce chapitre ne traite pas encore le pli |
| Rotation, Split View | Des conteneurs différents sur le même appareil | La largeur passe par des valeurs intermédiaires. Il n'y a pas que les deux crans « écran externe / écran interne plein » |
Pour le sens des barres, retenez une phrase : le portrait sur l'écran interne est la seule posture qui garde les barres horizontales en haut et en bas. Sur l'écran externe, et en paysage sur l'écran interne, les barres système passent sur le côté. En Split View, la barre de chaque côté colle à son bord extérieur. Ces détails deviennent du code au chapitre 5.
La bonne organisation
Une seule interface. L'état vit en dehors de la mise en page. Quand la largeur change, on change l'arrangement, pas les données.
struct Root: View {
@State private var selection: Item.ID?
var body: some View {
Home(selection: $selection)
}
}
final class RootController: UIViewController {
let model = HomeModel()
// 开合时只让同一个控制器重新布局
}
Les mots de ce chapitre
Il n'y a pas encore d'API Duo. Posez d'abord les bons mots, sinon la suite se branche de travers.
Écran externe / écran interne. Fermé, on regarde l'écran externe. Ouvert, on regarde l'écran interne. La scène principale change d'écran toute seule. Quand l'utiliser : pour décrire le matériel, et pour dire, à la recette, quelle face vous testez. Quand s'en passer : ne traitez pas les deux faces comme deux toiles que vous gérez vous-même.
Posture. Fermé, ouvert, semi-plié, puis multiplié par l'orientation. Quand l'utiliser : dans Device Hub, ouvrez chaque cas et vérifiez que la même interface est encore là. Quand s'en passer : ne faites pas un
switch posepour choisir la mise en page.compact / regular. Dessinez l'écran externe en largeur compact, l'écran interne en largeur regular. Quand l'utiliser : décider si l'on ne voit qu'un niveau à la fois, ou deux niveaux en même temps. Quand s'en passer : ne lisez pas regular comme « c'est un iPad ». La façon de le lire est au chapitre 3.
Barre horizontale / barre verticale. Le portrait sur l'écran interne garde les barres en haut et en bas. Les autres postures courantes rangent la navigation, la barre d'outils et la barre d'onglets sur le côté. Quand l'utiliser : dès que vous dessinez une barre, dessinez-la dans les deux sens. Quand s'en passer : ne dessinez pas une fausse barre latérale pour l'imiter.
Point de contrôle
En portrait sur l'écran interne, les barres système sont-elles en haut et en bas, ou sur le côté ?
Combien d'interfaces faut-il pour les six postures ?
Une fois le Duo ouvert, est-ce devenu une app iPad ?
Pas encore dans ce chapitre. N'écrivez pas if isDuo, ne lisez pas l'angle de la charnière, n'interrogez pas le pli. Pour l'instant, vous n'avez qu'à refuser « une UI par posture ».
Source : HIG · Designing for iPhone Duo, Tech Talk 111466.
Chapitre 1 · Plus large, montrez le niveau suivant, n'étirez pas la phrase
À la fin de ce chapitre. Devant une zone qui s'élargit, vous décidez s'il faut ajouter une colonne, ajouter une rangée, ou seulement étirer l'interface étroite d'origine. Vous savez aussi ce qui peut franchir le pli, et ce qui ne le peut pas.
L'exemple qui casse
Sur l'écran externe, la page de réglages est une colonne confortable. Une fois l'écran interne ouvert, chaque ligne de la même liste s'étire presque sur tout l'écran. Le titre et l'interrupteur partent aux deux bouts, et le milieu est un grand vide. L'autre façon de casser : placer le bouton principal, un code QR ou un visage pile au centre. Au semi-plié, le pli les coupe.
Quatre principes à emporter en revue
La fonction et l'état ne se coupent pas. Ce que l'on pouvait faire fermé est encore là une fois ouvert. Le défilement, la sélection et le contenu en cours d'édition ne se réinitialisent pas parce que l'appareil s'ouvre ou se ferme. Ce qui change, c'est le nombre de niveaux visibles en une fois.
La largeur en trop sert à montrer la hiérarchie. Sur l'écran externe, le mail est « la liste, et le message seulement après un toucher ». Sur l'écran interne, la liste et le message peuvent être côte à côte. On voit un niveau de plus. On ne passe pas la ligne du message à 900 pt.
Le pli est un dos de livre. Un fond, un dégradé, un article qui défile peuvent le franchir. Un bouton, une phrase, un visage, un code QR, une poignée de glisser ne doivent pas s'asseoir au milieu. Ce chapitre ne pose que ce jugement. Interroger le pli, c'est le chapitre 7.
Le contrôle reste près de ce qu'il commande. Les boutons d'une liste restent au-dessus de la liste. Une barre verticale sur le côté n'est pas une raison pour les envoyer tous au loin.
Un jeu doit remplir la posture courante. Changez d'abord le ratio de l'image, plutôt que de laisser des bandes noires en haut, en bas, à gauche ou à droite. La taille physique du texte et des boutons doit rester aussi stable que possible.
La même page de réglages, deux largeurs
Donnez aux textes longs et aux formulaires un plafond de lecture. L'espace en trop va au niveau d'à côté, pas à une phrase plus longue.
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)
}
Si cette zone est déjà assez large pour poser en même temps « la liste » et « le détail », le chapitre suivant le fera avec la vue scindée du système. Pour l'instant, n'écrivez pas une barre latérale à la main dans un HStack, et n'augmentez pas le corps de texte d'un cran seulement parce que c'est plus large.
Questions de revue, pas encore des API
Avant d'élargir, demandez. Navigation persistante, détail complémentaire, inspecteur, aperçu, plus de contrôles, densité d'information plus haute. Quand l'utiliser : en passant de compact à très large, cherchez dans cet ordre la structure que l'on peut montrer en même temps. Quand s'en passer : ne commencez pas par grossir les espacements et le corps pour boucher le vide.
Test du dos de livre. Imaginez au centre une fente que l'on ne peut pas toucher juste. Peut franchir : fond photo, texture, liste ou article qui défile sans s'arrêter. Ne doit pas franchir : bouton, courte phrase qu'il faut lire en entier, visage, code QR, champ de saisie, petite icône.
Regrouper près de l'action. L'action fréquente reste à côté du bloc qu'elle affecte. Quand l'utiliser : le filtre d'une liste reste sur la liste ; le partage d'un détail reste sur le détail. Quand s'en passer : ne jetez pas l'action principale dans un coin loin du contenu pour faire symétrique.
Le jeu remplit. La posture change, le ratio de l'image change. Quand l'utiliser : vous pouvez verrouiller le portrait ou le paysage, mais la posture courante doit être remplie. Quand s'en passer : ne vous contentez pas de bandes noires. S'il faut vraiment une marge, remplissez-la avec de l'image, pas avec une couleur unie.
Point de contrôle
Sur l'écran interne, un formulaire de réglages tiré à 900 pt de large : où est le problème ?
Au semi-plié, le fond en dégradé et le bouton « Payer » : lequel peut rester au centre ?
Le « Tout sélectionner » de la liste de mails doit-il suivre la liste, ou la barre système la plus à droite ?
Pas encore dans ce chapitre. Ne refaites pas toute la page pour le semi-plié, et n'écrivez pas encore le code qui interroge le pli. Sur la maquette, marquez seulement les éléments qui « ne peuvent pas s'asseoir au milieu ».
Chapitre 2 · Le SDK de compilation décide jusqu'où l'écran se remplit
À la fin de ce chapitre. Vous distinguez la cible de déploiement et le SDK de compilation, et vous expliquez pourquoi un binaire qui s'ouvre déjà sur Duo laisse encore des bandes vides, avec les barres toujours en haut.
L'exemple qui casse
Un binaire compilé avec le SDK iOS 26 s'installe. Une fois l'écran interne ouvert, la fenêtre est centrée et le tour est vide. Fermé, le contenu s'arrête à gauche de la barre d'état et de la caméra. Quelqu'un écrit alors un second correctif : « si c'est un Duo, étire le frame ».
Trois paliers de SDK, pas d'interrupteur Duo séparé
Le plein écran et la barre verticale arrivent avec le SDK de compilation. Ce n'est pas un booléen dans Info.plist. La cible de déploiement peut rester sur un système plus ancien, pour que les anciens appareils s'installent encore. Mais pour remplir l'écran du Duo et laisser les barres système passer en forme verticale, ce binaire doit être compilé avec le SDK iOS 27.1 ou un SDK plus récent. Avant d'envoyer un binaire 27.1, vérifiez que le contenu reste touchable une fois les barres devenues verticales.
| SDK de compilation | Écran interne à l'ouverture | Barres |
|---|---|---|
| iOS 26 ou plus ancien | Centré, vide autour. Sur l'écran externe, le contenu est à gauche de la barre d'état et de la caméra | Vos barres horizontales d'origine restent |
| iOS 27 | Remplit la plus grande partie de l'écran interne, mais évite encore la barre d'état de droite | La barre d'outils reste en haut |
| iOS 27.1 ou plus récent | Utilise toute la zone d'affichage | La barre d'outils et la barre d'onglets se rangent en barre verticale sous la barre d'état |
La cible peut être plus basse, les appels doivent être gardés
Les deux extraits suivants n'appellent encore aucune API de pliage. Ils posent seulement l'habitude que chaque chapitre devra tenir : les symboles 27.1 sont enfermés dans un contrôle de disponibilité. Sur Mac Catalyst, le problème connu de Xcode 27.1 est que ces symboles 27.1 peuvent ne pas compiler. Il faut les isoler encore.
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
Les faits liés à la compilation
Cible de déploiement
IPHONEOS_DEPLOYMENT_TARGET. Quand l'utiliser : décider du plus ancien système qui peut encore s'installer. Quand s'en passer : la monter à 27.1 n'est pas la condition pour remplir l'écran. Le plein écran dépend du SDK de compilation.SDK iOS 27.1. L'interrupteur du plein écran plus barre verticale. Quand l'utiliser : quand vous êtes prêt à livrer la forme complète sur Duo, compilez avec ce SDK. Quand s'en passer : n'envoyez pas un binaire 27.1 tant que la barre verticale n'est pas adaptée.
#available(iOS 27.1, *). Si la cible de déploiement est inférieure à 27.1, les nouveaux symboles doivent être enveloppés. Quand l'utiliser : à chaque appel d'une API 27.1 à partir du chapitre 5. Quand s'en passer : ne l'utilisez pas pour envelopper une size class ordinaire. Celles-là existent sur des systèmes plus anciens.Mac Catalyst
#if !targetEnvironment(macCatalyst). Quand l'utiliser : quand une API propre à 27.1 empêche la cible Catalyst de compiler, isolez ces appels. Quand s'en passer : ne supprimez pas pour autant l'adaptation sur iPhone. L'accessoire caméra lui-même n'est pas disponible sur Catalyst.
Point de contrôle
La cible de déploiement est encore iOS 18, mais la compilation utilise le SDK 27.1. La barre verticale apparaît-elle ?
Le binaire démarre déjà sur Duo, et le tour est vide. L'étape suivante est-elle de détecter le modèle pour étirer, ou de changer de SDK ?
Pourquoi un nouvel appel 27.1 ne peut-il pas être écrit directement dans un chemin de code iOS 18 ?
Pas encore dans ce chapitre. Ne lisez pas la taille de l'écran pour remplir. Le chapitre suivant remplace seulement UIScreen.main. Les règles d'entrées de la barre verticale ne sont pas encore là.
Source : Three steps to make your app shine on iPhone Duo. Xcode 27.1 demande macOS Tahoe 26.6 ou plus récent. Dans le simulateur, Device Hub ouvre, ferme, tourne et plie.
Chapitre 3 · Demandez la largeur de cette zone, pas si c'est un Duo
À la fin de ce chapitre. Vous remplacez les branches de mise en page fondées sur l'appareil, l'orientation et UIScreen.main par un recalcul selon la size class et la taille du conteneur courant.
L'exemple qui casse
Le même Duo peut apparaître sur l'écran externe, en plein écran interne, ou en Split View sur l'écran interne. La safe area et les barres changent encore. Lire l'écran une fois au lancement, c'est une valeur périmée dès le premier pli. L'écran interne ignore aussi l'orientation déclarée dans Info.
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
Deux faits faciles à inverser
Une largeur regular n'est plus un iPad. L'écran interne est large regular et haut regular. Le code qui disait « regular, donc page réservée à l'iPad » s'exécute maintenant sur iPhone. Apple demande que l'écran interne ressemble au prolongement naturel de votre mise en page iPad. Réparez cette mise en page. N'ajoutez pas un test d'idiom pour l'éteindre.
L'orientation n'est pas la forme. Pour savoir si la zone est plus large que haute, ou plus haute que large, comparez la largeur et la hauteur de la zone que vous possédez. L'écran externe en portrait est compact en largeur et regular en hauteur. L'écran externe en paysage est compact dans les deux sens. L'écran interne est regular dans les deux sens. Une app iPhone peut désormais rencontrer n'importe quelle combinaison de size class.
La taille change au milieu d'une session. Ne notez pas une valeur dans viewIsAppearing pour la réutiliser jusqu'au bout.
Recalculer selon le conteneur
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)
Quand vous avez vraiment besoin d'un UIScreen, prenez-le à ce moment-là depuis view.window?.windowScene?.screen. Ne le stockez pas dans une propriété. Créez la fenêtre depuis la scène. N'utilisez pas les bounds de l'écran principal comme frame.
API
horizontalSizeClass. SwiftUI :@Environment(\.horizontalSizeClass). UIKit :traitCollection.horizontalSizeClass. Quand l'utiliser : distinguer une largeur compact d'une largeur regular, et la croiser avec la size class verticale. Quand s'en passer : ne traduisez pas.regularpar « page iPad ».onGeometryChange. En SwiftUI, vous recevez leCGSizedu conteneur quand la taille change. L'équivalent UIKit estviewDidLayoutSubviewsetviewWillTransition(to:with:). Quand l'utiliser : comparer largeur et hauteur, ou mesurer localement selon le conteneur courant. Quand s'en passer : ne vous en servez pas pour lireUIScreen.main.bounds.displayScale. SwiftUI :@Environment(\.displayScale). UIKit :traitCollection.displayScale. Quand l'utiliser : quand il vous faut le rapport de pixels. Quand s'en passer : ne lisez plusUIScreen.main.scale. La référence à l'écran principal sera dépréciée.UIWindow(windowScene:). Crée la fenêtre avec la scène. Quand l'utiliser : remplacerUIWindow(frame: UIScreen.main.bounds). Quand s'en passer : le calcul de mise en page regarde encore les bounds de la view ou de la window, pas l'écran physique entier.
Point de contrôle
horizontalSizeClass == .regularpeut-il encore être vrai sur iPhone ?L'écran interne est passé en paysage.
interfaceOrientationest-il fiable pour dire « c'est plus large maintenant » ?Pourquoi
safeAreaInsets.left * 2se trompe-t-il sur la largeur utile ?
Pas encore dans ce chapitre. N'utilisez pas un seuil de largeur if width > 700 pour basculer entre deux arbres de navigation. Le chapitre suivant règle « étroit : on pousse ; large : côte à côte » avec un conteneur système.
Source : Step 2 de Three steps, et les resizing considerations dans Preparing. Tech Talk 111461.
Chapitre 4 · Laissez le conteneur système décider : côte à côte, ou pousser
À la fin de ce chapitre. Vous gardez la sélection dans un conteneur scindé, vous décidez le nombre de colonnes d'une grille par la largeur minimale d'un élément, et vous réarrangez localement selon « quelle disposition tient », pas selon « sur quel appareil je suis ».
L'exemple qui casse
En compact, un NavigationStack. En regular, une barre latérale écrite à la main dans un HStack. Chacun stocke sa propre sélection. La grille est écrite « trois colonnes sur Duo, sinon une ». Un groupe de boutons utilise if width > 700 pour se couper en deux vues complètement différentes, l'une horizontale, l'autre verticale.
Ordre de préférence
Si un conteneur de navigation système suffit, utilisez le conteneur. Pour des cartes répétées, une grille adaptative. Pour un cas local du type « ça ne tient pas en horizontal, alors on passe en vertical », une mise en page qui sonde l'espace restant. La size class vient ensuite. La géométrie exacte en dernier. L'architecture d'information de la vue scindée est « collection, sélection, détail ». Étroit : la liste pousse le détail. Large : côte à côte. La sélection doit survivre à ce changement. La largeur en trop sert à montrer ce niveau. Le texte long reste limité en largeur.
Liste et détail
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
// 不要写死侧栏点数
Grille de cartes : largeur minimale, pas un nombre de colonnes
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)
}
}
En Split View, quand on vous coupe à une largeur intermédiaire, le nombre de colonnes tombe de lui-même à 2. Il ne saute pas soudain de 3 à 1. Si vous voulez un nombre pair de colonnes au semi-plié, c'est le chapitre 7. Ne l'écrivez pas maintenant.
Changer l'arrangement du même groupe de vues, pas leur identité
ViewThatFits(in: .horizontal) {
HStack { Preview(); Inspector() }
VStack { Preview(); Inspector() }
}
let layout = isWiderThanTall
? AnyLayout(HStackLayout())
: AnyLayout(VStackLayout())
layout {
Chart()
Legend()
}
UIKit n'a pas ViewThatFits. L'idée correspondante : au moment de la mise en page, demandez « est-ce que l'horizontal tient encore ? », puis changez l'axe du même UIStackView. Ne détruisez pas les contrôleurs enfants pour les recréer, sinon le texte en cours d'édition est perdu.
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. Quand l'utiliser : collection, sélection, détail. Étroit, on pousse. Large, côte à côte. Quand s'en passer : ne le remplacez pas par une barre latérale écrite à la main dans unHStack. Ce n'est pas l'arrangement à deux contenus du chapitre 8.GridItem(.adaptive(minimum:)). UIKit : divisez la largeur du conteneur par la largeur minimale d'un élément pour obtenir le nombre de colonnes. Quand l'utiliser : cartes, albums, tableaux de bord, ce genre de contenu répété. Quand s'en passer : n'écrivez pasisDuo ? 3 : 1.ViewThatFits. Il sonde, sur l'axe donné, quel sous-arbre tient. Quand l'utiliser : un choix local, groupe de boutons, métadonnées, aperçu plus inspecteur. Quand s'en passer : ne vous en servez pas pour changer la navigation de toute l'app.AnyLayout. Change le conteneur de mise en page et garde l'identité des sous-vues. Quand l'utiliser : graphique et légende, aperçu et propriétés. L'arrangement change, mais ce sont encore ces deux vues. Quand s'en passer : n'écrivez pas les sous-vues une fois dans chaque branche d'unif.
Point de contrôle
Quand on revient de côte à côte à une seule colonne, où la sélection doit-elle vivre ?
Après le Split View, la largeur ne tient que deux cartes de 220 pt. D'où vient le nombre de colonnes ?
ViewThatFitsrépond-il à « quel appareil », ou à « quelle disposition tient » ?
Pas encore dans ce chapitre. La barre d'onglets doit-elle devenir une navigation latérale : ce sera le chapitre suivant. Deux contenus qui doivent éviter le pli : ce sera le chapitre 8. N'introduisez pas Arrangement maintenant.
Chapitre 5 · Les barres système se mettent sur le côté ; une fausse barre, non
À la fin de ce chapitre. Vous rendez la navigation, la barre d'outils et la barre d'onglets reconnaissables en forme horizontale et en forme verticale, et vous confiez les actions secondaires au menu de débordement du système.
L'exemple qui casse
En haut de la page, un HStack dessiné à la main, ou un UINavigationBar, un UIToolbar, un UITabBar créé à part avec new. Le système ne range pas ce contenu dans la barre verticale. Autre cas fréquent : le bouton de partage n'a que le titre "Share". La barre verticale n'affiche que des icônes. Une entrée sans icône ne se met jamais à la verticale. Dessiner soi-même un menu « … » entre en concurrence avec le menu de débordement du système.
Quand la barre est verticale, et qui a le droit d'y entrer
Une fois compilé avec le SDK 27.1, l'écran externe rassemble la barre de navigation, la barre d'outils et la barre d'onglets en une seule bande sur le côté. Le paysage sur l'écran interne, et certaines vues placées en leading ou en trailing, font de même. Le portrait sur l'écran interne garde la barre horizontale. Dans une vue scindée, seule la colonne de détail participe à la barre verticale. La colonne latérale et la colonne de contenu restent horizontales. L'inspecteur reste horizontal.
La barre s'aligne sur le bord du matériel. Une langue de droite à gauche ne la fait pas changer de côté. En Split View, la barre de l'app de gauche est à gauche, celle de l'app de droite est à droite.
Une entrée doit avoir à la fois un titre et une icône. La forme verticale utilise l'icône. La forme horizontale préfère l'icône, et peut aussi utiliser le titre. Dans le menu de débordement, l'icône et le titre apparaissent ensemble. Une vue personnalisée reste par défaut dans la barre horizontale, sauf si vous déclarez qu'elle se place bien à la verticale. Un bouton en texte seul reste dans la barre horizontale. Le bouton Edit du système bascule ainsi entre symbole et texte.
Le haut de la barre verticale est réservé au retour ou à la fermeture, puis à une action marquée comme Done. Les autres entrées gardent leur groupe d'origine. Le débordement se replie par défaut du bas vers le haut. Une page de navigation protège d'abord la barre d'onglets. Une page de tâche protège d'abord les outils.
Le même groupe d'actions, utilisable dans les deux barres
SwiftUI, accroché dans un 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, accroché sur le contrôleur enfant du contrôleur de navigation :
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
Le bouton Retour est posé par le contrôleur de navigation lui-même. Une fermeture personnalisée va du côté leading. Done va dans pinnedTrailingGroup. Quand la recherche occupe tout l'espace, les entrées épinglées sont les dernières à entrer dans le débordement. additionalOverflowItems sert à ranger votre propre « plus » dans le menu système. L'ellipse est réservée au débordement. Les autres menus prennent un autre symbole.
Une entrée qui oscille entre symbole et texte se règle sur .horizontalOnly : sans barre horizontale, elle se cache, au lieu de se coincer dans la barre verticale. Une vue personnalisée qui a vraiment un dessin vertical utilise .verticalPreferred. Un espace flexible a une taille 0 sur l'axe vertical. Groupez avec ToolbarItemGroup / UIBarButtonItemGroup. N'insérez pas à la main un espacement fixe. Le badge utilise badge. N'écrivez pas le nombre dans le titre, sinon cette entrée a du mal à devenir un symbole seul.
Sortir de la barre verticale est une exception stable
NavigationStack {
CalculatorView()
.toolbarVerticalBehavior(.disabled)
}
override var preferredVerticalBarBehavior: UIVerticalBarBehavior {
.disabled
}
La valeur par défaut est .automatic. On n'en sort que sur ces interfaces : vidéo plein écran, mise en page pleine largeur qui ne défile pas comme une calculatrice, sheet qui n'a qu'un bouton de fermeture. C'est un choix stable. Ne basculez pas dans viewWillAppear selon l'état de la page. Si une page doit seulement cacher la barre, utilisez l'API de visibilité, pas la coupure du comportement vertical. En UIKit, si la préférence a vraiment changé, appelez setNeedsUpdateOfVerticalBarConfiguration(). Par défaut, le contrôleur de navigation laisse la décision au sommet de la pile, et la barre d'onglets à la page sélectionnée.
Savoir de quel côté est la barre, et comment le fond s'y prolonge
@Environment(\.toolbarVerticalEdge) private var verticalEdge
// HorizontalEdge?;这个上下文从不用垂直栏时为 nil
Image("hero")
.backgroundExtensionEffect()
let edge = traitCollection.verticalBarEdge
// .leading / .trailing / .unspecified
// 表示系统偏好的边,不表示栏此刻是否可见
// 从不用垂直栏的上下文返回 .unspecified
let backdrop = UIBackgroundExtensionView()
backdrop.contentView = hero
Pour écouter ce bord en UIKit, passez UITraitCollection.systemTraitsAffectingVerticalBarEdge à registerForTraitChanges. Le trait peut changer et le bord rester le même.
Entrée de premier niveau : l'écran interne peut devenir une navigation latérale
Sur iPhone, .sidebarAdaptable seul reste une barre du bas. Pour passer à une navigation latérale là où c'est pris en charge, ajoutez defaultTabBarPlacement(.sidebar) d'iOS 27. N'utilisez pas defaultAdaptableTabBarPlacement d'iOS 18 : c'est pour iPad. Pour l'UI liée à la barre latérale, regardez isTabViewSidebarAvailable. Ne devinez plus la size class.
TabView {
Tab("Home", systemImage: "house") { HomeView() }
Tab("Library", systemImage: "books.vertical") { LibraryView() }
}
.tabViewStyle(.sidebarAdaptable)
.defaultTabBarPlacement(.sidebar)
tabBarController.sidebar.preferredPlacement = .sidebar
Le produit peut aussi garder clairement la barre du bas, et laisser l'espace élargi aux colonnes du contenu. Les deux tiennent. Ne dessinez pas vous-même une fausse barre latérale selon la largeur.
API
toolbarVerticalBehavior(iOS 27.1)..automatic/.disabled. UIKit : surchargezpreferredVerticalBarBehavior, de typeUIVerticalBarBehavior. Quand l'utiliser : calculatrice, vidéo plein écran, sheet à un seul bouton de fermeture. Quand s'en passer : ne le faites pas osciller selon la navigation ou l'état d'une vue.axisBehavior(iOS 27.1). SwiftUI :.automatic/.horizontalOnly/.verticalPreferred. UIKit :UIBarButtonItem.axisBehavior, les trois mêmes valeurs. Quand l'utiliser : horizontalOnly pour une entrée qui alterne symbole et texte ; verticalPreferred pour une vue personnalisée qui a vraiment un dessin vertical. Quand s'en passer : un bouton ordinaire, avec titre et icône, reste automatic.visibilityPriority(iOS 27.0). SwiftUI :.automatic/.low/.high, plusinit(lowerThan:)/init(higherThan:). UIKit :.standard/.low/.high. Quand l'utiliser : donnez d'abord la priorité du groupe, puis celle de chaque entrée dans le groupe. Les actions fréquentes et celles qui portent un badge débordent en dernier. Quand s'en passer : ne fabriquez pas un second menu « … ».ToolbarOverflowMenuetadditionalOverflowItemssont le menu système. En UIKit,additionalOverflowItemsexiste depuis iOS 16.Comportement de compression (iOS 27.1). SwiftUI :
.automatic/.prefersToolbarItems/.prefersTabBar. UIKit :navigationItem.verticalBarCompressionBehavior, avec.automatic/.prefersBarItems/.prefersTabBar. Quand l'utiliser : une page de navigation garde le défaut (on range d'abord les outils, on protège les onglets). Une page de tâche range d'abord la barre d'onglets. Quand s'en passer : n'écrivez pas deux jeux de barres pour simuler la compression.topBarPinnedTrailing(iOS 27.0). La propriété UIKit correspondante,pinnedTrailingGroup, existe depuis iOS 16. La fermeture utilise.cancellationAction. En UIKit, elle va dansleadingItemGroups. Quand l'utiliser : une action principale du genre Done, qui doit rester près du retour. Quand s'en passer : n'épinglez pas ici une action secondaire. Elle doit pouvoir déborder.toolbarVerticalEdge(iOS 27.1). SwiftUI :HorizontalEdge?, nil quand ce contexte n'utilise jamais de barre verticale. UIKit :traitCollection.verticalBarEdge, valeurs leading, trailing ou unspecified. Il reflète le bord préféré, même si la barre n'est pas visible à cet instant. Quand l'utiliser : quand un contenu personnalisé doit s'aligner sur cette barre. Quand s'en passer : ne lisez pas unspecified comme « la barre vient d'être cachée ». Cela veut dire que ce contexte n'utilisera pas de barre verticale.backgroundExtensionEffect()(iOS 26). UIKit :UIBackgroundExtensionView. PlacezcontentViewdans la safe area, et remplissez les bords par l'extension. Quand l'utiliser : un visuel principal ou un fond doit passer sous la barre verticale. Quand s'en passer : un premier plan touchable ne doit pas s'en servir pour sortir de la safe area.defaultTabBarPlacement(iOS 27.0)..sidebarAdaptableplus.sidebar. UIKit :sidebar.preferredPlacement = .sidebar. Les valeurs d'environnementisTabViewSidebarAvailableettabBarPlacementsont aussi iOS 27. Quand l'utiliser : des sections de premier niveau qui, sur une mise en page large, méritent une navigation toujours là. Quand s'en passer : n'utilisez pasdefaultAdaptableTabBarPlacementpour le Duo. Ce modificateur existe depuis iOS 18, et il agit sur iPad.
Point de contrôle
Un bouton qui n'a qu'un titre, sans icône, entre-t-il dans la barre verticale ?
Parmi les trois colonnes d'une vue scindée, laquelle a une barre verticale ?
Sur une page de navigation, quand l'espace manque, range-t-on d'abord les outils ou d'abord la barre d'onglets ?
Pourquoi un
UIToolbardessiné à part ne se met-il pas sur le côté ?
Pas encore dans ce chapitre. N'interrogez pas le pli pour décider si la barre est à gauche ou à droite. Le bord de la barre est mis par le système dans le trait / l'environment. De quel côté poser un sheet : c'est le chapitre suivant.
Source : HIG · Vertical controls ; Preparing · Optimize bars. Tech Talk 111462.
Chapitre 6 · Quatre bords calculés à part, le sheet déclare son côté
À la fin de ce chapitre. Vous choisissez la place d'un sheet, vous calculez la zone utile avec une safe area asymétrique, et vous gardez le sujet d'une image pleine largeur grâce à des coins concentriques à l'écran et à un point de focus.
L'exemple qui casse
Prendre l'inset de gauche et le multiplier par deux comme safe area totale. Le Split View ou la barre verticale rendent la gauche et la droite inégales, et le contenu se décale. L'autre problème : fabriquer soi-même un voile plein écran pour servir de sheet. Le sheet système, l'alerte, le menu et le popover évitent le pli. Votre voile, non. Un visuel principal rogné avec .fill : le visage visible sur l'écran externe est coupé sur l'écran interne.
La barre du sheet suit le placement
Sur l'écran externe, un sheet qui a une barre d'outils a par défaut une barre verticale. Sur l'écran interne, le sheet est centré par défaut et sa barre est horizontale. Le côté leading n'a pas de barre verticale. Le côté trailing en a une. Seul le sheet tient compte du placement. Ne fabriquez pas pour cela une seconde transition personnalisée. Un sheet qui n'a qu'un bouton de fermeture est le cas où le chapitre précédent autorise à couper la barre verticale. Quand sourceView est défini, UIKit ignore preferredPlacement.
.sheet(item: $selection) { item in
ItemDetail(item: item)
.presentationPlacement(.trailing)
}
let sheet = detail.sheetPresentationController
sheet?.preferredPlacement = .trailing
// .automatic / .leading / .center / .trailing
// sourceView 非 nil 时,这个属性被忽略
Les quatre bords de la safe area, chacun pour soi
content
.safeAreaPadding(.horizontal)
background
.ignoresSafeArea()
let usable = view.bounds.inset(by: view.safeAreaInsets)
// 分别使用 left、right、top、bottom
// 折痕不是 safe area inset
La décoration peut sortir de la safe area. Le contenu touchable et le texte qu'il faut lire restent dedans. La caméra, la barre verticale et le Split View rendent la gauche et la droite, ou le haut et le bas, inégaux. Pour une forme personnalisée qui épouse les coins de l'écran, utilisez un rectangle concentrique. N'écrivez pas un rayon en dur.
RoundedRectangle(cornerRadius: 12) // 不要写死去贴屏幕角
ConcentricRectangle()
view.cornerConfiguration = .corners(radius: .containerConcentric())
Une image pleine largeur doit garder son sujet
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
}
Si le sujet n'est pas au centre géométrique, donnez un point de focus au rognage, au lieu de seulement basculer entre fill et fit. Un jeu, lui, remplit et change de ratio : voir le chapitre 1.
API
presentationPlacement(iOS 27.0)..automatic/.center/.leading/.trailing. UIKit :UISheetPresentationController.preferredPlacement. Quand l'utiliser : chaque sheet doit être regardé fermé, ouvert et semi-plié, avant de décider s'il se colle au bord ou se centre. Quand s'en passer : ne posez pas ce modificateur sur un popover ni sur une alerte. QuandsourceViewest défini, UIKit l'ignore.Inset asymétrique.
bounds.inset(by: safeAreaInsets). Quand l'utiliser : partout où vous calculez à la main une largeur ou une hauteur utile. Quand s'en passer : ne miroitez pas l'inset d'un côté sur l'autre. Ne fourrez pas non plus le pli dans la safe area.ConcentricRectangle(iOS 26). UIKit : le rayon concentrique du conteneur dansUICornerConfiguration.UIView.cornerConfigurationexiste. Quand l'utiliser : un panneau personnalisé doit épouser les coins de l'écran ou du conteneur courant. Quand s'en passer : le petit rayon d'une carte peut rester un rayon fixe. Ne vous en servez pas pour éviter le pli.fill / fit. Choisissez selon le ratio courant ou la size class, ou posez un point de focus. Quand l'utiliser : visuel principal, jaquette vidéo, du petit écran externe au large écran interne, le sujet doit rester. Quand s'en passer : ne choisissez pas contentMode une seule fois au lancement.
Point de contrôle
Sur l'écran interne, la barre d'outils d'un sheet trailing est-elle horizontale ou verticale ?
Pourquoi
left * 2ne donne-t-il pas la largeur du contenu ?Le sheet système évite le pli. Un voile plein écran que vous dessinez le fait-il ?
Pas encore dans ce chapitre. Quand un canevas personnalisé, un code QR ou un contrôle de glisser doivent éviter le pli, le chapitre suivant interroge les régions réservées. Ne devinez pas les coordonnées du pli maintenant.
Chapitre 7 · Le pli et la caméra sont deux régions réservées, pas la safe area
À la fin de ce chapitre. Dans une mise en page personnalisée, vous trouvez le pli actif et la caméra active, et vous ne déplacez que le morceau masqué, au lieu de refaire toute la page.
L'exemple qui casse
Dès que c'est à moitié plié, on remplace par un autre arbre d'interface. Ou l'on écrit en dur « le pli est à 50 % de la largeur ». Après un Split View ou une rotation, ce ratio n'est plus le centre de votre vue. L'autre extrême : écarter du pli chaque paragraphe d'un article. Un contenu qui défile peut déjà rouler par-dessus cette fente.
Deux sortes de zones. La plupart des composants système les évitent déjà
Une région réservée est le rectangle qu'autre chose occupe dans votre système de coordonnées. frame inclut margins. Chaque entrée a une sorte, un état actif, un identifiant. La requête renvoie les zones qui intersectent la vue courante. Si votre vue n'occupe que la moitié de l'écran interne, ne supposez pas que vous voyez le pli de tout l'écran.
division, la séparation. Au semi-plié, la charnière coupe un grand bloc en deux côtés. Une fois complètement à plat, cette zone n'est pas active, et sa largeur est 0.
occlusion, le masquage. La caméra avant de l'écran externe est toujours là, et elle s'étend en Dynamic Island. La caméra avant de l'écran interne est d'habitude cachée sous l'écran. Elle n'occupe un rectangle, et ne fait reculer l'interface, que pendant l'usage.
Alerte, menu, sheet et vue scindée s'adaptent tout seuls. Canevas personnalisé, code QR, visage, courte phrase qui doit rester continue, objet en cours de glisser : c'est là qu'il faut lire ces rectangles. Ne déplacez que l'élément masqué, et emmenez avec lui les contrôles qui lui sont liés. Ne l'envoyez pas à l'autre bout de l'écran. Un article ou une liste qui défile ne se déplace pas en bloc.
Filtrer d'abord ce qui est en vigueur, puis placer le contenu personnalisé
reservedRegions en SwiftUI accepte aussi layoutDirectionBehavior:, par défaut .mirrors. Ne passez .fixed que si vous avez vous-même fait le miroir, sinon le miroir se fait deux fois. La position physique de la caméra ne se retourne pas avec la langue. SwiftUI miroite d'abord la géométrie pour vous. Le proxy de onGeometryChange sait aussi interroger.
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)
}
Une décision de structure comme « nombre pair de colonnes » doit rester visible même quand l'appareil est à plat et que la zone de division n'est pas encore active. C'est seulement alors que l'on passe .includeInactive. On utilise toujours isActive pour décider s'il faut déplacer les contrôles tout de suite.
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). En SwiftUI, surGeometryProxy. UIKit :view.reservedRegions(kind:options:), qui renvoie[UIView.ReservedRegion]. Quand l'utiliser : un contenu personnalisé que les conteneurs système ne déplaceront pas pour vous. Quand s'en passer : liste ordinaire, vue scindée, sheet, alerte. Ne prenez pas le résultat pour un inset de safe area.ReservedRegion(iOS 27.1).id,kind,frame,margins,isActive. Les sortes sont.divisionet.occlusion. Quand l'utiliser : placez avecframe.framecontient déjà les marges. Quand s'en passer : une division inactive ne sert pas à pousser un bouton. À plat, elle ne doit pas changer l'endroit du toucher..includeInactive(iOS 27.1).ReservedRegion.QueryOptions. Quand l'utiliser : une décision indépendante du semi-plié courant, du genre « s'il existe un pli, la grille utilise un nombre pair de colonnes ». Quand s'en passer : n'avancez pas le déplacement d'un bouton seulement parce que vous avez trouvé une zone inactive.layoutDirectionBehavior..mirrorsou.fixed. Quand l'utiliser : gardez le défaut, pour que les sous-vues en mise en page automatique et les régions partagent le même sens. Quand s'en passer : ne passez.fixedque si vous miroitez à la main.
Point de contrôle
Une fois complètement à plat, la division est-elle encore active ? Quelle est sa largeur ?
Faut-il écarter du pli chaque paragraphe d'un article qui défile ?
La caméra de l'écran interne occupe-t-elle l'occlusion en temps ordinaire ?
Pas encore dans ce chapitre. Un lecteur plus une file d'attente, ce couple de contenus, le chapitre suivant le confie à Arrangement. N'utilisez pas l'angle de la charnière pour décider d'éviter le pli.
Source : ReservedRegion ; Preparing · Adapt to reserved regions. Tech Talk 111463.
Chapitre 8 · Quand deux contenus forment une expérience, le conteneur les permute
À la fin de ce chapitre. Vous jugez si une paire de vues doit utiliser split ou overlay, et vous laissez la navigation à l'extérieur de ce conteneur.
L'exemple qui casse
Remplacer TabView ou NavigationSplitView par Arrangement. Ou placer Arrangement dans une List, une ScrollView, ou une colonne de vue scindée. Résultat : la moitié du contenu ne défile pas jusqu'au bout, ou ne se touche pas.
Si ça ressemble déjà à l'une des deux piles, mappez-le
Arrangement n'a qu'une vue principale et une vue secondaire. Il regarde l'espace, le ratio, la size class et la zone de division. Il ne regarde pas si vous êtes sur un Duo. Le conteneur de navigation enveloppe l'extérieur. L'exemple d'Apple est justement un Arrangement dans une pile de navigation. Ne remettez pas une vue scindée à l'intérieur d'Arrangement.
split. Correspond à l'ancien
HStack/VStack, ou à une relation principal / secondaire où personne ne doit couvrir l'autre. Plus large que haut : côte à côte. Plus haut que large : empilé. Quand une zone de division apparaît, il ajuste les deux côtés et évite le pli. Une fois limité à un seul axe, si cet axe ne peut pas servir, il ne laisse que la vue principale..split.axes(.horizontal)n'affiche que le contenu principal quand l'axe horizontal ne tient pas.overlay. Correspond à l'ancien
ZStack. Sans zone de division active, la vue principale couvre la vue secondaire (fermé, ou complètement ouvert). Au semi-plié, la vue principale passe du côté arrière ou en dessous du pli, et la vue secondaire du côté avant ou au-dessus.
Lecteur et 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)
Seulement gauche-droite ; en portrait, on ne garde que le lecteur :
.arrangementViewStyle(.split.axes(.horizontal))
arrangement.updateArrangement(.split.axes(.horizontal))
Des contrôles posés sur l'image : overlay
ArrangementView {
TransportControls()
} secondary: {
Stage()
}
.arrangementViewStyle(.overlay)
arrangement.setViewController(controls, for: .primary)
arrangement.setViewController(stage, for: .secondary)
arrangement.updateArrangement(.overlay)
Le ratio et l'empilement ont encore ces réglages, tous en iOS 27.1 : splitArrangementLayoutRatio, splitArrangementLayoutSize, splitArrangementFixedLayoutSize, overlayArrangementEdge. Les valeurs d'environnement splitArrangementAxis et overlayArrangementZIndex disent à la sous-vue dans quel arrangement elle se trouve. UIKit lit zIndex, splitAxis et isHidden avec state(for:). Les plages de taille des vues principale et secondaire sont dans UISplitArrangement.ViewProperties. Le bord de recouvrement est UIOverlayArrangement.ViewProperties.edge.
API
ArrangementView(iOS 27.1).initreçoit primary et secondary. UIKit :UIArrangementViewController, etsetViewController(_:for:animated:)place en.primary/.secondary. Quand l'utiliser : lecteur et file, éditeur et inspecteur, canevas et propriétés. Les deux morceaux sont la même expérience. Quand s'en passer : la navigation de premier niveau de l'app. Ne le mettez pas dans une vue scindée, une liste ou une vue qui défile..split(iOS 27.1).arrangementViewStyle(.split),.axes(.horizontal)ou.vertical. UIKit :updateArrangement(.split.axes(...)). Quand l'utiliser : les deux côtés ont besoin d'un espace à eux, et personne ne couvre l'autre. Quand s'en passer : des contrôles qui doivent flotter sur le contenu. Ça, c'est overlay..overlay(iOS 27.1). Sans zone de division, la vue principale est au-dessus. Au semi-plié, la vue principale va du côté arrière ou en dessous, la vue secondaire du côté avant ou au-dessus. Quand l'utiliser : premier plan / arrière-plan, quand ils peuvent se couvrir en temps ordinaire. Quand s'en passer : le mail à deux colonnes. C'est la vue scindée du chapitre 4, ou le split d'ici.updateArrangement(_:animated:)(iOS 27.1). Change split / overlay, ou seulement l'axe. Quand l'utiliser : le produit veut clairement verrouiller un axe, ou basculer entre les deux styles. Quand s'en passer : ne recalculez pas le frame vous-même à chaque rotation pour l'appeler ensuite. Le style par défaut suit déjà le conteneur.
Point de contrôle
C'était un
ZStack, et au semi-plié il faut le répartir des deux côtés du pli. split ou overlay ?Arrangement doit-il envelopper la navigation, ou être enveloppé par elle ?
Que se passe-t-il si on le met dans une
ScrollView?
Pas encore dans ce chapitre. Ne lisez pas l'angle de la charnière pour choisir split ou overlay. L'angle est une entrée d'interaction, au chapitre 10.
Source : les exemples SwiftUI / UIKit dans Preparing ; HIG · Arrangement views. Signatures vérifiées contre ArrangementView et UIArrangementViewController du SDK iOS 27.1.
Chapitre 9 · Ouvrir et fermer change la mise en page, ce n'est pas un redémarrage de l'app
À la fin de ce chapitre. Vous laissez la navigation, le défilement, la sélection, l'édition et la lecture au-dessus de la mise en page, et vous connaissez les limites du Split View et des nouvelles fenêtres sur Duo.
L'exemple qui casse
Fermé, la liste a sélectionné le troisième message. Ouvert, un autre arbre de vues repart du premier, et le brouillon est vide. L'autre façon de casser : n'avoir testé que le plein écran externe et le plein écran interne. À mi-chemin du Split View, les barres se chevauchent et le texte est rogné. Et l'on suppose encore que « ouvert veut dire forcément très large ».
Une largeur continue, plus deux limites matérielles
Sur l'écran interne, toutes les apps participent au Split View, et une vidéo peut aussi se superposer à une app. La barre verticale de chaque app colle à son propre bord extérieur. Votre barre peut donc être à gauche, ou à droite. Il faut tester les deux.
Une mise en page très large doit pouvoir revenir à une largeur intermédiaire, puis à compact. L'état — chemin de navigation, défilement, sélection, éditeur, progression de lecture, formulaire non enregistré — vit au-dessus de la couche qui décide une colonne ou deux.
Le Duo est le premier iPhone à prendre en charge plusieurs scènes. Une app qui ouvre de nouvelles fenêtres sur iPad le fait aussi ici. Une nouvelle fenêtre ne peut pas être créée sur l'écran externe. Si la demande échoue, restez dans la fenêtre courante. La fermeture de configuration de UIWindowScene.ActivationAction doit retourner un ActivationConfiguration. Ne modifiez pas un objet de requête. Quand la plateforme ne prend pas en charge plusieurs fenêtres, passez par l'alternate que vous fournissez. Ne supposez pas qu'un toucher sur l'écran externe ouvrira forcément une seconde fenêtre. UIRequiresFullScreen s'applique toujours, mais l'interface change quand même de taille quand l'appareil s'ouvre ou se ferme.
La sélection vit en dehors de la mise en page
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)
}
La position de défilement se garde par la sauvegarde propre à la vue de défilement du système, ou par un identifiant posé dans le model. Ne faites pas un scrollTo vers le haut au moment de fermer. La lecture utilise le même objet lecteur. Ne le replace pas quand la taille change.
Les faits à vérifier
Remonter l'état. Sélection, chemin, brouillon, lecteur vivent à l'extérieur du conteneur adaptatif. Quand l'utiliser : toute page qui passe de compact à regular. Quand s'en passer : ne faites pas un objet d'état pour l'écran externe et un autre pour l'écran interne.
Split View. La barre est sur le bord extérieur de cette app. Moitié gauche : barre à gauche. Moitié droite : barre à droite. Quand l'utiliser : dans Device Hub, placez-vous à gauche, puis à droite. Quand s'en passer : ne supposez pas que la barre verticale est toujours en trailing.
UIWindowScene.ActivationAction(iOS 15). La fermeture retourneUIWindowScene.ActivationConfiguration(userActivity:). Quand la plateforme ne prend pas en charge plusieurs fenêtres, utilisezalternate. Quand l'utiliser : l'app ouvre déjà plusieurs fenêtres sur iPad, et vous voulez la même chose sur l'écran interne du Duo. Quand s'en passer : ne forcez pas une nouvelle scène sur l'écran externe sans traiter l'échec. Une nouvelle fenêtre ne peut pas y être créée.UIRequiresFullScreen. L'exigence de plein écran est toujours respectée. Quand l'utiliser : si vous avez déjà besoin d'occuper l'écran seul, gardez le réglage d'origine. Quand s'en passer : ne croyez pas que cela empêche le changement de taille dû à l'ouverture et à la fermeture. La mise en page doit encore suivre.
Point de contrôle
Une fois l'écran interne ouvert, depuis quel état le brouillon doit-il revenir ?
Quand l'app est dans la moitié droite du Split View, de quel côté la barre verticale est-elle le plus probablement ?
Une nouvelle fenêtre peut-elle être créée sur l'écran externe ?
Pas encore dans ce chapitre. N'utilisez pas l'angle de la charnière pour réinitialiser la navigation. L'ouverture et la fermeture sont déjà exprimées par la taille et les régions réservées. L'angle est réservé à une vraie interaction physique.
Source : le HIG sur la continuité d'un écran à l'autre et sur la barre extérieure en Split View ; Preparing sur l'exigence de tester plusieurs tailles. Tech Talk 111464.
Chapitre 10 · L'angle pilote l'effet, pas les barres ni le nombre de colonnes
À la fin de ce chapitre. Quand il y a une charnière, vous transformez l'angle en hauteur de son, en cadrage ou en règle de jeu, et vous revenez à une valeur sûre quand il n'y a pas de charnière, ou quand l'interaction quitte la hiérarchie.
L'exemple qui casse
if hinge.angle > someRadians { showSidebar = true }
À plat, en Split View, ou sur un iPhone ordinaire, vous n'obtiendrez pas l'angle que vous attendez. La barre latérale doit-elle apparaître : les chapitres 3 et 4 ont déjà répondu avec l'espace.
La mise en page regarde l'espace. Seule l'interaction regarde la charnière
L'angle convient quand on le traite comme une entrée physique, un mécanisme de jeu, un instrument, une pose de caméra, une interaction sur la table, ou un effet explicitement lié à l'ouverture. Il ne convient pas à : barre latérale, nombre de colonnes, nombre de cases de grille, navigation repliée ou non.
En SwiftUI, DeviceHingeContext.hinge à nil veut dire que cet appareil n'a pas de charnière. Cela ne veut pas dire « il est fermé ». Fermé, c'est .closed. angle est un Angle. status vaut .closed, .partiallyOpen, .fullyOpen.
En UIKit, nil n'a pas le même sens : quand l'interaction a quitté la hiérarchie qui peut fournir des mises à jour de charnière, update.hinge est nil. L'angle est en radians. UIHinge.Status a en plus .unknown. La fréquence et la précision des mises à jour sont décidées par le système. Ne vous appuyez pas sur une cadence d'images. Si vous ne voulez savoir que ouvert, semi-ouvert ou complètement ouvert, lisez status.
Au semi-ouvert, l'angle devient une hauteur de son. Le reste du temps, on revient à zéro
.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 de onHingeChange(isEnabled:_:) vaut true par défaut. UIKit utilise interaction.isEnabled : pendant que c'est coupé, pas de rappel, et les mises à jour manquées ne sont pas mises en file. À la réouverture, si la hiérarchie courante a encore une charnière, l'état courant est rendu tout de suite, une fois de plus. Cette fermeture échappe. Utilisez [weak self] pour éviter une référence circulaire.
API
onHingeChange(iOS 27.1).(isEnabled:_:). Les paramètres sont l'ancien et le nouveauDeviceHingeContext. Quand l'utiliser : un effet qui doit suivre la charnière en continu. Quand s'en passer : éviter le pli, vue scindée, grille. Cela se fait avec les régions réservées et Arrangement.DeviceHinge(iOS 27.1).statusplusangle: Angle.hingesur le contexte est optionnel. Quand l'utiliser : à nil, ramenez l'effet à zéro, ne plantez pas. Quand s'en passer : ne lisez pas nil comme « c'est fermé ». Fermé, c'est.closed.UIHingeInteraction(iOS 27.1).init { interaction, update in }.update.hingeest unUIHingeoptionnel,angleest en radians, et l'état inclut.unknown. Quand l'utiliser : ajoutez-le à la vue qui doit observer la charnière. L'exemple officiel faitaddInteractiondansviewDidLoad. Quand s'en passer : ici, nil veut dire que l'interaction a quitté la hiérarchie qui fournit les mises à jour. Ce n'est pas forcément « ce téléphone n'a pas de charnière ».Fréquence des mises à jour. L'en-tête le dit : le rythme et la granularité changent avec l'état du système. Quand l'utiliser : traitez l'angle comme une entrée continue, et tolérez des images perdues. Quand s'en passer : ne calibrez pas, et n'animez pas une mise en page, sur l'exigence « N fois par seconde ».
Point de contrôle
Pour savoir si la barre latérale doit apparaître, peut-on regarder
hinge.angle?En SwiftUI, que veut dire
hinge == nil? Et le nil d'UIKit ?Si l'on veut seulement savoir si c'est semi-ouvert, lit-on l'angle ou status ?
Pas encore dans ce chapitre. Ne traitez pas l'écran externe comme une seconde fenêtre que vous placez vous-même. C'est le chapitre suivant, et cela n'existe que pendant une prise de vue.
Chapitre 11 · Le contenu en plus sur l'écran externe n'est affiché par le système que pendant une prise de vue
À la fin de ce chapitre. Vous expliquez comment la scène principale se déplace entre les deux écrans, et à quel moment seulement on peut poser un contenu complémentaire sur l'écran externe.
L'exemple qui casse
À l'ouverture, couper l'interface en deux UIWindow : un dessine l'écran interne, l'autre l'écran externe. Le système ne laissera pas votre seconde toile sur la face qui se ferme. Le simulateur n'a pas non plus de caméra. Ce code ne se vérifie pas dans Device Hub.
L'expérience principale suit l'unique scène principale
À l'ouverture et à la fermeture, la scène principale change de face toute seule. Tous les boutons essentiels restent sur l'interface principale. Le contenu complémentaire sur l'écran externe peut être interactif, mais il doit rester peu.
La seule voie prise en charge est l'accessoire de prise de vue caméra. L'appareil est ouvert, l'app est au premier plan, l'écran interne est en plein écran, et une session de prise de vue est en cours : alors seulement le système peut l'afficher sur l'écran externe. Le système décide quand et où, et il peut le retirer à tout moment. Coupez avec isEnabled. Ne désenregistrez pas pour allumer et éteindre. L'état passe par le même model.
Le simulateur n'a pas de caméra. Cette section se fait sur un appareil réel. L'orientation de la caméra peut se retourner après une ouverture, une fermeture ou une rotation. Pour choisir la caméra, partez de « vers quelle face elle regarde » : voir Choosing a camera by the direction it faces.
ExternalNonInteractiveAccessory concerne un écran externe branché et AirPlay. Ce n'est pas l'écran externe du Duo.
Prompteur : l'interface principale peut toujours filmer
CameraCaptureAccessory est marqué dans le SDK comme inutilisable sur macOS, tvOS, watchOS, visionOS et Mac Catalyst. sceneAccessory et registerSceneAccessory(_:) existent depuis iOS 27.0. cameraCapture(sceneConfiguration:) et le rôle de session .windowCameraCaptureAccessory sont iOS 27.1. Quand il faut remettre le contexte au délégué de scène, utilisez la méthode de fabrique qui prend userInfo, et récupérez-le dans UISceneConnectionOptions.sceneAccessoryUserInfo.
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:), accroché danssceneAccessory.onAvailabilityChangedit si le système veut encore l'afficher. Quand l'utiliser : prompteur, compte à rebours, laisser la personne filmée voir l'image. Enregistrez-le sur la même vue que l'UI de prise de vue. Quand s'en passer : déclencheur, mise au point, réglages du chemin principal. L'écran externe peut disparaître à tout moment.registerSceneAccessory(iOS 27.0). UIKit :UISceneAccessory.cameraCapture(sceneConfiguration:), rôle.windowCameraCaptureAccessory. L'enregistrement renvoyé sert àunregisterSceneAccessory. Quand l'utiliser : une scène complémentaire pendant qu'une session de prise de vue existe. Quand s'en passer : ne désenregistrez pas pour cacher temporairement. Utilisez l'état d'activation.ExternalNonInteractiveAccessory. L'écran auquel on est connecté / AirPlay. Quand l'utiliser : vous devez déjà prendre en charge un grand écran branché, et le contenu n'a pas besoin d'interaction. Quand s'en passer : la face externe du Duo quand il est fermé.Simulateur. Device Hub sait ouvrir, fermer, tourner et plier. Il n'a pas de caméra. La plupart des extensions d'app ne tournent pas non plus dans le simulateur Duo. Quand l'utiliser : mise en page, barres, sheet, Split View. Quand s'en passer : ne déclarez pas dans le simulateur que l'accessoire de prise de vue a été vérifié.
Point de contrôle
Hors prise de vue, peut-on épingler un tableau de bord sur l'écran externe ?
Quand le prompteur n'est pas disponible, où doit être le déclencheur ?
Pourquoi cette section ne peut-elle pas se conclure seulement dans le simulateur ?
Jusqu'ici. Si le produit n'a pas de caméra, ce chapitre peut ne pas écrire de code. Les chapitres 0 à 9 couvrent déjà le chemin principal, du dessin à la mise en page adaptative.
Source : Preparing · Improve your app’s camera handling. Tech Talk 111465.
Annexe · Correspondances, alertes et réponses
Les douze chapitres précédents construisent le jugement dans l'ordre. Ici, les trois couches d'API, les écritures qui ne doivent pas apparaître, et les réponses des points de contrôle sont réunies, pour revenir vérifier après un chapitre.
Trois couches. Ne sautez pas
| Couche | Quand l'écrire | SwiftUI | UIKit |
|---|---|---|---|
| 1 · N'importe quelle taille | Chapitres 3 à 6. Toutes les fenêtres Apple en profitent | NavigationSplitView, TabView, GridItem(.adaptive), ViewThatFits, AnyLayout, size class, onGeometryChange, .toolbar système |
UISplitViewController, UINavigationController, UITabBarController, largeur du conteneur, trait, layoutSubviews, viewWillTransition |
| 2 · Connaître le pli et la barre verticale | Dans les chapitres 5 à 8, quand le conteneur système ne suffit pas | reservedRegions, ArrangementView, toolbarVerticalBehavior, axisBehavior, visibilityPriority, presentationPlacement, toolbarVerticalEdge |
reservedRegions(kind:options:), UIArrangementViewController, preferredVerticalBarBehavior, axisBehavior, verticalBarEdge, preferredPlacement |
| 3 · Le matériel lui-même, seulement | Chapitres 10 à 11, quand le produit en a vraiment besoin | onHingeChange, CameraCaptureAccessory, sceneAccessory |
UIHingeInteraction, UISceneAccessory.cameraCapture, rôle .windowCameraCaptureAccessory |
Les règles d'entrées de la barre verticale sont au chapitre 5, parce que le SDK 27.1 dresse les barres système. Pas parce que vous devez interroger le pli. L'interrogation du pli ne commence qu'au chapitre 7.
Disponibilité, pour ne pas mélanger
| Système | Ce qui n'existe qu'à ce palier |
|---|---|
| iOS 26 | ConcentricRectangle, backgroundExtensionEffect(), UIBackgroundExtensionView, UICornerConfiguration |
| iOS 27.0 | visibilityPriority, ToolbarOverflowMenu, topBarPinnedTrailing, presentationPlacement, defaultTabBarPlacement, isTabViewSidebarAvailable, sceneAccessory / registerSceneAccessory |
| iOS 27.1 | Comportement et bord de la barre verticale, axisBehavior, comportement de compression, régions réservées, Arrangement, charnière, CameraCaptureAccessory |
En UIKit, leadingItemGroups, pinnedTrailingGroup et additionalOverflowItems existent depuis iOS 16. Sur Duo, ils portent l'ordre et le débordement dans la barre verticale, mais les propriétés elles-mêmes ne sont pas apparues en 27.1.
La priorité de visibilité SwiftUI est .automatic / .low / .high. Les valeurs UIKit correspondantes sont .standard / .low / .high. Le comportement de compression diffère aussi d'un nom : .prefersToolbarItems en SwiftUI, .prefersBarItems en UIKit.
En ouvrant un fichier, cherchez d'abord ceci
Si cela sert à une mise en page ordinaire, il faut le changer :
UIScreen.main.bounds,UIScreen.main.scale, unUIScreenmis de côtéUIWindow(frame: UIScreen.main.bounds)userInterfaceIdiom == .padou.phoneUIDevice.current.orientation,statusBarOrientation,interfaceOrientation, y compriseffectiveGeometry.interfaceOrientationhorizontalSizeClass == .regularqui entre dans une page « iPad seulement »bounds.height == 844, ou une table de hauteurs de modèles connussafeAreaInsets.left * 2if isDuo,if isFolded, l'angle de charnière pour décider la barre latérale ou le nombre de colonnesun grand frame écrit en dur, une largeur de barre latérale écrite en dur, un nombre de colonnes de grille écrit en dur
une barre du haut dessinée à la main, ou un
UIToolbar/UINavigationBar/UITabBarcréé à partun bouton de barre d'outils qui n'a qu'un titre, sans icône ; un menu « … » dessiné soi-même
un contenu essentiel centré, posé sur le pli ; un état pour compact et un autre pour l'état déplié
un visuel principal en
.fillsans point de focus, et sans choix entre fill et fit selon le ratiotoolbarVerticalBehavior(.disabled)en dehors des exceptions écrites dans ce document, ou un va-et-vient selon l'état de la page
Postures à avoir vues au moins une fois dans Device Hub
| Posture | Attendu |
|---|---|
| Fermé, portrait | Écran externe, largeur compact + hauteur regular, barre verticale |
| Fermé, paysage | Double compact, barre verticale |
| Ouvert, portrait | Écran interne regular × regular, barre horizontale. C'est la seule posture courante qui garde les barres en haut et en bas |
| Ouvert, paysage | regular × regular, barre verticale |
| Semi-plié | La même hiérarchie. Le contenu précis ne s'assoit pas sur le pli. Le contenu qui défile ne change pas de route pour toute la page |
| Split View sur l'écran interne, soi à gauche, soi à droite | La barre colle à son propre bord extérieur. L'état est encore là |
Chaque sheet et chaque popover doit être ouvert une fois dans ces postures. Les captures pour la boutique suivent les Screenshot specifications d'App Store Connect. Un rangement courant est 1398×2034 pour l'écran externe et 2007×2853 pour l'écran interne. En paysage, on échange les deux côtés. N'écrivez pas ces nombres dans la mise en page. Si un document dit « à partir d'avril 2027, la soumission exige des captures Duo », la date exacte est celle de l'annonce Apple du moment.
Réponses des points de contrôle
Chapitre 0. En portrait sur l'écran interne, les barres système sont en haut et en bas. Les six postures restent une seule interface. Une fois ouvert, c'est encore une app iPhone. L'écran interne est simplement devenu regular × regular.
Chapitre 1. Un formulaire de réglages large de 900 pt a étiré les phrases. L'étiquette et l'interrupteur sont séparés par du vide. Il faut limiter la largeur, et laisser l'espace en trop au niveau suivant. Le dégradé peut rester au centre. Le bouton Payer, non. Le tout-sélectionner de la liste suit la liste, pas la barre système la plus à droite.
Chapitre 2. La cible de déploiement peut rester iOS 18. Une fois compilé avec le SDK 27.1, la barre verticale apparaît. Quand le tour est vide, on change de SDK de compilation. On ne détecte pas le modèle pour étirer le frame. Les nouveaux symboles n'existent pas sur l'ancien système, donc ils doivent entrer dans #available.
Chapitre 3. La largeur regular est vraie sur l'écran interne du Duo, donc elle n'est plus égale à iPad. L'écran interne ignore la déclaration d'orientation. Large ou haut : comparez la largeur et la hauteur du conteneur. Les insets gauche et droit peuvent être inégaux. left * 2 se trompe sur l'autre côté.
Chapitre 4. La sélection vit dans le même état, à l'extérieur de la vue scindée. Le nombre de colonnes vient de la largeur du conteneur divisée par la largeur minimale d'un élément. Ici, cela tombe à 2. ViewThatFits répond à quelle disposition tient.
Chapitre 5. Un bouton qui n'a qu'un titre n'entre pas dans la barre verticale. Dans la vue scindée, seule la barre de la colonne de détail est verticale. Une page de navigation range d'abord les outils dans le débordement, et protège la barre d'onglets. Un UIToolbar créé à part n'est pas une barre fournie par le conteneur de navigation. Le système ne le dresse pas.
Chapitre 6. Sur l'écran interne, la barre d'outils d'un sheet trailing est verticale. La safe area gauche et droite est asymétrique. On ne peut pas la miroiter. Un voile plein écran dessiné soi-même n'évite pas le pli comme le fait un sheet système.
Chapitre 7. Une fois complètement à plat, la division n'est pas active, et sa largeur est 0. Un article qui défile ne se déplace pas paragraphe par paragraphe. La caméra de l'écran interne n'occupe pas l'occlusion en temps ordinaire. Elle ne l'occupe que pendant l'usage.
Chapitre 8. ZStack se mappe sur overlay. Arrangement est enveloppé par la navigation, pas l'inverse. Le mettre dans une ScrollView rend l'un des deux morceaux inatteignable.
Chapitre 9. Le brouillon revient du même état, pas d'un autre arbre de vues. La barre verticale de la moitié droite du Split View est à droite, c'est-à-dire sur le bord extérieur de cette app. Une nouvelle fenêtre ne peut pas être créée sur l'écran externe.
Chapitre 10. La barre latérale ne regarde pas l'angle de la charnière. Le nil de SwiftUI veut dire que cet appareil n'a pas de charnière. Le nil d'UIKit veut dire que l'interaction a quitté la hiérarchie qui peut fournir des mises à jour. Pour savoir seulement si c'est semi-ouvert, on lit status.
Chapitre 11. Hors prise de vue, on ne peut pas épingler un tableau de bord sur l'écran externe. Le déclencheur reste sur l'interface principale. Le simulateur n'a pas de caméra. L'accessoire de prise de vue se vérifie sur un appareil réel.
Entrées officielles
ReservedRegion, et
UIView.ReservedRegioncôté UIKitArrangementView, et
UIArrangementViewControllerTech Talks : 111466 conception, 111461 préparation, 111462 barres verticales, 111463 postures et Arrangement, 111464 plusieurs écrans et scènes, 111465 caméra
La page Preparing cite aussi WWDC 2026 · 278 et WWDC 2025 · 282
Bonnes pratiques · Les entrées du bas restent, la largeur va au contenu
Une fois ceci lu. Vous confrontez cette app aux règles précédentes, et vous séparez ce qui est déjà posé à la façon d'Apple, ce qui est un choix de produit, et ce qui ne peut pas encore compter comme vérifié.
Cette app n'a pas de version Duo. La cible de déploiement reste iOS 18. C'est la compilation avec le SDK 27.1 qui remplit l'écran et permet aux barres système d'être verticales. Ouvrir et fermer ne change ni la route ni le modèle de page. La largeur regular en trop sert aux colonnes gauche et droite à l'intérieur d'« Aujourd'hui » et de « Respiration », pas à transformer les trois entrées de premier niveau en barre latérale. L'angle de charnière, l'écran externe caméra et les fenêtres multiples n'ont pas de raison produit, donc on ne les écrit pas.
Correspondance
| Le jugement du tutoriel | Ce que fait cette app |
|---|---|
| Chapitres 0 à 2 : une seule interface, le SDK est l'interrupteur | Pas de bifurcation sur le nom de l'appareil. Les nouvelles API sont enveloppées par #available. L'ancien système garde la même relation en HStack / VStack |
| Chapitres 3 à 4 : montrer le niveau suivant selon la largeur du conteneur | Une coque compact reste une seule colonne, même si elle est plus large. En regular, 800 pt est encore une seule colonne. Côte à côte commence à 801 pt. On n'entre dans un cran plus aéré que si la colonne principale maximale de 480 pt et le détail lisible de 720 pt tiennent ensemble (1201 pt au total). 1024 pt reste côte à côte. Ce n'est pas une phrase étirée |
| Chapitre 5 : barres système, mais la barre latérale n'est pas obligatoire | .sidebarAdaptable plus defaultTabBarPlacement(.tabBar). Les trois onglets restent en bas |
| Chapitre 6 : le sheet déclare sa place ; un voile dessiné n'évite pas le pli | Le sheet système, en regular, va en trailing ou en center selon l'intention. Le paywall en compact reste un voile sur la racine. Il n'évite pas le pli tout seul |
| Chapitres 7 à 8 : ne déplacer que les deux contenus qui forment une paire | Respiration, bande horizontale du paywall, colonnes principale et secondaire de même largeur utilisent le split d'ArrangementView. L'accueil et le détail qui défilent ne changent pas de route à cause du pli |
| Chapitres 9 à 11 : l'état est continu ; les API matérielles sont facultatives | La sélection et la route vivent en dehors de la mise en page. Pas de onHingeChange, pas d'accessoire de prise de vue, pas de fenêtres multiples |
La navigation racine garde trois entrées
L'écran interne peut devenir une barre latérale. Ce produit choisit de ne pas le faire. « Aujourd'hui », Tendances et Respiration restent des onglets du bas. Réglages se pousse depuis « Aujourd'hui ». Le chemin iPad utilise defaultAdaptableTabBarPlacement(.tabBar). iOS 27 précise encore defaultTabBarPlacement(.tabBar). Une fois l'écran interne ouvert, l'espace en trop apparaît à l'intérieur de la page : Aujourd'hui, carte à gauche et détail à droite ; Respiration, mode à gauche et exercice à droite.
Côte à côte exige deux conditions en même temps : la coque de cette page est regular, et la size class horizontale courante est regular aussi. Ainsi l'arbre étroit ne vole pas la sélection partagée. Si la coque est compact, le cran reste une seule colonne, même si le conteneur est très large.
TabView(selection: adaptiveSectionSelection) {
// 三个 Tab
}
.tabViewStyle(.sidebarAdaptable)
.defaultAdaptableTabBarPlacement(.tabBar)
// iOS 27:content.defaultTabBarPlacement(.tabBar)
adaptiveShellRole == .regular && horizontalSizeClass == .regular
Arrangement ne décrit qu'une relation qui existe déjà
La navigation reste dehors. La mise en page des anciennes versions et l'Arrangement 27.1 sont la même relation, pas une autre page.
| Cas | La relation d'origine | 27.1 |
|---|---|---|
| Exercice de respiration | Le visuel principal est en haut, les contrôles en bas | .split. Haut : haut et bas. Large : gauche et droite, et l'on évite le pli. On ne verrouille pas l'axe, et l'on ne regarde pas l'angle de la charnière |
| Paywall en horizontal | Le visuel principal et l'achat sont côte à côte | .split. L'ancien système reste un HStack |
| Colonnes principale et secondaire de même largeur | Les deux colonnes ont besoin d'un espace à elles | .split.axes(.horizontal), ratio 0.5. Le fond peut franchir le pli. Le contenu touchable est à l'intérieur d'Arrangement |
Ce que le système fait, et ce qui ne compte pas encore comme fini
La safe area se lit sur les quatre bords. Les images dans une liste qui défile et dans un détail ne se déplacent pas à cause du semi-plié. Sheet système : compact prend .automatic. En regular, on choisit trailing ou center selon l'intention.
Ce que l'on ne fait pas : ne pas poser la respiration ni la barre latérale avec l'angle de la charnière. Pas de caméra, donc pas d'enregistrement d'accessoire de prise de vue. Pas de nouvelle fenêtre créée sur l'écran externe. Pas de modèle de page copié pour les six postures. On ne reconsidère que si le produit veut vraiment traiter l'ouverture comme une entrée, ou si une interface de prise de vue apparaît.
Ce qui ne compte pas encore comme fini : les six postures doivent encore être regardées à la main dans Device Hub. Les commandes publiques du simulateur ne savent pas ouvrir, fermer ou semi-plier. Le voile de paywall en compact n'est pas un sheet système. Pour le sheet d'explication qui n'a qu'un bouton de fermeture, couper ou non la barre verticale reste à trancher. La barre du haut fixe utilisée par la transition d'éléments partagés est encore hors de la couche de zoom. Compiler ne veut pas dire que le pli et la barre verticale ont été vus.
Comment brancher une nouvelle page. D'abord la poser dans un onglet existant ou dans la pile de navigation, et laisser l'état dehors. Ensuite, selon le cran du conteneur, une colonne ou côte à côte. Le texte long s'arrête vers 720 pt. Arrangement seulement quand deux contenus devaient déjà permuter, et #available(iOS 27.1, *) pour garder l'ancienne mise en page. D'abord la toolbar système, la size class, la largeur de lecture. Ensuite les régions réservées et Arrangement. La charnière et l'écran secondaire restent non écrits, jusqu'à ce que le produit en ait besoin.
Se tester avec cette app
Une fois l'écran interne élargi, les trois onglets doivent-ils monter en barre latérale, ou rester en bas ?
Quand le conteneur regular n'a que 800 pt, « Aujourd'hui » doit-il être en une colonne ou côte à côte ?
Pourquoi la sphère et les contrôles de l'exercice de respiration utilisent-ils
.splitplutôt queonHingeChange?Le voile de paywall en compact, au semi-plié, est posé sur le pli. Peut-on dire « le sheet système l'évite » ?
Réponses. Les trois onglets restent en bas. 800 pt est encore une seule colonne. Côte à côte commence à 801 pt. La sphère et les contrôles sont la relation haut-bas du même exercice. On la confie à split. L'angle ne décide pas la mise en page. Le voile de paywall n'est pas un sheet système. Il n'évite pas le pli tout seul.
Continuer
Essayez ces idées dans Stress HRV
Télécharger sur l'App StoreCes notes partagent une réflexion produit pour le bien-être. Ce n’est pas un avis médical.