Langsung ke konten utama

Dukungan iPhone Duo di aplikasi Capacitor

Siapkan aplikasi WebView Capacitor untuk iPhone Duo: viewport-fit=cover, env(safe-area-inset-*), plugin System Bars dan Status Bar, pengujian fold dan resize, serta @capgo/capacitor-native-navigation di sidebar.

iPhone Duo terbuka dalam mode landscape dengan bar native di side rail, dan terbuka dalam mode portrait dengan tab bar di bagian bawah

iPhone Duo lipat dari Apple mengubah cara sistem chrome dan safe area bekerja. Expo menerbitkan panduan praktis untuk aplikasi React Native di SDK 58. Artikel ini adalah ekuivalen Capacitor: satu WebView, CSS env insets, plugin inti Capacitor, dan bar native opsional dari @capgo/capacitor-native-navigation.

Apa yang berubah di iPhone Duo

Pada banyak pose, iOS memindahkan status bar, navigation bar, dan tab bar ke dalam rel sisi vertikal di tepi depan atau belakang. Bilah horizontal tetap muncul dalam orientasi potret pada layar dalam yang terbuka dan pada tata letak ponsel klasik.

Gambar di atas adalah tampilan layar terbuka. Dalam orientasi lanskap, toolbar dan tab bar ditumpuk di rel. Dalam orientasi potret, toolbar kembali ke atas dan tab bar kembali ke bawah.

Untuk konten web di dalam WebView Capacitor, pergeseran penting adalah:

  • env(safe-area-inset-top) bisa 0 ketika status bar berada di rail, bukan di bawah notch.
  • Inset leading dan trailing membesar (Apple mendokumentasikan sekitar 84pt untuk rail dalam landscape dan posisi terlipat). Kiri dan kanan bukan cerminan satu sama lain.
  • Perubahan ukuran window ketika pengguna melipat, membentangkan, memutar, atau menggunakan Split View di layar dalam. Nilai layout yang di-cache cepat menjadi usang.
  • Tab bar native mengikuti rail saat Anda menggunakan UITabBar (atau Capgo Native Navigation dengan tab bar sistem). Tab bar HTML fixed kustom tidak mengikuti.

Perlakukan layar luar terlipat sebagai lebar kompak dan layar dalam terbuka sebagai lebar reguler. Perlebar layout alih-alih mengganti ke app shell lain.

Aplikasi email di iPhone Duo dalam tiga posisi. Saat terlipat, toolbar dan tab bar bertumpuk di rail vertikal di sebelah kanan. Di Split View, tiap jendela mempertahankan rail di tepi luarnya. Saat dibuka dalam portrait, toolbar membentang di bagian atas dan tab bar berada dalam pill di bagian bawah.

Di mana system bar berada: rail di kanan saat perangkat terlipat, di tepi luar tiap jendela di Split View, dan bar horizontal lagi di layar terbuka dalam portrait.

Persyaratan

  • Capacitor 8 aktif iOS 27 (Xcode 27.1 dengan runtime simulator iPhone Duo).
  • Device Hub (Xcode > Open Developer Tool > Device Hub) untuk melipat, mengatur engsel, dan memutar simulator Duo.
  • Sebuah viewport yang mengaktifkan safe areas (lihat di bawah).

viewport-fit=cover dan CSS safe areas

Variabel env safe area hanya berlaku saat viewport menutupi seluruh layar:

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />

Pad interaktif konten di setiap sisi. Biarkan background memenuhi layar untuk kesan imersif.

Pilih satu inset owner untuk seluruh aplikasi. Dua cuplikan kode di bawah adalah alternatif, bukan pasangan untuk digabungkan.

Opsi A: shell per-halaman

.page {
  min-height: 100dvh;
  padding-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
  padding-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
  padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
  padding-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px));
}

Lewati Opsi B saat memakai Opsi A.

Pada scroll view, beri inset pada konten, bukan hanya kontainer scroll luarnya, agar background bisa scroll selebar penuh sementara teks dan tombol tetap bebas dari rail.

Gunakan empat nilai independen. Jangan asumsikan padding-left sama dengan padding-right. Dalam mode RTL, rail tetap berada di sisi kamera; baca keduanya left dan right nilai env.

Dua tempat yang mudah terlewat:

  • Scroller horizontal di dalam halaman yang sudah diberi padding tetap perlu padding ujung sendiri. Kalau tidak, kartu terakhir akan bergeser ke bawah rail.
  • Tombol mengambang yang disetel ke right: 16px mengabaikan rail. Tambahkan inset kiri atau kanan live, lalu hitung ulang saat jendela di-resize.

