Docs · Mobile Apps

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 UISceneStoryboardFile key from Info.plist's scene manifest.
  • Clear "Main storyboard file base name" in Build Settings. Xcode 16 re-injects this setting into the built Info.plist from 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 Hotwire Navigator with the remote path configuration.
  • AppDelegate.swift: standard app lifecycle.
  • OAuthSession.swift: opens Google and Apple sign-in in ASWebAuthenticationSession, 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.