A minimal iPadOS app that turns an iPad Pro 11" (2018, USB-C) connected to a
USB-C dock/monitor into a full-screen web browsing workstation: the iPad
shows browser controls, the external monitor shows one full-screen
WKWebView. No tabs, no Safari/Chrome embedding, no mirroring.
Phase 1 (external display detection) and Phase 2 (persistent WKWebView on the external display) are implemented together in this first build, so they can be validated on real hardware in a single sideload. Per the project spec, everything after this is deliberately gated on that hardware test succeeding — see "Known risk" below.
Implemented so far:
- External display detection via
UIScreenconnect/disconnect notifications (ExternalDisplay/ExternalDisplayManager.swift) - A dedicated
UIWindowon the external screen, entirely separate from the iPad's own window — not mirrored - One persistent
WKWebView(Browser/BrowserEngine.swift) that is never recreated, with a real toolbar (back/forward/reload/URL) on the external display - iPad controller UI showing connection status, resolution, and browser
state (
Controller/ControllerView.swift) - Full-screen toggle, keyboard shortcuts (⌘L, ⌘R, ⌘[, ⌘], Esc), persistent cookies/storage, homepage + clear-data settings
Not yet built (waiting on Phase 1/2 hardware confirmation before continuing, per spec section 38): further Microsoft 365 / general web-compatibility testing pass, refined disconnect/reconnect polish.
Tag stable-1 is a hardware-confirmed good build: mouse, keyboard,
modifier keys, the external toolbar, and scrolling all verified working
on the target iPad. If a later change misbehaves, go back to it:
git checkout stable-1A copy of that build is also kept locally as
build-artifacts/ExternalBrowser-STABLE.ipa — sideload it directly to
recover without waiting for a rebuild.
- App Transport Security is set to allow arbitrary loads (plain HTTP, not just HTTPS). This is deliberate and matches every third-party iOS browser (Chrome/Firefox for iOS do the same) — a general-purpose browser has to be able to load whatever URL the user types, including old HTTP-only sites. It does not weaken TLS certificate validation for HTTPS sites — that's enforced by WebKit independently of this setting. If you'd rather the browser refuse plain HTTP entirely (HTTPS-only), say so and I'll flip it.
- JavaScript
alert/confirm/promptare shown as real dialogs the user has to respond to — an earlier draft of this code auto-acceptedconfirm()calls and silently swallowedalert()messages, which would have let a page's JS treat an unseen "OK" as a real user decision. Fixed inBrowser/BrowserEngine.swift. - No autoplaying media.
WKWebViewConfigurationis left at WebKit's default (mediaTypesRequiringUserActionForPlayback = .all), so video/ audio needs a user tap before it plays — same as Safari. An earlier draft disabled that gate, which would have let any page's background video or ad autoplay at full volume and burn battery/CPU on load. - Exactly one
WKWebViewever exists — popup/window.open()requests load into the same view rather than spawning a hidden second web view, and nothing is preloaded speculatively. No timers or polling loops anywhere in the app; all state updates are event-driven (KVO on the web view,UIScreenconnect/disconnect notifications), so there's nothing running when the app is idle. - No network code beyond
WKWebViewitself, no analytics, no telemetry, no third-party SDKs. Cookies/site data persist viaWKWebsiteDataStore.default()and are only cleared when you tap "Clear Website Data" in Settings — never automatically.
There are two different public iPadOS APIs for driving a second screen:
UIScreen+UIWindow(frame:).screen = externalScreen— the classic approach, used here. Simpler, fewer exotic symbols to get wrong without a compiler to check my work against.UIWindowScenewith an external-display session role — the more "modern" scene-based approach the spec also mentions.
Both are public, documented APIs. I chose (1) because it's the
longer-standing and simplest option to hand-write correctly without being
able to compile/test locally (there's no Mac in this environment). The
one thing I can't verify without your hardware: whether the mouse
pointer reaches the web page on the external monitor with this
approach. If it doesn't, that's the signal to switch to approach (2) —
tell me and I'll rework ExternalDisplayManager.
You don't need Xcode yourself — see docs/SIDELOAD.md.
CI (.github/workflows/build.yml) runs on a macOS GitHub Actions runner,
regenerates the Xcode project from project.yml with
XcodeGen, builds an unsigned
.ipa, and uploads it as a build artifact. AltStore/AltServer does the
actual code-signing at install time using your own free Apple ID — no
Apple Developer Program membership or secrets needed in this repo.
ExternalBrowser/
├── App/ AppDelegate, SceneDelegate (iPad's own scene)
├── ExternalDisplay/ ExternalDisplayManager — owns the second UIWindow
├── Browser/ BrowserState, BrowserEngine (the one WKWebView),
│ BrowserView (external-display UI)
├── Controller/ ControllerView (iPad UI)
└── Settings/ AppSettings, SettingsView