Diseñar para el iPhone Duo, del diseño al código
Doce capítulos, SwiftUI y UIKit para la misma situación. Un juicio que se puede comprobar por capítulo, y al final por qué las pestañas se quedan abajo.
Haz que la interfaz se vea bien en cualquier ancho y, solo después, suma el hardware plegable. El iPhone Duo tiene pantalla exterior, pantalla interior y bisagra. No es un idiom nuevo, y no debe hacer crecer una segunda app. Lo que sigue está escrito según la curva de aprendizaje: cada capítulo entrega un solo criterio que puedes comprobar. Primero mira la forma que se rompe, luego por qué hay que diseñarlo así, y después el mismo escenario en SwiftUI y en UIKit.
Las firmas se contrastan con el SDK de iOS 27.1 de Xcode 27.1. El comportamiento procede de Designing for iPhone Duo, Preparing your app for iPhone Duo y Three steps. El cuerpo es una exposición, no una copia literal del original.
Cómo leerlo: cada capítulo empieza con «Al terminar podrás», sigue con el ejemplo que se rompe, la forma correcta y «cuándo usarlo / cuándo no usarlo». Responde primero los puntos de control; las respuestas están al final del texto. Los capítulos 0 a 6 no consultan el pliegue, no usan Arrangement y no leen el ángulo de la bisagra. Eso viene después.
Capítulo 0 · Seis posturas son la aceptación, no seis interfaces
Al terminar podrás. Al ver un Duo cerrado, abierto, a media apertura y girado, decir en qué ancho debe pensar la interfaz, en lugar de escribir una versión distinta para cada postura.
El ejemplo que se rompe
No organices el producto así: vertical cerrado, horizontal cerrado, vertical abierto, horizontal abierto, media apertura como un libro y media apertura de pie sobre la mesa, cada uno con su propio árbol de vistas. Un solo ciclo de abrir y cerrar cambia la navegación, cambia los botones y pierde la posición de desplazamiento.
SwiftUI ramificado por postura:
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 ramificado por postura:
func installRoot(for pose: String) {
let root: UIViewController
switch pose {
case "closed": root = PhoneHomeController()
case "open": root = TabletHomeController()
default: root = BookHomeController()
}
// 三套控制器,三份选中项
}
Por qué Apple solo da dos anchos
El Duo tiene pantalla exterior y pantalla interior. La bisagra puede cerrarse, abrirse del todo o quedarse a medias. La persona lo sostiene en vertical, en horizontal, lo abre como un libro o lo deja de pie sobre la mesa. Todo eso es un cambio continuo dentro del mismo uso. El HIG pide esto: la pantalla exterior usa ancho compact, la pantalla interior usa ancho regular, y el diseño que ya tienes crece con el espacio disponible. No rehagas la app para cada forma de sujetarla. Sigues diseñando un iPhone. Las reglas de diseño, de barras y de área segura de iOS siguen vigentes.
| Lo que ves | Qué es | Cómo pensarlo al diseñar |
|---|---|---|
| Cerrado | Pantalla exterior. Más ancha y más baja que un iPhone habitual | Ancho compact. En vertical, la altura sigue siendo regular |
| Totalmente abierto | Pantalla interior grande, regular × regular | Extensión natural de un diseño de iPad, pero sigue siendo una app de iPhone |
| Media apertura | La pantalla interior queda partida en dos lados por el pliegue | Sigue siendo la misma jerarquía. Este capítulo todavía no trata el pliegue |
| Rotación y Split View | Distintos contenedores en el mismo dispositivo | El ancho pasa por valores intermedios. No hay solo dos escalones, «exterior / interior completa» |
La dirección de las barras, una frase para memorizar desde ahora: la vertical de la pantalla interior es la única postura que conserva las barras horizontales arriba y abajo. En la pantalla exterior, y en la interior en horizontal, las barras del sistema se mueven al lateral. En Split View, la barra de cada lado se pega a su propio borde exterior. Esos detalles se convierten en código en el capítulo 5.
La forma correcta de organizarlo
Una sola interfaz. El estado vive fuera del diseño. Si cambia el ancho, solo cambia la disposición, no los datos.
struct Root: View {
@State private var selection: Item.ID?
var body: some View {
Home(selection: $selection)
}
}
final class RootController: UIViewController {
let model = HomeModel()
// 开合时只让同一个控制器重新布局
}
Las palabras que hay que retener en este capítulo
Todavía no hay una API de Duo. Usa bien las palabras ahora, para no conectarlas mal después.
Pantalla exterior / pantalla interior. Cerrado se mira la exterior; abierto, la interior. La escena principal cambia de pantalla sola. Cuándo usarlo: al describir el hardware y, en la aceptación, al decir con claridad qué cara estás probando. Cuándo no usarlo: no trates las dos caras como dos lienzos que tú administras.
Postura. Cerrado, abierto, media apertura, y además la orientación. Cuándo usarlo: en Device Hub, ábrelas una por una y comprueba si sigue estando la misma interfaz. Cuándo no usarlo: no hagas
switch posepara elegir el diseño.compact / regular. Diseña la exterior como ancho compact y la interior como ancho regular. Cuándo usarlo: decidir si se ve una sola capa a la vez o dos capas a la vez. Cuándo no usarlo: no leas regular como «esto es un iPad». Cómo se lee está en el capítulo 3.
Barra horizontal / barra vertical. La interior en vertical mantiene las barras de arriba y de abajo. El resto de posturas habituales recoge la navegación, la barra de herramientas y la barra de pestañas en el lateral. Cuándo usarlo: al dibujar las barras, dibújalas en las dos direcciones. Cuándo no usarlo: no dibujes una barra lateral falsa para imitarlas.
Punto de control
En la interior en vertical, ¿las barras del sistema están arriba y abajo, o en el lateral?
¿Cuántas interfaces hay que hacer para las seis posturas?
Después de abrir el Duo, ¿se ha convertido en una app de iPad?
En este capítulo todavía no. No escribas if isDuo, no leas el ángulo de la bisagra y no consultes el pliegue. Ahora solo tienes que rechazar «una UI por cada postura».
Fuente: HIG · Designing for iPhone Duo, Tech Talk 111466.
Capítulo 1 · Si hay más ancho, muestra la capa siguiente; no estires la frase
Al terminar podrás. Cuando te den una zona que se ha ensanchado, decidir si hay que añadir una columna, añadir una fila o si solo estás estirando la interfaz estrecha de antes. También podrás decir qué puede cruzar el pliegue y qué no.
El ejemplo que se rompe
La página de ajustes es una columna cómoda en la pantalla exterior. Al abrir la interior, cada fila de la misma lista se estira hasta casi toda la pantalla. El título y el interruptor se van a los dos extremos, y en medio queda un gran vacío. Otra forma de romperlo: colocar el botón principal, un código QR o una cara justo en el centro de la pantalla, de modo que a media apertura el pliegue los corte.
Cuatro principios que puedes llevar a una revisión
La función y el estado no se cortan. Lo que se puede hacer cerrado sigue estando abierto. El desplazamiento, la selección y el contenido que se está editando no se reinician porque el dispositivo se abra o se cierre. Lo que cambia es cuántas capas se ven a la vez.
El ancho que sobra sirve para mostrar la jerarquía. El correo en la exterior es «lista; solo al tocar se ve la carta». En la interior, la lista y la carta pueden ir una al lado de la otra. Eso es ver una capa más, no convertir el ancho de línea de la carta en 900 pt.
El pliegue es como el lomo de un libro. El fondo, el degradado y un artículo que se desplaza pueden cruzarlo. Un botón, una frase, una cara, un código QR o un asa de arrastre no deben montarse en el centro. Este capítulo solo establece ese criterio. Cómo consultar el pliegue es el capítulo 7.
El control se queda cerca del contenido sobre el que actúa. Los botones que gobiernan una lista se quedan encima de la lista. No los empujes todos lejos solo porque haya una barra vertical en el lateral.
Un juego tiene que llenar la postura actual. Cambia primero la proporción de la imagen, en lugar de dejar bandas negras arriba y abajo o a los lados. El tamaño físico del texto y de los botones debe mantenerse lo más estable posible.
La misma página de ajustes, dos anchos
A un texto largo y a un formulario dales un límite de lectura. El espacio que sobra se reserva para la jerarquía de al lado, no para una frase más larga.
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 esta zona ya es lo bastante ancha como para colocar a la vez «lista» y «detalle», el capítulo siguiente lo hará con la columna dividida del sistema. Ahora no escribas a mano una barra lateral con HStack, y no agrandes el tamaño de letra una vuelta entera solo porque hay más ancho.
Preguntas para llevar a revisión; todavía no son una API
Antes de ensanchar, pregunta. Navegación persistente, detalle complementario, inspector, vista previa, más controles, mayor densidad de información. Cuándo usarlo: al pasar de compact a muy ancho, busca en ese orden la estructura que puede mostrarse a la vez. Cuándo no usarlo: no empieces por agrandar el espaciado y el tamaño de letra para rellenar el vacío.
Prueba del lomo. Imagina en el centro una costura que no se puede tocar con precisión. Puede cruzarla: un fondo fotográfico, una textura, una lista que se desplaza de continuo y un artículo. No debe cruzarla: un botón, una frase corta que hay que leer entera, una cara, un código QR, un campo de texto, un icono pequeño.
Agrupa junto a lo que afecta. La acción frecuente se queda al lado del bloque al que afecta. Cuándo usarlo: el filtro de una lista se queda en la lista; compartir un detalle se queda en el detalle. Cuándo no usarlo: no mandes la acción principal a una esquina muy lejos del contenido solo por simetría.
El juego llena la postura. Si la postura cambia, cambia la proporción de la imagen. Cuándo usarlo: puedes bloquear vertical u horizontal, pero la postura actual tiene que llenarse. Cuándo no usarlo: no arregles el hueco con bandas negras. Si de verdad tienes que dejar un margen, rellénalo con imagen, no con un color plano.
Punto de control
En la pantalla interior, estirar el formulario de ajustes hasta 900 pt de ancho: ¿dónde está el problema?
A media apertura, el fondo en degradado y el botón «Pagar»: ¿cuál de los dos puede quedarse en el centro?
«Seleccionar todo» de la lista de correo, ¿debe seguir a la lista o a la barra del sistema del extremo derecho?
En este capítulo todavía no. No rehagas la página entera para la media apertura, y no escribas todavía el código que consulta el pliegue. En el boceto solo tienes que marcar los elementos que «no pueden montarse en el centro».
Capítulo 2 · El SDK con el que compilas decide cuánto se llena
Al terminar podrás. Distinguir el destino de implementación del SDK de compilación, y juzgar por qué un paquete que ya se abre en el Duo sigue dejando bandas vacías y la barra todavía arriba.
El ejemplo que se rompe
Un paquete compilado con el SDK de iOS 26 se puede instalar. Al abrir la pantalla interior, la ventana queda centrada y alrededor hay vacío. Al cerrar, el contenido se detiene a la izquierda de la barra de estado y de la cámara. Alguien escribe entonces otro parche: «si es un Duo, estira el frame hasta llenarlo».
Tres escalones de SDK, sin un interruptor Duo aparte
Llenar la pantalla y la barra vertical llegan con el SDK de compilación. No son un booleano dentro de Info.plist. El destino de implementación puede seguir en un sistema más bajo, para que los dispositivos antiguos sigan pudiendo instalar. Pero si en el Duo quieres usar toda la pantalla y que las barras del sistema entren en forma vertical, este paquete tiene que compilarse con el SDK de iOS 27.1 o con uno más nuevo. Antes de publicar un paquete 27.1, confirma que el contenido sigue pudiéndose tocar después de que las barras se vuelvan verticales.
| SDK usado para compilar | Pantalla interior al abrir | Barras |
|---|---|---|
| iOS 26 o anterior | Centrada, con vacío alrededor. En la exterior, el contenido queda a la izquierda de la barra de estado y de la cámara | Conserva tus barras horizontales de antes |
| iOS 27 | Llena la mayor parte de la interior, pero sigue evitando la barra de estado de la derecha | La barra de herramientas sigue arriba |
| iOS 27.1 o más nuevo | Usa toda el área de visualización | La barra de herramientas y la barra de pestañas se ordenan en una barra vertical, debajo de la barra de estado |
El destino de implementación puede ser más bajo; las llamadas tienen que ir con puerta
Los dos fragmentos de abajo todavía no llaman a ninguna API de plegado. Solo instalan el hábito que hay que guardar en cada capítulo siguiente: los símbolos de 27.1 van dentro de una comprobación de disponibilidad. En Mac Catalyst, el problema conocido de Xcode 27.1 es que esos símbolos de 27.1 pueden no compilar, y hay que separarlos otra vez.
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
Hechos ligados a la compilación
Destino de implementación
IPHONEOS_DEPLOYMENT_TARGET. Cuándo usarlo: decidir el sistema más antiguo que puede instalar. Cuándo no usarlo: subirlo a 27.1 no es el requisito para llenar la pantalla. Lo que decide el llenado es el SDK de compilación.SDK de iOS 27.1. El interruptor de pantalla completa más barra vertical. Cuándo usarlo: cuando vayas a entregar en el Duo la forma completa, compila con este SDK. Cuándo no usarlo: no publiques un paquete 27.1 antes de haber adaptado las barras verticales.
#available(iOS 27.1, *). Si el destino de implementación es inferior a 27.1, los símbolos nuevos tienen que ir envueltos. Cuándo usarlo: cada llamada a una API de 27.1 a partir del capítulo 5. Cuándo no usarlo: no envuelvas con esto un size class ordinario. Esos existen en sistemas anteriores.Mac Catalyst
#if !targetEnvironment(macCatalyst). Cuándo usarlo: cuando una API exclusiva de 27.1 impida compilar el destino Catalyst, separa esas llamadas. Cuándo no usarlo: no borres por eso la adaptación del iPhone. El accesorio de cámara, en sí, no está disponible en Catalyst.
Punto de control
El destino de implementación sigue siendo iOS 18, pero compilas con el SDK 27.1. ¿Aparecen las barras verticales?
El paquete ya arranca en el Duo y alrededor hay vacío. El paso siguiente, ¿es detectar el modelo y estirar, o cambiar de SDK?
¿Por qué una llamada nueva de 27.1 no puede escribirse directamente en la ruta de código de iOS 18?
En este capítulo todavía no. No leas el tamaño de la pantalla para llenarla. El capítulo siguiente es el que sustituye UIScreen.main. Las reglas de los ítems de la barra vertical tampoco han llegado.
Fuente: Three steps to make your app shine on iPhone Duo. Xcode 27.1 necesita macOS Tahoe 26.6 o una versión más nueva. El simulador se abre, se cierra, se gira y se pliega en Device Hub.
Capítulo 3 · Pregunta cuánto mide esta zona, no si esto es un Duo
Al terminar podrás. Convertir las ramas de diseño hechas por dispositivo, por orientación y por UIScreen.main en un recálculo según el size class y el tamaño del contenedor actual.
El ejemplo que se rompe
El mismo Duo puede aparecer en la pantalla exterior, a pantalla completa en la interior y en Split View dentro de la interior. El área segura y las barras también cambian. Si lees la pantalla una vez al arrancar, un pliegue deja esa lectura caducada. La pantalla interior tampoco obedece la orientación que declaraste en 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
Dos hechos fáciles de recordar al revés
El ancho regular ya no equivale a iPad. La pantalla interior es ancha regular y alta regular. El código que antes decía «si es regular, ve a la página exclusiva de iPad» ahora se ejecuta en un iPhone. Lo que pide Apple es que la interior parezca la extensión natural de tu diseño de iPad. Arregla ese diseño. No añadas otro juicio de idiom para apagarlo.
La orientación no es la forma. Para saber si ahora es más ancho que alto o más alto que ancho, compara el ancho y el alto de la zona que posees. La exterior en vertical es ancho compact más alto regular. La exterior en horizontal es compact en las dos direcciones. La interior es regular en las dos direcciones. A partir de aquí, una app de iPhone puede encontrarse cualquier combinación de size class.
El tamaño cambia a mitad de la sesión. No apuntes una cifra en viewIsAppearing y la reutilices hasta el final.
Recalcula según el contenedor
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)
Cuando de verdad necesites UIScreen, tómalo en el momento de usarlo desde view.window?.windowScene?.screen. No lo guardes como propiedad. Crea la ventana desde la scene. No uses los bounds de la pantalla principal como frame.
API
horizontalSizeClass. SwiftUI:@Environment(\.horizontalSizeClass). UIKit:traitCollection.horizontalSizeClass. Cuándo usarlo: distinguir ancho compact y ancho regular, y combinarlo con el size class vertical. Cuándo no usarlo: no traduzcas.regularcomo «página de iPad».onGeometryChange. En SwiftUI recibes elCGSizedel contenedor cuando cambia el tamaño. En UIKit, lo correspondiente esviewDidLayoutSubviewsyviewWillTransition(to:with:). Cuándo usarlo: comparar ancho y alto, o medir en local según el contenedor actual. Cuándo no usarlo: no lo uses para leerUIScreen.main.bounds.displayScale. SwiftUI:@Environment(\.displayScale). UIKit:traitCollection.displayScale. Cuándo usarlo: cuando necesites la proporción de píxeles. Cuándo no usarlo: no vuelvas a leerUIScreen.main.scale. La referencia a la pantalla principal va a quedar en desuso.UIWindow(windowScene:). Crea la ventana con la scene. Cuándo usarlo: sustituirUIWindow(frame: UIScreen.main.bounds). Cuándo no usarlo: el cálculo del diseño sigue mirando los bounds de la view o de la window, no la pantalla física entera.
Punto de control
¿
horizontalSizeClass == .regularpuede cumplirse ahora también en un iPhone?La pantalla interior se ha puesto en horizontal. ¿Sirve
interfaceOrientationpara juzgar «ahora es más ancha»?¿Por qué
safeAreaInsets.left * 2calcula mal el ancho usable?
En este capítulo todavía no. No uses un umbral de ancho if width > 700 para cambiar entre dos árboles de navegación. El capítulo siguiente resuelve «estrecho, se avanza; ancho, van en paralelo» con un contenedor del sistema.
Fuente: el Step 2 de Three steps, y las resizing considerations de Preparing. Tech Talk 111461.
Capítulo 4 · Deja que el contenedor del sistema decida si van en paralelo o se avanza
Al terminar podrás. Conservar la selección con un contenedor de columnas, decidir el número de columnas de una cuadrícula con el ancho mínimo de ítem, y reordenar en local según «qué disposición cabe», no según «en qué dispositivo estoy».
El ejemplo que se rompe
En compact, un NavigationStack. En regular, una barra lateral escrita a mano con HStack. Cada uno guarda su propia selección. La cuadrícula se escribe como «tres columnas en Duo, si no una». Un grupo de botones usa if width > 700 para partirse en dos vistas completamente distintas, una horizontal y otra vertical.
Orden de preferencia
Si puedes usar un contenedor de navegación del sistema, usa el contenedor. El contenido repetido en tarjetas usa una cuadrícula adaptable. Un «si en horizontal no cabe, ponlo en vertical» local usa un diseño que puede probar el espacio que queda. El size class viene después. La geometría exacta se deja para el final. La arquitectura de información de la columna dividida es «colección, selección, detalle». En estrecho, la lista avanza hacia el detalle. En ancho, van en paralelo. La selección tiene que sobrevivir a ese cambio. El ancho que sobra sirve para mostrar esta capa. El texto largo sigue con límite de ancho.
Lista y detalle
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
// 不要写死侧栏点数
Cuadrícula de tarjetas: ancho mínimo, no número de columnas
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)
}
}
Cuando Split View te corta a un ancho intermedio, el número de columnas cae solo a 2, en lugar de saltar de golpe de 3 a 1. Si a media apertura quieres un número par de columnas, eso es asunto del capítulo 7. Ahora no lo escribas.
La misma familia de vistas cambia de disposición, no de identidad
ViewThatFits(in: .horizontal) {
HStack { Preview(); Inspector() }
VStack { Preview(); Inspector() }
}
let layout = isWiderThanTall
? AnyLayout(HStackLayout())
: AnyLayout(VStackLayout())
layout {
Chart()
Legend()
}
UIKit no tiene ViewThatFits. La idea correspondiente es esta: en el momento del diseño, pregunta «¿todavía cabe en horizontal?» y luego cambia el eje del mismo UIStackView. No destruyas y vuelvas a crear los controladores hijos. Si lo haces, se pierde el texto que se está editando.
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. Cuándo usarlo: colección, selección, detalle. Estrecho, se avanza; ancho, van en paralelo. Cuándo no usarlo: no lo sustituyas por una barra lateral escrita a mano conHStack. No es la disposición de dos contenidos del capítulo 8.GridItem(.adaptive(minimum:)). UIKit: divide el ancho del contenedor entre el ancho mínimo de ítem y obtienes el número de columnas. Cuándo usarlo: tarjetas, álbumes, paneles y otro contenido repetido. Cuándo no usarlo: no lo escribas comoisDuo ? 3 : 1.ViewThatFits. Prueba, en el eje que le das, cuál de los subárboles cabe. Cuándo usarlo: una elección local, como un grupo de botones, metadatos, o una vista previa más un inspector. Cuándo no usarlo: no lo uses para cambiar la navegación de toda la app.AnyLayout. Cambia el contenedor de diseño y conserva la identidad de las subvistas. Cuándo usarlo: un gráfico y su leyenda, una vista previa y sus propiedades: cambia la disposición, pero siguen siendo esas dos vistas. Cuándo no usarlo: no escribas las subvistas una vez en cada rama de dosif.
Punto de control
Al volver de ir en paralelo a una sola columna, ¿dónde debe vivir la selección?
Después de Split View, el ancho solo alcanza para dos tarjetas de 220 pt. ¿De dónde sale el número de columnas?
ViewThatFits, ¿responde «qué dispositivo» o «qué disposición cabe»?
En este capítulo todavía no. Si la barra de pestañas debe convertirse en navegación lateral, se deja para el capítulo siguiente. Que dos bloques de contenido eviten el pliegue se deja para el capítulo 8. Ahora no introduzcas Arrangement.
Capítulo 5 · Las barras del sistema se ponen de lado solas; una barra falsa no
Al terminar podrás. Hacer que la navegación, la barra de herramientas y la barra de pestañas se reconozcan tanto en forma horizontal como en forma vertical, y entregar las acciones secundarias al menú de desbordamiento del sistema.
El ejemplo que se rompe
Arriba de la página hay un HStack dibujado por ti, o un UINavigationBar, un UIToolbar y un UITabBar creados aparte con new. El sistema no mete ese contenido en la barra vertical. Otro problema frecuente: el botón de compartir solo tiene el título "Share". La barra vertical solo muestra iconos. Un ítem sin icono no se pone en vertical en absoluto. Si además dibujas tú un menú «…», compite por el sitio con el menú de desbordamiento del sistema.
Cuándo es vertical la barra, y quién tiene derecho a entrar
Después de compilar con el SDK 27.1, la pantalla exterior funde la barra de navegación, la barra de herramientas y la barra de pestañas en una sola franja lateral. La interior en horizontal, y algunas vistas que están en posición leading / trailing, hacen lo mismo. La interior en vertical sigue usando barras horizontales. Dentro de una columna dividida, solo la columna de detalle participa en la barra vertical. La columna lateral y la columna de contenido se mantienen horizontales. El inspector se mantiene horizontal.
La barra se alinea con el borde del hardware. Un idioma de derecha a izquierda tampoco la cambia al otro lado. En Split View, la barra de la app de la izquierda está a la izquierda, y la barra de la app de la derecha está a la derecha.
Cada ítem tiene que tener a la vez título e icono. La forma vertical usa el icono. La forma horizontal prefiere el icono, y también puede usar el título. En el menú de desbordamiento aparecen juntos el icono y el título. Una vista personalizada se queda por defecto en la barra horizontal, salvo que declares que sirve para colocarse en vertical. Un botón de solo texto se queda en la barra horizontal. El botón Edit del sistema cambia así entre símbolo y texto.
La parte de arriba de la barra vertical se reserva para volver o cerrar, y después para una acción destacada como Done. El resto de ítems conserva su agrupación original. El desbordamiento recoge por defecto de abajo hacia arriba. Una página de navegación protege primero la barra de pestañas. Una página de tarea protege primero los ítems de herramientas.
El mismo grupo de acciones sirve en las dos barras
SwiftUI, colgado dentro de 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, colgado en el controlador hijo que está dentro del controlador de navegación:
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
El botón de volver lo coloca el propio controlador de navegación. El cierre personalizado entra en el lado leading. Done entra en pinnedTrailingGroup. Cuando la búsqueda ocupa todo el espacio, los ítems fijados son los últimos en entrar en el desbordamiento. additionalOverflowItems sirve para meter tu propio «más» dentro del menú del sistema. Los puntos suspensivos se reservan para el desbordamiento. Los demás menús usan otro símbolo.
Un ítem que va y viene entre símbolo y texto se marca .horizontalOnly: cuando no hay barra horizontal, se oculta, en lugar de meterse a la fuerza en la barra vertical. Si una vista personalizada tiene de verdad un dibujo vertical, usa .verticalPreferred. El espacio flexible mide 0 en el eje vertical. Agrupa con ToolbarItemGroup / UIBarButtonItemGroup. No insertes a mano un espaciado fijo. La insignia usa badge. No escribas el número dentro del título: si lo haces, a ese ítem le cuesta convertirse en un símbolo puro.
Salir de la barra vertical es una excepción estable
NavigationStack {
CalculatorView()
.toolbarVerticalBehavior(.disabled)
}
override var preferredVerticalBarBehavior: UIVerticalBarBehavior {
.disabled
}
El valor por defecto es .automatic. Sal solo en estas interfaces: vídeo a pantalla completa, un diseño de ancho completo que no se desplaza, como una calculadora, y un sheet que solo tiene un botón de cerrar. Es una elección estable. No lo cambies de un lado a otro en viewWillAppear según el estado de la página. Si solo una página tiene que ocultar la barra, usa la API de visibilidad, no apagues el comportamiento vertical. En UIKit, si la preferencia de verdad ha cambiado, llama a setNeedsUpdateOfVerticalBarConfiguration(). El controlador de navegación, por defecto, deja la decisión en la cima de la pila. La barra de pestañas se la deja a la página seleccionada en ese momento.
Saber de qué lado está la barra, y cómo el fondo se extiende por debajo
@Environment(\.toolbarVerticalEdge) private var verticalEdge
// HorizontalEdge?;这个上下文从不用垂直栏时为 nil
Image("hero")
.backgroundExtensionEffect()
let edge = traitCollection.verticalBarEdge
// .leading / .trailing / .unspecified
// 表示系统偏好的边,不表示栏此刻是否可见
// 从不用垂直栏的上下文返回 .unspecified
let backdrop = UIBackgroundExtensionView()
backdrop.contentView = hero
Para observar ese borde en UIKit, pasa UITraitCollection.systemTraitsAffectingVerticalBarEdge a registerForTraitChanges. Aunque el trait cambie, el borde puede seguir siendo el mismo.
Entrada de primer nivel: la pantalla interior puede volverse navegación lateral
En un iPhone, escribir solo .sidebarAdaptable sigue siendo la barra de abajo. Para convertirla en navegación lateral donde esté soportado, añade además el defaultTabBarPlacement(.sidebar) de iOS 27. No uses el defaultAdaptableTabBarPlacement de iOS 18: ese es para iPad. La interfaz relacionada con la barra lateral se mira en isTabViewSidebarAvailable. No vuelvas a adivinar el size class.
TabView {
Tab("Home", systemImage: "house") { HomeView() }
Tab("Library", systemImage: "books.vertical") { LibraryView() }
}
.tabViewStyle(.sidebarAdaptable)
.defaultTabBarPlacement(.sidebar)
tabBarController.sidebar.preferredPlacement = .sidebar
El producto también puede decidir, de forma explícita, mantener la barra de abajo y dejar el espacio ensanchado para las columnas dentro del contenido. Las dos opciones valen. No dibujes tú una barra lateral falsa según el ancho.
API
toolbarVerticalBehavior(iOS 27.1)..automatic/.disabled. UIKit: sobrescribepreferredVerticalBarBehavior, de tipoUIVerticalBarBehavior. Cuándo usarlo: calculadora, vídeo a pantalla completa, sheet con un solo botón de cerrar. Cuándo no usarlo: no lo cambies de un lado a otro según la navegación o según el estado de una vista.axisBehavior(iOS 27.1). SwiftUI:.automatic/.horizontalOnly/.verticalPreferred. UIKit:UIBarButtonItem.axisBehavior, los mismos tres valores. Cuándo usarlo: un ítem que alterna símbolo y texto usa horizontalOnly; una vista personalizada que de verdad tiene dibujo vertical usa verticalPreferred. Cuándo no usarlo: un botón ordinario, con título e icono, se queda en automatic.visibilityPriority(iOS 27.0). SwiftUI:.automatic/.low/.high, y tambiéninit(lowerThan:)/init(higherThan:). UIKit:.standard/.low/.high. Cuándo usarlo: da primero la prioridad al grupo entero y después a cada ítem dentro del grupo. Lo de uso frecuente y lo que lleva insignia se deja para el final, y solo entonces desborda. Cuándo no usarlo: no hagas tú un segundo menú «…».ToolbarOverflowMenuyadditionalOverflowItemsson el menú del sistema. EladditionalOverflowItemsde UIKit se puede usar desde iOS 16.Comportamiento de compresión (iOS 27.1). SwiftUI:
.automatic/.prefersToolbarItems/.prefersTabBar. UIKit:navigationItem.verticalBarCompressionBehavior, con.automatic/.prefersBarItems/.prefersTabBar. Cuándo usarlo: una página de navegación usa el valor por defecto (primero se recogen los ítems de herramientas y se conserva la pestaña). Una página de tarea pasa a recoger primero la barra de pestañas. Cuándo no usarlo: no escribas dos juegos de barras para simular la compresión.topBarPinnedTrailing(iOS 27.0). La propiedad correspondiente de UIKit,pinnedTrailingGroup, se puede usar desde iOS 16. El cierre usa.cancellationAction. En UIKit entra enleadingItemGroups. Cuándo usarlo: una acción principal que tiene que quedarse cerca de volver, como Done. Cuándo no usarlo: una acción secundaria no se fija aquí. Tiene que poder desbordar.toolbarVerticalEdge(iOS 27.1). SwiftUI:HorizontalEdge?. Es nil cuando este contexto no usa nunca la barra vertical. UIKit:traitCollection.verticalBarEdge, con valor leading, trailing o unspecified. Refleja el borde preferido, aunque la barra no sea visible en ese momento. Cuándo usarlo: cuando un contenido personalizado tenga que alinearse con esa barra. Cuándo no usarlo: no leas unspecified como «la barra acaba de ocultarse». Significa que este contexto no va a usar la barra vertical.backgroundExtensionEffect()(iOS 26). UIKit:UIBackgroundExtensionView. ColocacontentViewdentro del área segura y rellena el borde con la extensión. Cuándo usarlo: una imagen principal o un fondo que tiene que extenderse por debajo de la barra vertical. Cuándo no usarlo: un primer plano que se puede tocar no debe salir del área segura apoyándose en esto.defaultTabBarPlacement(iOS 27.0)..sidebarAdaptabley además.sidebar. UIKit:sidebar.preferredPlacement = .sidebar. Los valores de entornoisTabViewSidebarAvailableytabBarPlacementtambién son de iOS 27. Cuándo usarlo: cuando una sección de primer nivel, en un diseño ancho, conviene como navegación permanente. Cuándo no usarlo: no usesdefaultAdaptableTabBarPlacementpara el Duo. Ese modificador existe desde iOS 18 y actúa en el iPad.
Punto de control
Un botón que solo tiene título y no tiene icono, ¿entra en la barra vertical?
De las tres columnas de una vista dividida, ¿la barra de cuál se vuelve vertical?
Cuando a una página de navegación no le alcanza el espacio, ¿por defecto se recogen primero los ítems de herramientas o primero la barra de pestañas?
¿Por qué un
UIToolbardibujado por ti no se pone de lado?
En este capítulo todavía no. No consultes el pliegue para decidir si la barra va a la izquierda o a la derecha. El borde de la barra lo mete el sistema en el trait / environment. En qué lado se coloca un sheet es el capítulo siguiente.
Fuente: HIG · Vertical controls; Preparing · Optimize bars. Tech Talk 111462.
Capítulo 6 · Calcula los cuatro bordes por separado; el sheet declara a qué lado se pega
Al terminar podrás. Elegir la posición de un sheet, calcular la zona usable con un área segura asimétrica, y conservar una imagen a todo el ancho con esquinas concéntricas a la pantalla y con un punto de enfoque.
El ejemplo que se rompe
Usar el inset de la izquierda multiplicado por dos como área segura total. Split View o una barra vertical hacen que izquierda y derecha no sean iguales, y el contenido se descentra. Otro tipo de problema es fabricar tú una máscara a pantalla completa y usarla como sheet: el sheet del sistema, las alertas, los menús y los popover evitan el pliegue. Tu máscara no. La imagen principal se recorta con .fill: la cara que se ve en la exterior queda cortada en la interior.
La barra del sheet sigue a la colocación
En la pantalla exterior, un sheet con barra de herramientas usa por defecto la barra vertical. En la interior, el sheet va centrado por defecto y la barra es horizontal. El lado leading no tiene barra vertical. El lado trailing sí. Solo el sheet hace caso de la colocación. No hagas por eso otro juego de transiciones personalizadas. Un sheet con un solo botón de cerrar es el caso en el que el capítulo anterior permite apagar la barra vertical. Si has asignado sourceView, UIKit ignora preferredPlacement.
.sheet(item: $selection) { item in
ItemDetail(item: item)
.presentationPlacement(.trailing)
}
let sheet = detail.sheetPresentationController
sheet?.preferredPlacement = .trailing
// .automatic / .leading / .center / .trailing
// sourceView 非 nil 时,这个属性被忽略
Los cuatro bordes del área segura son independientes
content
.safeAreaPadding(.horizontal)
background
.ignoresSafeArea()
let usable = view.bounds.inset(by: view.safeAreaInsets)
// 分别使用 left、right、top、bottom
// 折痕不是 safe area inset
La decoración puede salir del área segura. El contenido que se puede tocar y el texto que hay que leer se quedan dentro. La cámara, la barra vertical y Split View hacen que izquierda y derecha, o arriba y abajo, no sean iguales. Una forma personalizada pegada a la esquina redondeada de la pantalla usa un rectángulo concéntrico. No escribas el radio a mano.
RoundedRectangle(cornerRadius: 12) // 不要写死去贴屏幕角
ConcentricRectangle()
view.cornerConfiguration = .corners(radius: .containerConcentric())
Una imagen a todo el ancho tiene que conservar el sujeto
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 el sujeto no está en el centro geométrico, dale al recorte un punto de enfoque, en lugar de limitarte a alternar entre fill y fit. Un juego, en cambio, llena y cambia la proporción: está en el capítulo 1.
API
presentationPlacement(iOS 27.0)..automatic/.center/.leading/.trailing. UIKit:UISheetPresentationController.preferredPlacement. Cuándo usarlo: cada sheet tiene que verse cerrado, abierto y a media apertura antes de decidir si se pega a un borde o se centra. Cuándo no usarlo: un popover y una alerta no llevan este modificador. Si has asignadosourceView, UIKit lo ignora.Inset asimétrico.
bounds.inset(by: safeAreaInsets). Cuándo usarlo: cualquier sitio donde calcules a mano el ancho o el alto usable. Cuándo no usarlo: no espejes el inset de un lado hacia el lado contrario. El pliegue tampoco se mete dentro del área segura.ConcentricRectangle(iOS 26). UIKit: radio concéntrico del contenedor enUICornerConfiguration. ExisteUIView.cornerConfiguration. Cuándo usarlo: un panel personalizado que tiene que seguir la esquina redondeada de la pantalla o del contenedor actuales. Cuándo no usarlo: la esquina pequeña de una tarjeta puede seguir usando un radio fijo. No lo uses para evitar el pliegue.fill / fit. Elige según la proporción actual o el size class, o define un punto de enfoque. Cuándo usarlo: imagen principal y portada de vídeo, de la exterior estrecha a la interior ancha, conservando el sujeto. Cuándo no usarlo: no elijas el contentMode una sola vez al arrancar.
Punto de control
En la pantalla interior, la barra de herramientas de un sheet trailing, ¿es horizontal o vertical?
¿Por qué no se puede obtener el ancho del contenido con
left * 2?El sheet del sistema evita el pliegue. ¿Lo hace una máscara a pantalla completa dibujada por ti?
En este capítulo todavía no. Cuando un lienzo personalizado, un código QR o un control de arrastre tengan que evitar el pliegue, el capítulo siguiente es el que consulta la región reservada. Ahora no adivines las coordenadas del pliegue.
Capítulo 7 · El pliegue y la cámara son dos regiones reservadas, no el área segura
Al terminar podrás. En un diseño personalizado, encontrar el pliegue activo y la cámara activa, y mover solo el trozo que queda tapado, en lugar de rehacer la página entera.
El ejemplo que se rompe
En cuanto se pliega a medias, se cambia a otro árbol de interfaz. O se escribe a mano «el pliegue está en el 50 % del ancho». Después de Split View y de una rotación, ese porcentaje ya no es el centro de tu vista. El otro extremo es apartar del pliegue cada párrafo de un artículo: el contenido que se desplaza ya puede rodar por encima de esa costura.
Dos regiones; la mayoría de los componentes del sistema ya las evitan
Una región reservada es el rectángulo que otra cosa ocupa dentro de tu sistema de coordenadas. frame incluye margins. Cada una tiene clase, si está activa y un identificador. La consulta devuelve las regiones que intersecan la vista actual. Si tu vista solo ocupa la mitad de la pantalla interior, no supongas que puedes ver el pliegue de toda la pantalla.
division, división. A media apertura, la bisagra parte una zona grande en dos lados. Cuando queda completamente plana, esa región no está activa y su ancho es 0.
occlusion, oclusión. La cámara frontal de la pantalla exterior está siempre, y además se ensancha hasta el Dynamic Island. La cámara frontal de la interior suele estar escondida bajo la pantalla. Solo mientras se usa ocupa un trozo y hace que la interfaz se aparte.
Las alertas, los menús, los sheet y las columnas divididas se adaptan solos. Un lienzo personalizado, un código QR, una cara, una frase corta que tiene que leerse seguida y un objeto que se está arrastrando sí necesitan que leas esos rectángulos. Mueve solo el elemento que queda tapado, y llévate con él los controles que le pertenecen. No lo mandes al otro extremo de la pantalla. Un artículo que se desplaza y una lista no se apartan enteros.
Filtra primero lo que está en vigor y después coloca el contenido personalizado
reservedRegions de SwiftUI también acepta layoutDirectionBehavior:. El valor por defecto es .mirrors. Solo si tú mismo has hecho el espejo pasas .fixed. Si no, se espeja dos veces. La posición física de la cámara no se voltea con el idioma. SwiftUI, por defecto, te espeja primero la geometría. El proxy de onGeometryChange también puede consultarlas.
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)
}
Una decisión de estructura como «número par de columnas» tiene que poder ver la división también cuando el dispositivo está plano y la región de división todavía no está activa. Solo entonces pasas .includeInactive, y sigues usando isActive para decidir si hay que mover los controles ya.
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, sobreGeometryProxy. UIKit:view.reservedRegions(kind:options:), que devuelve[UIView.ReservedRegion]. Cuándo usarlo: contenido personalizado que el contenedor del sistema no va a mover por ti. Cuándo no usarlo: una lista ordinaria, una columna dividida, un sheet, una alerta. No trates el resultado como un inset del área segura.ReservedRegion(iOS 27.1).id,kind,frame,margins,isActive. Las clases son.divisiony.occlusion. Cuándo usarlo: coloca conframe.frameya incluye el margen. Cuándo no usarlo: una division que no está activa no sirve para empujar un botón. En plano no debe cambiar el sitio del toque..includeInactive(iOS 27.1).ReservedRegion.QueryOptions. Cuándo usarlo: una decisión que no depende de si ahora mismo está a media apertura, del tipo «si existe un pliegue, la cuadrícula usa un número par de columnas». Cuándo no usarlo: no muevas el botón por adelantado solo porque la consulta ha encontrado una región inactiva.layoutDirectionBehavior..mirrorso.fixed. Cuándo usarlo: deja el valor por defecto, para que las subvistas de diseño automático y la región usen la misma dirección. Cuándo no usarlo: pasa.fixedsolo cuando tú espejas a mano.
Punto de control
Después de quedar completamente plano, ¿division sigue activa? ¿Cuánto mide el ancho?
Un artículo que se puede desplazar, ¿hay que apartar cada párrafo del pliegue?
¿La cámara de la pantalla interior ocupa occlusion en condiciones normales?
En este capítulo todavía no. Un contenido en pareja, como un reproductor más una lista de reproducción, se lo entregas a Arrangement en el capítulo siguiente. No uses el ángulo de la bisagra para decidir si hay que evitar el pliegue.
Fuente: ReservedRegion; Preparing · Adapt to reserved regions. Tech Talk 111463.
Capítulo 8 · Cuando dos contenidos forman una experiencia, deja que el contenedor los recoloque
Al terminar podrás. Juzgar si un par de vistas debe usar split u overlay, y dejar la navegación fuera de ese contenedor.
El ejemplo que se rompe
Sustituir TabView o NavigationSplitView por Arrangement. O meter Arrangement dentro de un List, de un ScrollView o de una columna de una vista dividida. El resultado es que la mitad del contenido no se puede desplazar hasta verse y no se puede tocar.
Si ya se parece a una de las dos pilas, mapea hacia allá
Arrangement solo tiene vista principal y vista secundaria. Mira el espacio, la relación de aspecto, el size class y la región de división. No mira si eres un Duo. El contenedor de navegación va por fuera. El propio ejemplo de Apple es un Arrangement dentro de una pila de navegación. No metas otra columna dividida dentro de Arrangement.
split. Corresponde al
HStack/VStackde antes, o a una relación principal y secundaria en la que ninguno de los dos debe tapar al otro. Si es más ancho que alto, se parte a izquierda y derecha. Si es más alto que ancho, se apila arriba y abajo. Cuando aparece la región de división, ajusta los dos lados para evitar el pliegue. Después de limitarlo a un eje, si ese eje no se puede usar, solo queda la vista principal..split.axes(.horizontal)muestra solo el contenido principal cuando no cabe en el eje horizontal.overlay. Corresponde al
ZStackde antes. Cuando no hay una región de división activa, la vista principal cubre a la secundaria (cerrado o totalmente abierto). A media apertura, la vista principal va al lado posterior o al lado inferior del pliegue, y la secundaria al lado anterior o al lado superior.
Reproductor y lista: 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)
Solo se permite izquierda y derecha; en vertical solo queda el reproductor:
.arrangementViewStyle(.split.axes(.horizontal))
arrangement.updateArrangement(.split.axes(.horizontal))
Los controles cubren la imagen: overlay
ArrangementView {
TransportControls()
} secondary: {
Stage()
}
.arrangementViewStyle(.overlay)
arrangement.setViewController(controls, for: .primary)
arrangement.setViewController(stage, for: .secondary)
arrangement.updateArrangement(.overlay)
La proporción y el nivel todavía tienen estos ajustes, todos de iOS 27.1: splitArrangementLayoutRatio, splitArrangementLayoutSize, splitArrangementFixedLayoutSize, overlayArrangementEdge. Los valores de entorno splitArrangementAxis y overlayArrangementZIndex sirven para que la subvista sepa en qué disposición está ahora. UIKit lee zIndex, splitAxis e isHidden con state(for:). El rango de tamaño de las vistas principal y secundaria está en UISplitArrangement.ViewProperties. El borde de oclusión está en UIOverlayArrangement.ViewProperties.edge.
API
ArrangementView(iOS 27.1).initrecibe primary y secondary. UIKit:UIArrangementViewController. La posición desetViewController(_:for:animated:)es.primary/.secondary. Cuándo usarlo: reproductor y cola, editor e inspector, lienzo y propiedades. Los dos bloques son la misma experiencia. Cuándo no usarlo: la navegación de primer nivel de la app. No lo metas en una columna dividida, en una lista ni en una vista de desplazamiento..split(iOS 27.1).arrangementViewStyle(.split),.axes(.horizontal)o.vertical. UIKit:updateArrangement(.split.axes(...)). Cuándo usarlo: los dos lados necesitan su propio espacio y ninguno cubre al otro. Cuándo no usarlo: unos controles que de por sí deben flotar sobre el contenido. Eso es overlay..overlay(iOS 27.1). Sin región de división, la vista principal queda encima. A media apertura, la vista principal va al lado posterior o inferior, y la secundaria al lado anterior o superior. Cuándo usarlo: primer plano / fondo, cuando se permite que se tapen en condiciones normales. Cuándo no usarlo: el correo de dos columnas. Eso es la columna dividida del capítulo 4, o el split de aquí.updateArrangement(_:animated:)(iOS 27.1). Cambia split / overlay, o solo cambia el eje. Cuándo usarlo: el producto quiere fijar un eje de forma explícita, o alternar entre los dos estilos. Cuándo no usarlo: no recalcules tú el frame en cada rotación y vuelvas a llamarlo. El estilo por defecto ya sigue al contenedor.
Punto de control
Antes era un
ZStacky, a media apertura, tiene que repartirse a los dos lados del pliegue. ¿split u overlay?¿Arrangement debe envolver a la navegación, o la navegación debe envolver a Arrangement?
¿Qué pasa si lo metes en un
ScrollView?
En este capítulo todavía no. No leas el ángulo de la bisagra para decidir entre split y overlay. El ángulo es una entrada de interacción del capítulo 10.
Fuente: los ejemplos de SwiftUI / UIKit dentro de Preparing; HIG · Arrangement views. Las firmas se contrastan con ArrangementView y UIArrangementViewController del SDK de iOS 27.1.
Capítulo 9 · Abrir y cerrar es un cambio de diseño, no reiniciar la app
Al terminar podrás. Dejar la navegación, el desplazamiento, la selección, la edición y la reproducción por encima del diseño, y conocer el límite de Split View y de una ventana nueva en el Duo.
El ejemplo que se rompe
Cerrado, la lista tiene seleccionada la tercera carta. Al abrir, otro árbol de vistas empieza por la primera, y el borrador también está vacío. Otra forma de romperlo: probar solo la exterior a pantalla completa y la interior a pantalla completa. A mitad de un Split View, las barras se solapan y el texto queda cortado. Y todavía se supone que «si está abierto, seguro que es muy ancho».
Un ancho continuo, más dos límites del hardware
En la pantalla interior, todas las apps participan en Split View. Un vídeo también puede superponerse a una app. La barra vertical de cada app se pega a su propio borde exterior, así que tu barra puede estar a la izquierda o a la derecha. Hay que probar los dos lados.
Un diseño muy ancho tiene que poder retroceder a un ancho intermedio y, desde ahí, volver a compact. El estado —ruta de navegación, desplazamiento, selección, editor, progreso de reproducción, formulario sin guardar— vive en la capa que está por encima de la que decide si hay una columna o dos.
El Duo es el primer iPhone que admite varias escenas. Una app que en el iPad abre una ventana nueva también lo hace aquí. Una ventana nueva no se puede crear en la pantalla exterior. Si la petición falla, quédate en la ventana actual. El cierre de configuración de UIWindowScene.ActivationAction tiene que devolver un ActivationConfiguration. No modifiques un objeto de petición. Cuando la plataforma no admite varias ventanas, se sigue el alternate que tú proporcionas. No supongas que un toque en la pantalla exterior va a abrir por fuerza una segunda. UIRequiresFullScreen sigue en vigor, pero al abrir y cerrar el dispositivo la interfaz cambia de tamaño de todas formas.
La selección vive fuera del diseño
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 posición de desplazamiento usa el propio guardado de la vista de desplazamiento del sistema, o un identificador que vive en el model. No hagas scrollTo de vuelta arriba al cerrar. La reproducción usa el mismo objeto reproductor. No lo replace cuando cambia el tamaño.
Hechos que hay que comprobar
Elevar el estado. La selección, la ruta, el borrador y el reproductor viven fuera del contenedor adaptable. Cuándo usarlo: cualquier página que vaya a alternar entre compact y regular. Cuándo no usarlo: no hagas un objeto de estado para la exterior y otro para la interior.
Split View. La barra está en el borde exterior de esa app. La mitad izquierda tiene la barra a la izquierda. La mitad derecha tiene la barra a la derecha. Cuándo usarlo: en Device Hub, colócate a la izquierda y a la derecha. Cuándo no usarlo: no supongas que la barra vertical está siempre en trailing.
UIWindowScene.ActivationAction(iOS 15). El cierre devuelveUIWindowScene.ActivationConfiguration(userActivity:). Cuando la plataforma no admite varias ventanas, usaalternate. Cuándo usarlo: ya admites varias ventanas en el iPad y quieres que la pantalla interior del Duo también pueda. Cuándo no usarlo: no fuerces una escena nueva en la pantalla exterior sin tratar el fallo. Una ventana nueva no se puede crear en la exterior.UIRequiresFullScreen. El requisito de pantalla completa se sigue respetando. Cuándo usarlo: si de por sí necesitas la pantalla en exclusiva, conserva el ajuste original. Cuándo no usarlo: no creas que puede impedir el cambio de tamaño que trae abrir y cerrar. El diseño tiene que seguir cambiando.
Punto de control
Después de abrir la pantalla interior, ¿desde qué estado debe recuperarse el borrador?
Cuando la app está en el lado derecho de Split View, ¿en qué lado es más probable la barra vertical?
¿Se puede crear una ventana nueva en la pantalla exterior?
En este capítulo todavía no. No reinicies la navegación con el ángulo de la bisagra. Abrir y cerrar ya se expresan mediante el tamaño y la región reservada. El ángulo se reserva para una interacción física de verdad.
Fuente: lo que el HIG dice sobre la continuidad entre pantallas y sobre la barra exterior en Split View; lo que Preparing exige al probar varios tamaños. Tech Talk 111464.
Capítulo 10 · El ángulo mueve el efecto, no las barras ni el número de columnas
Al terminar podrás. Cuando hay bisagra, convertir el ángulo en tono, en encuadre o en una regla de juego, y volver a un valor seguro cuando no hay bisagra o cuando la interacción sale de la jerarquía.
El ejemplo que se rompe
if hinge.angle > someRadians { showSidebar = true }
En plano, en Split View y en un iPhone normal, el ángulo no llega como tú esperas. Si la barra lateral debe aparecer o no, los capítulos 3 y 4 ya lo respondieron con el espacio.
El diseño mira el espacio; la interacción es la que mira la bisagra
Sirve para el ángulo: tratarlo como entrada física, un mecanismo de juego, un instrumento, la postura de la cámara, una interacción de sobremesa, o un efecto atado de forma explícita a abrir y cerrar. No sirve: la barra lateral, el número de columnas, la cantidad de la cuadrícula, si la navegación está plegada.
En SwiftUI, que DeviceHingeContext.hinge sea nil significa que este dispositivo no tiene bisagra. No significa «está cerrado». Cerrado es .closed. angle es un Angle. status es .closed, .partiallyOpen, .fullyOpen.
El nil de UIKit significa otra cosa: cuando la interacción ha salido de la jerarquía que puede proporcionar actualizaciones de la bisagra, update.hinge es nil. El ángulo va en radianes. UIHinge.Status añade un .unknown. La frecuencia y la precisión de la actualización las decide el sistema. No dependas de una tasa de fotogramas concreta. Si solo te importa abierto, a medias o del todo abierto, lee status.
A media apertura, el ángulo se convierte en tono; el resto del tiempo vuelve a cero
.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:_:) vale true por defecto. UIKit usa interaction.isEnabled: mientras está apagado no hay callback, y las actualizaciones perdidas tampoco se encolan. Al volver a encenderlo, si la jerarquía actual todavía tiene bisagra, entrega enseguida otra vez el estado actual. Este cierre escapa. Usa [weak self] para evitar una referencia circular.
API
onHingeChange(iOS 27.1).(isEnabled:_:). Los parámetros son elDeviceHingeContextanterior y el nuevo. Cuándo usarlo: un efecto que tiene que cambiar de forma continua con la bisagra. Cuándo no usarlo: evitar el pliegue, las columnas divididas, la cuadrícula. Eso usa la región reservada y Arrangement.DeviceHinge(iOS 27.1).statusmásangle: Angle.hingeen el contexto es opcional. Cuándo usarlo: si es nil, lleva el efecto a cero. No falles. Cuándo no usarlo: no leas nil como «está cerrado». Cerrado es.closed.UIHingeInteraction(iOS 27.1).init { interaction, update in }.update.hingees unUIHingeopcional.angleva en radianes. El estado incluye.unknown. Cuándo usarlo: añádelo a la vista que tiene que observar la bisagra. El ejemplo oficial haceaddInteractionenviewDidLoad. Cuándo no usarlo: aquí, nil significa que la interacción ha salido de la jerarquía que proporciona actualizaciones. No tiene por qué significar «este teléfono no tiene bisagra».Frecuencia de actualización. La cabecera lo dice con claridad: la tasa y la granularidad cambian con el estado del sistema. Cuándo usarlo: trata el ángulo como una entrada continua y tolera fotogramas perdidos. Cuándo no usarlo: no uses «tiene que ser N veces por segundo» para calibrar o para animar el diseño.
Punto de control
Si la barra lateral debe aparecer o no, ¿se puede mirar
hinge.angle?En SwiftUI, ¿qué significa
hinge == nil? ¿Y el nil de UIKit?Si solo quieres saber si está a media apertura, ¿lees el ángulo o
status?
En este capítulo todavía no. No trates la pantalla exterior como una segunda ventana que tú colocas. Eso es el capítulo siguiente, y solo vale durante la captura.
Capítulo 11 · El contenido extra de la pantalla exterior solo lo muestra el sistema durante la captura
Al terminar podrás. Explicar cómo la escena principal se mueve entre las dos pantallas, y cuándo sí se puede poner un contenido complementario en la exterior.
El ejemplo que se rompe
Al abrir, partir la interfaz en dos UIWindow: uno dibuja la interior y otro dibuja la exterior. El sistema no va a dejar tu segundo lienzo en la cara que queda cerrada. El simulador tampoco tiene cámara. Ese código no se puede comprobar en Device Hub.
La experiencia principal sigue a la única escena principal
Al abrir y al cerrar, la escena principal cambia sola a la otra cara. Todos los botones importantes se quedan en la interfaz principal. El contenido complementario de la pantalla exterior puede ser interactivo, pero tiene que ser poco.
La única forma admitida es el accesorio de captura de la cámara: el dispositivo está abierto, la app está en primer plano y la pantalla interior está a pantalla completa, y hay una sesión de captura en curso. Solo entonces el sistema puede mostrarlo en la exterior. El sistema decide cuándo y dónde, y también puede retirarlo en cualquier momento. Para apagarlo usa isEnabled. No desregistras para encender y apagar. El estado va por el mismo model.
El simulador no tiene cámara. Esta sección necesita un dispositivo real. La dirección a la que mira la cámara puede invertirse después de abrir, cerrar y girar. Al elegir la cámara, hazlo según «hacia qué cara mira». Véase Choosing a camera by the direction it faces.
ExternalNonInteractiveAccessory es una pantalla externa y AirPlay. No es la pantalla exterior del Duo.
Teleprompter: la interfaz principal sigue pudiendo capturar
CameraCaptureAccessory indica en el SDK que no se puede usar en macOS, tvOS, watchOS, visionOS ni Mac Catalyst. sceneAccessory y registerSceneAccessory(_:) existen desde iOS 27.0. cameraCapture(sceneConfiguration:) y el rol de sesión .windowCameraCaptureAccessory son de iOS 27.1. Cuando necesites entregar el contexto al delegado de la escena, usa el método de fábrica que lleva userInfo y recupéralo en 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:), colgado dentro desceneAccessory.onAvailabilityChangeinforma de si el sistema sigue dispuesto a mostrarlo. Cuándo usarlo: un teleprompter, una cuenta atrás, dejar que la persona a la que se graba vea la imagen. El registro va en la misma vista que la interfaz de captura. Cuándo no usarlo: el obturador, el enfoque y los ajustes del camino principal. La pantalla exterior puede desaparecer en cualquier momento.registerSceneAccessory(iOS 27.0). UIKit:UISceneAccessory.cameraCapture(sceneConfiguration:), con el rol.windowCameraCaptureAccessory. El registration que devuelve sirve paraunregisterSceneAccessory. Cuándo usarlo: una escena complementaria mientras existe la sesión de captura. Cuándo no usarlo: no hagas unregister para ocultarlo un momento. Usa el estado de activación.ExternalNonInteractiveAccessory. La pantalla a la que te conectas, o AirPlay. Cuándo usarlo: si de por sí quieres admitir una pantalla grande externa y el contenido no necesita interacción. Cuándo no usarlo: la pantalla exterior del Duo cuando está cerrado.Simulador. Device Hub puede abrir, cerrar, girar y plegar. No tiene cámara. La mayoría de las extensiones de app tampoco pueden ejecutarse en el simulador del Duo. Cuándo usarlo: diseño, barras, sheet, Split View. Cuándo no usarlo: no declares en el simulador que el accesorio de captura ya está comprobado.
Punto de control
Cuando no se está capturando, ¿se puede fijar un panel en la pantalla exterior?
Cuando el teleprompter no está disponible, ¿dónde debe estar el obturador?
¿Por qué este tramo no puede cerrarse solo en el simulador?
Hasta aquí. Si el producto no tiene cámara, este capítulo puede no escribir código. Los capítulos 0 a 9 ya cubren el camino principal, del diseño al diseño adaptable.
Fuente: Preparing · Improve your app’s camera handling. Tech Talk 111465.
Apéndice · Contraste, señales de alarma y respuestas
Los doce capítulos anteriores construyen el criterio en orden. Aquí se juntan las tres capas de API, las formas de escribir que no deben aparecer y las respuestas de los puntos de control, para volver a contrastarlas al terminar un capítulo.
Tres capas; no te saltes ninguna
| Capa | Cuándo se escribe | SwiftUI | UIKit |
|---|---|---|---|
| 1 · Cualquier tamaño | Capítulos 3 a 6. Todas las ventanas de Apple se benefician | NavigationSplitView, TabView, GridItem(.adaptive), ViewThatFits, AnyLayout, size class, onGeometryChange, .toolbar del sistema |
UISplitViewController, UINavigationController, UITabBarController, ancho del contenedor, trait, layoutSubviews, viewWillTransition |
| 2 · Conocer el pliegue y la barra vertical | En los capítulos 5 a 8, cuando el contenedor del sistema no basta | reservedRegions, ArrangementView, toolbarVerticalBehavior, axisBehavior, visibilityPriority, presentationPlacement, toolbarVerticalEdge |
reservedRegions(kind:options:), UIArrangementViewController, preferredVerticalBarBehavior, axisBehavior, verticalBarEdge, preferredPlacement |
| 3 · Solo el hardware en sí | Capítulos 10 a 11, cuando el producto de verdad lo necesita | onHingeChange, CameraCaptureAccessory, sceneAccessory |
UIHingeInteraction, UISceneAccessory.cameraCapture, rol .windowCameraCaptureAccessory |
Las reglas de los ítems de la barra vertical están en el capítulo 5 porque el SDK 27.1 pone en vertical las barras del sistema, no porque tengas que consultar el pliegue. La consulta del pliegue empieza en el capítulo 7.
Disponibilidad, para no mezclarlas
| Sistema | Lo que aparece por primera vez en este escalón |
|---|---|
| iOS 26 | ConcentricRectangle, backgroundExtensionEffect(), UIBackgroundExtensionView, UICornerConfiguration |
| iOS 27.0 | visibilityPriority, ToolbarOverflowMenu, topBarPinnedTrailing, presentationPlacement, defaultTabBarPlacement, isTabViewSidebarAvailable, sceneAccessory / registerSceneAccessory |
| iOS 27.1 | Comportamiento y borde de la barra vertical, axisBehavior, comportamiento de compresión, región reservada, Arrangement, bisagra, CameraCaptureAccessory |
leadingItemGroups, pinnedTrailingGroup y additionalOverflowItems de UIKit se pueden usar desde iOS 16. En el Duo se encargan del orden y del desbordamiento dentro de la barra vertical, pero las propiedades en sí no aparecen por primera vez en 27.1.
La prioridad de visibilidad de SwiftUI es .automatic / .low / .high. El valor correspondiente de UIKit es .standard / .low / .high. El comportamiento de compresión también difiere en un nombre: .prefersToolbarItems en SwiftUI, .prefersBarItems en UIKit.
Al abrir un archivo, busca primero esto
Si se usan para un diseño ordinario, hay que cambiarlos:
UIScreen.main.bounds,UIScreen.main.scale, unUIScreenguardadoUIWindow(frame: UIScreen.main.bounds)userInterfaceIdiom == .pado.phoneUIDevice.current.orientation,statusBarOrientation,interfaceOrientation, incluidoeffectiveGeometry.interfaceOrientationentrar en una página «solo iPad» porque
horizontalSizeClass == .regularbounds.height == 844, o una tabla de alturas de modelos conocidossafeAreaInsets.left * 2if isDuo,if isFolded, usar el ángulo de la bisagra para decidir la barra lateral o el número de columnasun frame grande escrito a mano, un ancho de barra lateral escrito a mano, un número de columnas de cuadrícula escrito a mano
una barra superior dibujada por ti, o un
UIToolbar/UINavigationBar/UITabBarcreado aparteun botón de barra de herramientas que solo tiene título y no tiene icono; un menú «…» dibujado por ti
contenido importante centrado y pisando el pliegue; un estado guardado para compact y otro para expandido
una imagen principal en
.fillsin punto de enfoque, y sin elegir entre fill y fit según la proporciónusar
toolbarVerticalBehavior(.disabled)fuera de las excepciones que el texto documenta, o cambiarlo de un lado a otro según el estado de la página
Posturas que, como mínimo, hay que haber visto en Device Hub
| Postura | Qué se espera |
|---|---|
| Cerrado, vertical | Pantalla exterior, ancho compact + alto regular, barra vertical |
| Cerrado, horizontal | Los dos compact, barra vertical |
| Abierto, vertical | Interior regular × regular, barras horizontales. Es la única postura habitual que conserva las barras de arriba y de abajo |
| Abierto, horizontal | regular × regular, barra vertical |
| Media apertura | La misma jerarquía. El contenido preciso no se monta en el pliegue. El contenido que se desplaza no se desvía de página entera |
| Split View en la interior, tú a la izquierda y tú a la derecha | La barra pegada a tu propio borde exterior. El estado sigue ahí |
Cada sheet y cada popover tienen que abrirse una vez en estas posturas. Las capturas de la tienda se rigen por las Screenshot specifications de App Store Connect. Una ordenación habitual es exterior 1398×2034 e interior 2007×2853; en horizontal se intercambian los dos lados. Ese grupo de números no se escribe dentro del diseño. Si un material dice «a partir de abril de 2027, el envío necesita capturas del Duo», la fecha exacta es la del anuncio de Apple en ese momento.
Respuestas de los puntos de control
Capítulo 0. Las barras del sistema de la interior en vertical están arriba y abajo. Las seis posturas siguen siendo una sola interfaz. Después de abrirlo sigue siendo una app de iPhone. Lo que ocurre es que la pantalla interior pasa a ser regular × regular.
Capítulo 1. Un formulario de ajustes de 900 pt de ancho estira la frase hasta llenarla, y el vacío separa la etiqueta del interruptor. Hay que limitar el ancho y dejar el espacio que sobra para la capa siguiente de la jerarquía. El degradado puede quedarse en el centro. El botón de pagar no. Seleccionar todo de la lista sigue a la lista, no a la barra del sistema del extremo derecho.
Capítulo 2. El destino de implementación puede seguir siendo iOS 18. Después de compilar con el SDK 27.1, aparecen las barras verticales. Cuando alrededor queda vacío, hay que cambiar el SDK de compilación. No detectes el modelo para estirar el frame. Los símbolos nuevos no existen en el sistema antiguo, así que tienen que entrar en #available.
Capítulo 3. El ancho regular se cumple en la pantalla interior del Duo, así que ya no equivale a iPad. La interior no hace caso de la orientación declarada. Si es ancho o alto se compara el ancho y el alto del contenedor. Los inset de izquierda y derecha pueden no ser iguales. left * 2 calcula mal el otro lado.
Capítulo 4. La selección vive en el mismo estado, fuera de la columna dividida. El número de columnas sale de dividir el ancho del contenedor entre el ancho mínimo de ítem. Aquí cae en 2. ViewThatFits responde qué disposición cabe.
Capítulo 5. Un botón que solo tiene título no entra en la barra vertical. En una columna dividida, solo la barra de la columna de detalle es vertical. Una página de navegación, por defecto, mete primero los ítems de herramientas en el desbordamiento y conserva la barra de pestañas. Un UIToolbar creado aparte no es una barra que proporcione el contenedor de navegación. El sistema no lo pone en vertical.
Capítulo 6. En la pantalla interior, la barra de herramientas de un sheet trailing es vertical. El área segura de izquierda y derecha es asimétrica y no se puede espejar. Una máscara a pantalla completa dibujada por ti no evita el pliegue como lo hace un sheet del sistema.
Capítulo 7. Después de quedar completamente plano, division no está activa y el ancho es 0. Un artículo que se puede desplazar no se aparta párrafo a párrafo. La cámara de la interior no ocupa occlusion en condiciones normales. Solo la ocupa mientras se usa.
Capítulo 8. Un ZStack se mapea a overlay. La navegación envuelve a Arrangement. No al revés. Meterlo en un ScrollView hace que uno de los dos bloques quede fuera de alcance.
Capítulo 9. El borrador se recupera desde el mismo estado, no desde otro árbol de vistas. La barra vertical del lado derecho de Split View está a la derecha, es decir, en el borde exterior de esa app. Una ventana nueva no se puede crear en la pantalla exterior.
Capítulo 10. La barra lateral no mira el ángulo de la bisagra. El nil de SwiftUI significa que este dispositivo no tiene bisagra. El nil de UIKit significa que la interacción ha salido de la jerarquía que puede proporcionar actualizaciones. Si solo quieres saber si está a media apertura, lee status.
Capítulo 11. Cuando no se está capturando, no se puede fijar un panel en la pantalla exterior. El obturador se queda en la interfaz principal. El simulador no tiene cámara. El accesorio de captura necesita un dispositivo real.
Entradas oficiales
ReservedRegion, y
UIView.ReservedRegionde UIKitArrangementView, y
UIArrangementViewControllerTech Talks: 111466 Diseño, 111461 Preparación, 111462 Barras verticales, 111463 Posturas y Arrangement, 111464 Varias pantallas y escenas, 111465 Cámara
La página Preparing también lista WWDC 2026 · 278 y WWDC 2025 · 282
Buenas prácticas · Los accesos de abajo no se mueven; el ancho se lo queda el contenido
Al terminar de leerlo podrás. Contrastar esta app con las reglas de antes, y separar lo que ya está hecho a la manera de Apple, lo que es una elección de producto y lo que todavía no se puede dar por comprobado.
Esta app no tiene una versión Duo. El destino de implementación sigue siendo iOS 18. Solo al compilar con el SDK 27.1 se llena la pantalla y las barras del sistema pueden ponerse en vertical. Abrir y cerrar no cambia la ruta ni el modelo de página. El ancho regular que sobra se usa en las columnas de izquierda y derecha dentro de «Hoy» y de «Respiración», en lugar de convertir los tres accesos de primer nivel en una barra lateral. El ángulo de la bisagra, la pantalla exterior de la cámara y las ventanas múltiples no tienen motivo de producto, así que no se escriben.
Contraste
| Criterio del tutorial | Cómo lo hace esta app |
|---|---|
| Capítulos 0 a 2: una sola interfaz; el SDK es el interruptor | No se ramifica por nombre de dispositivo. Las API nuevas van envueltas en #available. En el sistema antiguo queda un HStack / VStack de la misma relación |
| Capítulos 3 a 4: mostrar la capa siguiente según el ancho del contenedor | Un shell compact sigue siendo una sola columna por muy ancho que sea. En regular, 800 pt sigue siendo una sola columna. A partir de 801 pt van en paralelo a izquierda y derecha. Solo entra en un escalón más holgado cuando caben la columna principal máxima de 480 pt más el detalle legible de 720 pt (1201 pt en total). 1024 pt sigue siendo en paralelo. No es estirar la frase hasta llenarla |
| Capítulo 5: barras del sistema, pero la barra lateral no es obligatoria | .sidebarAdaptable más defaultTabBarPlacement(.tabBar). Las tres pestañas se quedan abajo |
| Capítulo 6: el sheet declara la posición; una máscara dibujada por ti no evita el pliegue | El sheet del sistema, en regular, va a trailing o a center según la intención. La capa de pago en compact sigue siendo una máscara en la raíz y no evita el pliegue sola |
| Capítulos 7 a 8: mover solo los dos bloques que van en pareja | La respiración, la franja horizontal de pago y las columnas principal y secundaria de igual ancho usan el split de ArrangementView. La página de inicio que se desplaza y el detalle no se desvían por el pliegue |
| Capítulos 9 a 11: el estado es continuo; la API de hardware es opcional | La selección y la ruta viven fuera del diseño. No se hace onHingeChange, ni accesorio de captura, ni ventanas múltiples |
La navegación raíz conserva tres entradas
La pantalla interior puede volverse barra lateral. Este producto elige no volverse. «Hoy», tendencias y respiración siguen siendo pestañas de abajo. Ajustes se avanza desde «Hoy». La ruta de iPad usa defaultAdaptableTabBarPlacement(.tabBar). En iOS 27 se vuelve a dejar explícito defaultTabBarPlacement(.tabBar). Después de abrir la pantalla interior, el espacio que sobra aparece dentro de la página: Hoy, tarjeta a la izquierda y detalle a la derecha; respiración, modo a la izquierda y ejercicio a la derecha.
Ir en paralelo tiene que cumplir dos cosas a la vez: el shell de esta página es regular, y el size class horizontal actual también es regular. Así, el árbol estrecho no se lleva la selección compartida. Si el shell es compact, aunque el contenedor sea muy ancho, el escalón sigue siendo una sola columna.
TabView(selection: adaptiveSectionSelection) {
// 三个 Tab
}
.tabViewStyle(.sidebarAdaptable)
.defaultAdaptableTabBarPlacement(.tabBar)
// iOS 27:content.defaultTabBarPlacement(.tabBar)
adaptiveShellRole == .regular && horizontalSizeClass == .regular
Arrangement solo describe una relación que ya existía
La navegación se queda fuera. El diseño de la versión baja y el Arrangement de 27.1 son la misma relación, no otra página.
| Escena | Relación de antes | 27.1 |
|---|---|---|
| Ejercicio de respiración | La imagen principal arriba, los controles abajo | .split. Alto, arriba y abajo. Ancho, izquierda y derecha, y evita el pliegue. No se bloquea el eje y no se mira el ángulo de la bisagra |
| Página de pago en horizontal | La imagen principal y la compra van en paralelo | .split. En el sistema antiguo sigue siendo un HStack |
| Columnas principal y secundaria de igual ancho | Las dos columnas necesitan su propio espacio | .split.axes(.horizontal), proporción 0.5. El fondo puede cruzar el pliegue. El contenido que se puede tocar está dentro de Arrangement |
Lo que el sistema hará, y lo que todavía no cuenta como terminado
El área segura se lee por los cuatro bordes. Una lista que se desplaza y las imágenes dentro del detalle no se desplazan por una media apertura. Sheet del sistema: en compact va por .automatic. En regular se elige trailing o center según la intención.
Lo que de forma explícita no se hace: no se usa el ángulo de la bisagra para colocar la respiración ni la barra lateral. No hay cámara, así que no se registra un accesorio de captura. No se crea una ventana nueva en la pantalla exterior. No se copia el modelo de página para las seis posturas. Solo se vuelve a considerar cuando el producto de verdad quiera tratar abrir y cerrar como una entrada, o cuando aparezca una interfaz de captura.
Lo que todavía no cuenta como terminado: las seis posturas todavía hay que verlas a mano en Device Hub. Los comandos públicos del simulador no pueden abrir, cerrar ni dejarlo a media apertura. La máscara de pago en compact no es un sheet del sistema. Un sheet de explicación que solo tiene un botón de cerrar, y si se apaga o no su barra vertical, sigue pendiente. La barra superior fija que usa la transición de elemento compartido sigue fuera de la capa de escala. Que compile no significa que el pliegue y la barra vertical ya se hayan visto.
Cómo se engancha una página nueva: primero entra en una pestaña existente o en la pila de navegación, y el estado se queda fuera. Después, según el escalón del contenedor, se decide una sola columna o ir en paralelo. El texto largo se detiene alrededor de 720 pt. Arrangement solo se usa cuando dos bloques de contenido ya tenían que cambiar de sitio, y con #available(iOS 27.1, *) se deja el diseño antiguo. Primero se escriben la toolbar del sistema, el size class y el ancho de lectura. Después se escriben la región reservada y Arrangement. La bisagra y la pantalla secundaria se siguen sin escribir, hasta que el producto las necesite.
Compruébalo tú con esta app
Después de que la pantalla interior se ensanche, ¿las tres pestañas deben subir a una barra lateral o quedarse abajo?
Cuando el contenedor regular solo tiene 800 pt, «Hoy», ¿debe ir en una sola columna o en paralelo?
La esfera y los controles del ejercicio de respiración, ¿por qué usan
.splity noonHingeChange?Si a media apertura la máscara de pago en compact pisa el pliegue, ¿se puede decir «el sheet del sistema lo evita»?
Respuestas. Las tres pestañas se quedan abajo. 800 pt sigue siendo una sola columna. En paralelo empieza a 801 pt. La esfera y los controles son la relación de arriba y abajo del mismo ejercicio. Se lo entregas a split. El ángulo no decide el diseño. La máscara de pago no es un sheet del sistema y no evita el pliegue sola.
Seguir leyendo
Prueba estas ideas en Stress HRV
Descargar en App StoreEstas notas comparten pensamiento de producto para bienestar. No son consejo médico.