La primera pregunta que oí de mis colegas tras el anuncio del iPhone Duo: «¿se puede leer el ángulo de plegado?». Se puede. Apple expone tanto el estado discreto de la bisagra como el ángulo continuo — en SwiftUI y en UIKit. Pero en el mismo Tech Talk donde enseñan este API suena una restricción tajante: la bisagra es para efectos e interacciones, no para layout. La diferencia es de fondo, y sobre ella se construye toda la segunda mitad de las nuevas APIs. Lo repaso en orden: hinge API, reserved regions, arrangements y cámaras — con lo que está disponible desde Objective-C y lo que va a requerir Swift.
Como en el primer artículo de la serie, un recordatorio: las firmas salen de los Tech Talks 111463–111465, Xcode 27.1 sigue en beta — antes de producción contrástalas con las cabeceras del SDK.
onHingeChange: tres estados y un ángulo en vivo#
El modificador de SwiftUI onHingeChange invoca un closure con el contexto anterior y el actual. El ejemplo canónico de Apple es una app de guitarra donde el ángulo de plegado funciona como palanca de trémolo:
struct InstrumentView: View {
/// 0 — no bend, 1 — maximum
@State private var pitchBend: Double = 0
var body: some View {
GuitarView(pitchBend: pitchBend)
.onHingeChange { _, context in
if let hinge = context.hinge, hinge.status == .partiallyOpen {
pitchBend = calculatePitchBend(angle: hinge.angle)
} else {
pitchBend = 0
}
}
}
}Aquí hay dos elementos obligatorios que es fácil pasar por alto:
context.hingees Optional.nilsignifica que el dispositivo no tiene bisagra — el mismo binario corre en los iPhone normales. Sin ese guard, el código se cae en la mayoría de los dispositivos del mundo.- La rama
elsecon el reset. Los estados son tres:.closed,.partiallyOpen,.fullyOpen. Si solo reseteas el efecto en.closed, al desplegar el dispositivo en plano el efecto se queda «pegado» en el último ángulo.
En UIKit lo mismo lo hace UIHingeInteraction — por la convención de la familia UIInteraction se añade a una view, y eso significa que está disponible desde Objective-C:
// API shape follows the UIInteraction family; check the exact
// handler signature against the iOS 27.1 SDK headers
UIHingeInteraction *hinge = [[UIHingeInteraction alloc] init];
[self.effectView addInteraction:hinge];Importa lo que aquí no hay: notificaciones. Ninguna fuente oficial menciona una NSNotification para la bisagra — la observación es solo vía interaction o modificador. Si en algún sitio te encuentras UIDeviceHingeDidChangeNotification, es una invención.
Por qué la bisagra no es para layout#
La tentación es obvia: leer el ángulo y disponer la interfaz en función de él. Apple dice con todas las letras que no lo hagas. El ángulo es un flujo sensorial continuo; un layout atado a él tiembla durante el plegado y se rompe en todos los dispositivos sin bisagra. Para las decisiones estructurales hay dos APIs aparte.
Reserved regions (iOS 27.1) — zonas de la view «ocupadas» por el hardware. Hay dos tipos: .division — el pliegue, divide el área (activo solo mientras el dispositivo está parcialmente plegado; en plano la región queda inactiva y con ancho cero); .occlusion — las cámaras, tapan el área.
// SwiftUI — from GeometryProxy
let folds = proxy.reservedRegions(kind: .division)
let cameras = proxy.reservedRegions(kind: .occlusion)
// For stable decisions: account for the fold even when inactive
let allFolds = proxy.reservedRegions(kind: .division, options: .includeInactive)// Objective-C: same method on UIView; regions are UIViewReservedRegion
NSArray<UIViewReservedRegion *> *folds =
[self.view reservedRegionsWithKind:UIViewReservedRegionKindDivision
options:0];
for (UIViewReservedRegion *region in folds) {
// region.frame — the fold rectangle in view coordinates
}La opción .includeInactive resuelve un problema poco evidente: si la rejilla se reconstruye cada vez que la región del pliegue aparece y desaparece, la interfaz se «rebaraja» con cada movimiento de la bisagra. La decisión estable es, por ejemplo, un número siempre par de columnas en un dispositivo que, de entrada, tiene pliegue.
La buena noticia: los contenedores del sistema traen el fold avoidance gratis. NavigationSplitView, TabView, sheets, alerts, menús y popovers apartan por sí solos los elementos interactivos de la curvatura del pliegue. La única excepción es el contenido con scroll: a un artículo o a un feed se le permite fluir bajo el pliegue, no hay que moverlo.
ArrangementView: split u overlay#
Para el par de views «entre la navegación y el contenido» aparece un contenedor propio. En SwiftUI, ArrangementView; en UIKit, UIArrangementViewController:
ArrangementView {
PlayerView()
} secondary: {
UpNextView()
}
.arrangementViewStyle(.split)UIArrangementViewController *arrangement = [[UIArrangementViewController alloc] init];
[arrangement setViewController:playerVC forPlacement:UIArrangementPlacementPrimary];
[arrangement setViewController:upNextVC forPlacement:UIArrangementPlacementSecondary];Dos estilos con semánticas distintas. Split divide los bounds y no tapa nada — main/detail tipo reproductor con transcripción; un bonus agradable frente al comportamiento de iPad: al cerrar la secondary, la view primaria no se recentra cruzando toda la pantalla, sino que se queda junto al pliegue. Overlay es una pila que al plegar se despliega lado a lado; son los controles en primer plano sobre contenido con scroll. La heurística de migración es simple: lo que era HStack/VStack — split; lo que era ZStack — overlay.
Y dos prohibiciones del Tech Talk: no meter un NavigationSplitView dentro de un arrangement (el contenedor no aporta infraestructura de navegación) y no meter un arrangement dentro de List/ScrollView. Si necesitas columnas plegables, eso sigue siendo UISplitViewController; el arrangement no es para eso.
Cámaras: position == .front ya no significa «te está mirando»#
El Duo trae por primera vez en un iPhone dos cámaras frontales: la exterior (4K/120) y la interior bajo la pantalla (1080p/60). Ambas reportan honestamente position == .front — pero las pantallas pueden mirar en direcciones opuestas, así que la cámara «frontal» no necesariamente mira al usuario. Sobre ese hecho se rompe toda la lógica de mirroring escrita antes de 2026.
El camino simple es la cámara frontal virtual: un dispositivo del sistema que conmuta solo entre el módulo interior y el exterior al abrir y cerrar. El discovery es como siempre (AVCaptureDeviceDiscoverySession con position .front — funciona también desde Objective-C), pero entrega solo la intersección de capacidades de ambas cámaras: 1080p, 60 fps, sin depth. Para la mayoría de las apps es suficiente, y conmutar módulos deja de ser problema tuyo.
Si necesitas las capacidades completas de un módulo concreto, aparecen los tipos .builtInInnerUltraWideCamera y .builtInOuterUltraWideCamera — pero entonces la conmutación corre de tu cuenta, y aquí entra la principal herramienta nueva, AVCaptureDeviceDirectionCoordinator de AVKit:
let coordinator = AVCaptureDeviceDirectionCoordinator(
view: previewView,
deviceTypes: [.builtInInnerUltraWideCamera, .builtInOuterUltraWideCamera]
) { descriptor in
// The descriptor is Sendable; build the AVCaptureDevice on the camera actor
Task { await cameraActor.switchTo(descriptor) }
}El coordinator va ligado a una view concreta y responde a la pregunta «qué cámara mira ahora mismo hacia el mismo lado que este display». Está aislado en el main actor, y a través de la frontera del actor no viaja un AVCaptureDevice, sino un AVCaptureDeviceDescriptor — la representación sendable del dispositivo. En el change handler hay tres obligaciones: reconfigurar la sesión, recalcular el mirroring por dirección, no por position, y actualizar la UI.
Dos detalles de cámara más del Tech Talk 111465: tras pasar a AVCaptureDeviceRotationCoordinator, desactiva isCameraSensorOrientationCompensationEnabled — en las frontales del Duo la compensación viene activada por defecto y se convertiría en trabajo duplicado; y dynamicAspectRatio permite aprovechar el sensor cuadrado para el landscape de la pantalla interior.
Teleprompter en la segunda pantalla#
Las apps de cámara reciben una posibilidad más: mientras la UI principal ocupa la pantalla interior con la sesión de cámara activa, en la exterior se puede mostrar contenido adicional — CameraCaptureAccessory. Los escenarios de la charla: un teleprompter para grabar vídeo, o algo divertido para el niño al que estás fotografiando.
CameraView(model: model)
.sceneAccessory {
CameraCaptureAccessory(isEnabled: $model.isEnabled) {
TeleprompterView(model: model)
}
.onAvailabilityChange { model.isAvailable = $0 }
}La disponibilidad la gobierna el sistema, así que onAvailabilityChange no es una opción sino una obligación: el accesorio puede desaparecer en cualquier momento en que el usuario pliegue el dispositivo.
Balance#
La lógica de las nuevas APIs compone un cuadro coherente: la bisagra es un sensor para efectos, las reserved regions son hechos sobre el hardware para el layout, los arrangements son patrones listos para un par de views, y la dirección de la cámara es una entidad propia que no se deduce de position. Desde Objective-C está disponible toda la superficie UIKit/AVFoundation; los exclusivos de SwiftUI (onHingeChange, ArrangementView, sceneAccessory) en un proyecto ObjC pedirán o los análogos UIKit o una capa fina de Swift — de eso va el tercer artículo de la serie.