Untuk aplikasi Tailwind, @capgo/tailwind-capacitor menyediakan pt-safe, px-safe, dan utilitas terkait alih-alih mengulang env() di mana-mana.

Capacitor System Bars (safe area di Android dan config di iOS)

Capacitor 8 menghadirkan System Bars di @capacitor/core. Ini menggantikan yang lama khusus Android adjustMarginsForEdgeToEdge flag. Konfigurasikan di capacitor.config.ts:

import type { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  plugins: {
    SystemBars: {
      insetsHandling: 'css',
    },
  },
};

export default config;
  • insetsHandling: 'css' (default di Android): menyuntikkan --safe-area-inset-* variabel CSS ketika nilai env WebView salah (build Android WebView lama).
  • Opsi B (shell situs) menggunakan pola fallback dari Dokumentasi System Bars. Lewati Opsi A jika Anda menggunakan Opsi B.
html {
  padding-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px));
  padding-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
  padding-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
  padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
}

Saat runtime, Anda dapat menata atau menyembunyikan bar dengan SystemBars.setStyle, SystemBars.show, dan SystemBars.hide dari @capacitor/core. Tidak ada yang terpisah @capacitor/safe-area paket terpisah: safe area untuk aplikasi Capacitor adalah env CSS, variabel System Bars di Android, dan listener Anda sendiri di resize / visualViewport.

@capacitor/status-bar

Panduan plugin Status Bar masih mengontrol overlay dan gaya di iOS:

import { StatusBar, Style } from '@capacitor/status-bar';

await StatusBar.setOverlaysWebView({ overlay: true });
await StatusBar.setStyle({ style: Style.Dark });

setOverlaysWebView({ overlay: true }) membiarkan HTML menggambar di bawah area status bar. Kombinasikan dengan padding env atau konten akan terpotong. Di iPhone Duo, mode overlay tetap berlaku; geometri mana inset atas vs samping berasal dari safe area sistem, bukan hanya dari API Status Bar.

Jangan menumpuk margin otomatis WebView, padding env halaman penuh, dan variabel inset bar native tanpa strategi eksplisit. Pola ini menyebabkan spasi ganda di setiap iPhone, dan semakin parah saat rail memindahkan insets ke sisi.

contentInset WebView iOS

Di capacitor.config.ts, ios.contentInset dipetakan ke scroll view WebView contentInsetAdjustmentBehavior. Banyak aplikasi menggunakan 'never' ketika menangani insets di CSS. Jika Anda mengandalkan penyesuaian UIKit otomatis dan padding env CSS, pilih satu sumber kebenaran.

Lihat Konfigurasi Capacitor iOS.

Resizing WebView saat dilipat dan dibuka

Capacitor mempertahankan satu WebView full-screen. Saat Duo berganti pose:

  1. UIKit memperbarui safe area inset pada view controller.
  2. Layout pass WebView berjalan dengan lebar dan tinggi baru.
  3. CSS env(safe-area-inset-*) dan 100dvh / visualViewport nilai diperbarui pada style flush berikutnya.

Pantau perubahan geometri di JavaScript saat Anda meng-cache posisi yang bergantung pada inset (floating button, toast, overlay canvas):

function onGeometryChange() {
  const vv = window.visualViewport;
  const width = vv?.width ?? window.innerWidth;
  const height = vv?.height ?? window.innerHeight;
  const offsetLeft = vv?.offsetLeft ?? 0;
  const offsetTop = vv?.offsetTop ?? 0;
  document.documentElement.style.setProperty('--app-vv-width', `${width}px`);
  document.documentElement.style.setProperty('--app-vv-height', `${height}px`);
  document.documentElement.style.setProperty('--app-vv-offset-left', `${offsetLeft}px`);
  document.documentElement.style.setProperty('--app-vv-offset-top', `${offsetTop}px`);
}

window.addEventListener('resize', onGeometryChange);
window.visualViewport?.addEventListener('resize', onGeometryChange);
window.visualViewport?.addEventListener('scroll', onGeometryChange);
onGeometryChange();

Scroll listener mencakup pergeseran visual-viewport yang mengubah offsetLeft / offsetTop tanpa mengubah lebar atau tinggi (misalnya pergeseran viewport yang dipicu keyboard di iOS).

Di Android, sertakan density di android:configChanges di main activity Anda agar perangkat lipat dan perubahan ukuran layar tidak menghancurkan WebView. Capacitor 8 template mendokumentasikan ini di panduan upgrade Capacitor 8.

Orientasi dan kelas ukuran jendela

