Skip to content

iOS#

On iOS, your screens are drawn with UIKit: a Button is a UIButton, a TextInput a UITextField, a ScrollView a UIScrollView. The app that leptoskit new generates is a small UIKit app that runs your screens as WebAssembly, so it can also replace them over the air with code push. Text is measured with the same fonts UIKit draws it with, so each label and button gets exactly the space it needs.

The generated app runs your screens' code on a thread of its own, not UIKit's main thread. Updating state, laying the screen out, and measuring text happen there, and the main thread only applies the result to the views. A slow update delays what you draw next, but scrolling, animations, and touches keep running meanwhile.

What you need#

  • A Mac with Xcode and an installed iOS simulator runtime.
  • The Rust targets for the simulator and for devices:
rustup target add aarch64-apple-ios-sim aarch64-apple-ios wasm32-unknown-unknown

On an Intel Mac, the simulator target is x86_64-apple-ios.

You don't need CocoaPods. A plugin's Swift Package Manager dependencies resolve automatically, and CocoaPods is needed only when a plugin declares apple-pods — leptoskit then writes the Podfile and runs pod install itself before every build. leptoskit doctor says when CocoaPods is required.

Develop with live reload#

leptoskit dev --platform ios

dev builds the app for the simulator, installs it in every booted iOS Simulator, and opens it. With no simulator booted, it boots the newest iPhone simulator. When you save, it rebuilds your interface and the running app swaps it in. Building and serving covers the rest.

Run it in the simulator#

leptoskit native build ios-simulator
open .leptoskit/ios/Host.xcodeproj

The first command compiles your app to WebAssembly, compiles the UIKit host, and generates an Xcode project. In Xcode, pick a simulator and press Run. Simulator builds are signed ad hoc, so you don't need a development certificate.

The Xcode project is regenerated by every build. Make your changes in the ios/ folder instead: App.swift starts the app, Info.plist holds its name, version, and permission strings, and Cargo.toml lists native Rust dependencies such as runtime plugins.

Run it on a device#

leptoskit build --platform ios

This builds for aarch64-apple-ios. Device builds are not signed: open .leptoskit/ios/Host.xcodeproj, choose your development team under Signing & Capabilities, and run it on your device. For TestFlight and the App Store, leptoskit publish app_store archives, signs, and uploads the app; see Publishing to the stores. The app's version is app.version from leptoskit.toml.

Test it#

Each project includes a UI test, ios/AppUITests.swift, that taps Add one and waits for Count: 1. Update it when you change the home screen, then run it on a simulator:

xcodebuild -project .leptoskit/ios/Host.xcodeproj -scheme App \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
  CODE_SIGNING_ALLOWED=YES CODE_SIGN_IDENTITY=- test

How it differs from macOS#

  • Generated apps also draw Switch, Checkbox, Radio, Slider, LinearProgressIndicator, and ActivityIndicator, alongside View, Text, Button, Pressable, TextInput, PasswordInput, ScrollView, Image, Vector, and Rive. Switches and sliders use UIKit controls; checkboxes and radio options use system-symbol buttons. Control callbacks propose state changes: update the component's controlled value in the callback to keep the new selection or value.
  • In generated apps, tapping outside a text field ends editing without consuming the tapped button's action. Return ends editing and sends the field's submit event; a submit handler can move focus to another field with .focused(...). Dragging a scroll view can dismiss the keyboard interactively.
  • Rive displays native vector frames in a UIImageView. The generated host uses a display link, pauses when backgrounded or clipped offscreen, and follows Reduce Motion. Text, image, layout, script, and audio assets are rejected by the native renderer.
  • iOS has no native checkbox or radio button, so Checkbox and Radio are drawn with UIButton.
  • VoiceOver gets fewer roles than macOS's accessibility API, and no expanded or collapsed state.
  • Sign-in uses the system's passkey sheet, and Sign in with Apple's own sheet for Apple, once your site vouches for the app and the app has the associated domain webcredentials: your site, which you add in Xcode; the generated project doesn't yet. Other providers open in an ASWebAuthenticationSession, never a web view in the app. Passkeys aren't offered in a text field's suggestions.
  • A hardware keyboard or VoiceOver activates a button without showing its pressed style. A touch shows it.
  • Image loads file:// URLs in the generated app; the backend crate also accepts resource:// names from the app bundle. Neither loads web URLs.
  • The generated app keeps your screen inside the safe area, below the status bar and above the home indicator. The strips around it take your root view's background, as a browser paints the page's background across the whole window, and the status bar switches to light text on a dark one. A root without an opaque background leaves the system background. With the backend crate, UIKitHost reports the insets instead, and pt-safe and the other safe-area utilities pad by them.

Ship an update without the App Store#

The App Store build carries the UIKit host, the interpreter, and a copy of your screens. To change the screens afterwards, publish a code push release for iOS:

leptoskit native bundle --target ios --out dist/ios
leptoskit codepush release --from dist/ios --platform ios --channel production

The app downloads the release when the user asks for an update, tries it, and keeps it only if it renders. Anything that needs new native code, such as a new plugin or permission, still needs an App Store release. Code push covers signing, publishing, and rollback, and host projects the update flow inside the app.

A focused(true) request takes effect when the property changes. After the user dismisses the keyboard, unrelated renders do not focus the field again. Set focus to false and then true to request focus again.

esc