iOS Setup
Building the iOS shell in Xcode.
The iOS shell is a thin native wrapper that presents your existing Hotwire web views natively, following the path configuration Rails already serves. Setup means creating an Xcode project, dropping in the provided Swift sources, and pointing it at your app.
Creating the project
Open Xcode and choose File, New, Project, App. Name it OneShot, and choose Storyboard as the
interface, not SwiftUI: these sources use the UIKit app-delegate lifecycle and a programmatic
UIWindow, and a SwiftUI @main App struct would conflict with the @main AppDelegate this shell
provides.
Add the Hotwire Native package through File, Add Package Dependencies, using
https://github.com/hotwired/hotwire-native-ios. If Xcode fails with an authentication error
because of a stale GitHub account, use the SSH form of the URL instead, or remove the stale account
under Settings, Accounts, so the public repository fetches anonymously.
Adding the shell's source files
Add the Swift files in mobile/ios/Sources/ to your target, replacing the generated
AppDelegate.swift and SceneDelegate.swift, and delete the generated ViewController.swift.
Because the window is built programmatically, you also have to remove the storyboard entirely, in
three places, or the app crashes at launch:
- Delete
Main.storyboard. - Remove the
UISceneStoryboardFilekey fromInfo.plist's scene manifest. - Clear "Main storyboard file base name" in Build Settings. Xcode 16 re-injects this setting into
the built
Info.plistfrom the storyboard template, so deleting the file alone isn't enough.
Pointing it at your app
Set your app's URL in Sources/Config.swift: http://localhost:3000 for the simulator against a
local Rails server, or your production host once you're ready to test against it.
The app icon
The template ships with an empty AppIcon set, so the app installs with a blank icon until you set
one. Drop a single 1024x1024, opaque, full-bleed PNG (no alpha channel; iOS applies its own corner
rounding, so don't pre-round the corners yourself) into Assets.xcassets/AppIcon.appiconset/. If
the icon still looks blank after you change it, delete the app from the simulator first; it caches
icons aggressively.
What's in the shell
Config.swift: the base URL and the derived path-configuration URL.SceneDelegate.swift: boots the HotwireNavigatorwith the remote path configuration.AppDelegate.swift: standard app lifecycle.OAuthSession.swift: opens Google and Apple sign-in inASWebAuthenticationSession, never the embedded web view, since Google blocks OAuth there. See Auth in the App.StoreKitManager.swift: runs a StoreKit 2 purchase and posts the receipt to/iap/purchase, the entitlement bridge back to your Rails app. See In-App Purchases.
In-app purchase support is wired but optional: web Stripe billing works fully without any of the StoreKit code running.
Local development gotchas
Pointing Config at http://localhost:3000 can be blocked by App Transport Security. If the app
won't load against a local server, add an ATS exception for local networking in Info.plist; this
isn't needed once you're pointed at an https:// host.
Add ITSAppUsesNonExemptEncryption set to NO (a Boolean) in Info.plist. The shell only uses
standard HTTPS from the OS, which is exempt from export compliance review, and this key skips the
encryption-documentation prompt on every TestFlight and App Store upload; without it, you'd answer
that prompt by hand every time.
Next
See the same setup for Android: Android Setup. Or, once you're ready to ship, Submitting to the Stores.