Gunakan live lebar untuk branching layout, bukan screen.width diambil sekali saat startup:

const compactQuery = window.matchMedia('(max-width: 600px)');

function readCompactWidth() {
  return compactQuery.matches;
}

compactQuery.addEventListener('change', () => {
  document.documentElement.dataset.widthClass = readCompactWidth() ? 'compact' : 'regular';
});
document.documentElement.dataset.widthClass = readCompactWidth() ? 'compact' : 'regular';

Di CSS, utamakan flex dan grid dengan minmax() daripada fixed 100vw lebar. 100vw sering mengabaikan scrollbar dan asimetri rail, sehingga menyebabkan overflow horizontal.

Untuk Split View di layar dalam, setiap jendela bisa sempit (Apple menyebutkan lebar sekitar 469pt). Uji aplikasi Anda saat docked di kiri dan kanan: rail menempel di jendela tepi, jadi inset horizontal bisa berpindah sisi antar jendela.

Chrome HTML kustom vs bar native

Panduan Expo merekomendasikan native Stack dan NativeTabs sehingga iOS yang menentukan penempatan rail. Prinsip yang sama berlaku di Capacitor:

Pendekatan Perilaku iPhone Duo
Header HTML tetap + tab bawah Tetap di safe area klasik atas/bawah. Tidak pindah ke rail.
@capgo/capacitor-native-navigation dengan sistem UITabBar / UINavigationBar Bar mengikuti penempatan sistem, termasuk rail vertikal.

Jika Anda harus tetap memakai HTML chrome, periksa setiap layar untuk padding env kiri dan kanan, bukan hanya atas dan bawah.

Layar yang sama dengan header HTML tetap dan tab bawah. Saat dilipat, header dan tab tetap berada di persegi panjang klasik dan hanya menghindari rail. Saat dibuka dalam mode portrait, keduanya tetap di atas dan bawah.

HTML chrome tetap tidak mengikuti rail. Ia tetap berada di area aman atas dan bawah. Bar native, yang digambar oleh UIKit atau @capgo/capacitor-native-navigation, bergerak mengikuti sistem.

@capgo/capacitor-native-navigation di iPhone Duo

Native Navigation menggambar bar asli UINavigationBar dan UITabBar (atau bar Material di Android) di atas satu Capacitor WebView. Di iOS 27 dengan iPhone Duo, tab bar sistem berpindah ke rail samping saat dilipat dan dalam banyak posisi landscape. Lengkungan plugin ini bentuk tab bar tetap di tepi bawah. Tombol tengah tetap berada di cradle melingkar saat jendela melebar.

Dua layout landscape terbuka dari @capgo/capacitor-native-navigation.. Di kiri, tab bar sistem berada di rail samping. Di kanan, tab bar curve tetap di bawah, dan tombol tengah berada di cradle melingkar.

Kiri, tab bar sistem ada di rail. Kanan, tab bar curve tetap di bawah dan tombol tengah tetap di cradle-nya.

Konfigurasi sekali saja

import { NativeNavigation } from '@capgo/capacitor-native-navigation';

await NativeNavigation.configure({
  contentInsetMode: 'css',
});

contentInsetMode: 'css' menulis ukuran overlap bar dan safe area ke root dokumen:

  • --cap-native-navigation-top
  • --cap-native-navigation-right
  • --cap-native-navigation-bottom
  • --cap-native-navigation-left
  • --cap-native-navbar-height
  • --cap-native-tabbar-height
  • --cap-native-system-tabbar-height (tinggi nominal tab bar saat tabbarHeight adalah 0 untuk system bar yang di-host)

Saat Native Navigation mengelola bar, beri padding dengan variabel plugin di bawah. Variabel tersebut sudah mencakup overlap bar native dan tepi safe-area UIKit saat bar aktif. Gunakan max() dengan fallback env agar tepi tetap benar saat variabel 0 (misalnya tab bar sistem yang di-host) tanpa menambahkan plugin dan padding env secara bersamaan.

main {
  padding-top: max(var(--cap-native-navigation-top, 0px), env(safe-area-inset-top, 0px));
  padding-right: max(var(--cap-native-navigation-right, 0px), env(safe-area-inset-right, 0px));
  padding-bottom: max(var(--cap-native-navigation-bottom, 0px), env(safe-area-inset-bottom, 0px));
  padding-left: max(var(--cap-native-navigation-left, 0px), env(safe-area-inset-left, 0px));
}

Jangan terapkan juga padding Opsi A atau Opsi B pada elemen main yang sama.

Di iOS 26+ dengan system-hosted tab bar, tabbarHeight inset plugin dapat 0 sehingga konten web tidak terangkat dua kali. Gunakan --cap-native-system-tabbar-height ketika Anda membutuhkan spasi untuk aksi mengambang di atas rail.

safeAreaChanged

Berlangganan saat fold, sisi rail, atau visibilitas bar berubah:

await NativeNavigation.addListener('safeAreaChanged', ({ insets }) => {
  console.log(insets.top, insets.right, insets.bottom, insets.left);
  console.log(insets.navbarHeight, insets.tabbarHeight, insets.systemTabbarHeight);
});

Plugin ini juga mendispatch capNativeNavigation:safeAreaChanged aktif window dengan yang sama insets payload.

Native code menerapkan sedikit celah atas saat safeInsets.top nol (status bar di rail) agar tombol navbar tidak menyentuh sudut layar melengkung. Padding web Anda tetap harus menggunakan env left/right untuk rail.

setNavbar, setTabbar, dan configure semuanya return { insets } ketika Anda perlu membaca sekali setelah memperbarui chrome.

Referensi lengkap API: Dokumentasi Native Navigation.

Checklist pengujian

Di Device Hub dengan simulator iPhone Duo:

  1. Terlipat: kontrol tidak berada di bawah rail; tap terdaftar; inset env horizontal tidak nol di posisi yang diharapkan.
  2. Terbuka portrait: bar atas/bawah kembali ke posisi yang ditetapkan iOS; layout tidak melompat ke arsitektur informasi lain.
  3. Terbuka landscape / rail: konten tidak tertutup rail samping; floating actions menggunakan insets.right atau insets.left, bukan nilai hard-coded 16px.
  4. Lipatan sebagian: tidak ada UI kritis di bagian engsel; modal dan sheet tetap dapat digunakan.
  5. Split View: jalankan aplikasi di jendela kiri dan kanan; tepi rail mengikuti setiap jendela.
  6. Lipat saat layar terbuka: state rute, posisi scroll, dan input formulir tetap terjaga selama transisi.
  7. Simulator iPhone klasik: tidak ada regresi.

Prompt agen (salin dan tempel)

Gunakan blok ini dengan Claude Code, Cursor, Codex, atau agen coding lain yang bekerja di repo Capacitor:

Task: Make this Capacitor app safe for iPhone Duo (iOS 27 foldable).

Context:
- Read https://capgo.app/blog/capacitor-iphone-duo-safe-area-guide/ and Expo's https://expo.dev/blog/supporting-iphone-duo for platform behavior.
- One WebView. Use viewport-fit=cover and per-edge env(safe-area-inset-*) padding for interactive content; backgrounds may ignore safe areas.
- Capacitor 8: configure plugins.SystemBars (insetsHandling css). Set viewport-fit=cover in index.html. Use var(--safe-area-inset-*, env(...)) fallbacks on Android.
- @capacitor/status-bar: only if the app already uses it; setOverlaysWebView must match CSS padding strategy.
- Do not double-apply Capacitor margin adjustment, full-page env padding, and native navigation CSS variables.
- On resize and visualViewport resize, recompute any cached inset or FAB positions.
- If the app uses @capgo/capacitor-native-navigation: contentInsetMode css, listen for safeAreaChanged, pad with max(var(--cap-native-navigation-*, 0px), env(safe-area-inset-*, 0px)) per edge (not sum); verify system tab bar rail poses in the iOS 27 Duo simulator.
- If the app uses custom HTML headers or tab bars, add left/right safe padding on every screen and document that bars will not move into the rail.

Deliverables:
1. List files changed and the inset strategy (single source of truth).
2. Replace fixed top-only safe padding with four-edge padding where needed.
3. Add or update capacitor.config and index.html viewport meta if missing.
4. Note manual test poses: folded, open portrait, open landscape rail, Split View left/right.

Constraints:
- Only use APIs that exist in @capacitor/core, @capacitor/status-bar, and @capgo/capacitor-native-navigation definitions.
- No em dashes or en dashes in user-facing strings you add.

Instal Skill Capgo agar agen tahu cara mengintegrasikan Native Navigation: npx skills add Cap-go/capgo-skills --skill capacitor-plugins.

Bacaan lanjutan

Live updates untuk aplikasi Capacitor

Saat bug web-layer sudah live, kirim perbaikannya lewat Capgo alih-alih menunggu berhari-hari untuk persetujuan app store. Pengguna mendapat update di background, sementara perubahan native tetap mengikuti jalur review normal.

Terbaru dari Blog kami

Capgo memberikan insight terbaik yang Anda butuhkan untuk membuat aplikasi mobile yang benar-benar profesional.