Build iOS applications with Ring Slint.
- Overview
- Prerequisites
- Building for iOS
- Project Structure
- Asset Management
- Platform Differences
- Deploying
- Troubleshooting
Ring Slint supports iOS with the following characteristics:
| Feature | iOS Support |
|---|---|
| Rendering | GPU-accelerated via Skia (Metal) |
| Min iOS | 14.0 |
| Architectures | arm64 (device), arm64-sim (simulator) |
The following desktop features are not available on iOS:
- File dialogs (
fileOpen,fileSave,folderOpen) - Message boxes (
msgbox,msgboxWarning,msgboxError,confirm,yesno) - Desktop notifications (
notify) - Clipboard operations (
clipboardGet,clipboardSet,clipboardClear) - Global hotkeys (
hotkeyRegister) - System tray (
trayCreate) - Window drag (
windowDrag) - Always-on-top (
windowSetAlwaysOnTop) - Window icon (
windowSetIcon)
-
macOS with Xcode installed
- Xcode Command Line Tools:
sudo xcodebuild -runFirstLaunch
- Xcode Command Line Tools:
-
Rust with iOS targets
rustup target add aarch64-apple-ios # Device rustup target add aarch64-apple-ios-sim # Simulator
-
iOS Simulator Runtime (for simulator testing)
- Install via Xcode:
Settings → Platforms → iOS Simulator - Or manually:
xcrun simctl runtime add <path-to-runtime.dmg>
- Install via Xcode:
-
Optional: XcodeGen (for Xcode project generation)
brew install xcodegen
-
Optional: ios-deploy (for device installation from terminal)
brew install ios-deploy
The iOS project is located at ios/ within the Ring Slint repository.
cd ios/
# Release build for simulator
./build.sh --simulator
# Debug build for simulator
./build.sh --simulator --debug
# Release build for device
./build.sh
# Build and install on device
./build.sh --install
# Build, install, and run on device
./build.sh --run-
Generate the Xcode project:
cd ios/ xcodegen generate -
Open in Xcode:
open RingSlint.xcodeproj
-
Select your target device/simulator and build (⌘B).
cd ios/
# 1. Build for simulator
cargo build --release --target aarch64-apple-ios-sim --bin ring-slint-ios
# 2. Create .app bundle
mkdir -p RingSlint.app
cp target/aarch64-apple-ios-sim/release/ring-slint-ios RingSlint.app/
cp Info.plist RingSlint.app/
cp resources/*.ring RingSlint.app/
# 3. Sign
codesign --force --sign - RingSlint.app
# 4. Install on simulator
xcrun simctl install booted RingSlint.app
xcrun simctl launch booted dev.ring.slint.appFor device builds, use aarch64-apple-ios instead of aarch64-apple-ios-sim.
ring-slint/
├── ios/ # iOS project
│ ├── Cargo.toml # Rust dependencies & build config
│ ├── Cargo.lock
│ ├── Info.plist # iOS app metadata
│ ├── build.sh # Terminal build script
│ ├── build.rs # Cargo build script
│ ├── project.yml # XcodeGen project definition
│ ├── src/
│ │ └── main.rs # Rust entry point
│ └── resources/
│ ├── main.ring # Ring app entry point
│ └── slint.ring # Ring Slint bindings
├── src/
│ ├── rust_src/ # Ring Slint library (shared)
│ └── slint.ring # Source slint.ring bindings
├── examples/
└── docs/
├── IOS.md
└── ANDROID.md
[package]
name = "ring-slint-ios"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "ring-slint-ios"
path = "src/main.rs"
[dependencies]
ring_slint = { path = "../src/rust_src" }
ring-lang-rs = "0.1"
libc = "0.2"
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
strip = trueThe Rust entry point registers the Ring Slint extension and runs main.ring:
use ring_lang_rs::*;
use std::path::PathBuf;
fn main() {
// Register slint extension (static linking)
ring_register_extension(ring_slint::ringlib_init);
let script_path = get_script_path();
let state = ring_state_new();
if state.is_null() {
eprintln!("Failed to create Ring state");
return;
}
ring_state_runfile_str(state, script_path.to_str().unwrap());
ring_state_delete(state);
}
fn get_script_path() -> PathBuf {
// On iOS, the executable is inside AppName.app/
// Resources are in the same bundle directory
if let Ok(exe) = std::env::current_exe() {
if let Some(bundle_dir) = exe.parent() {
let script = bundle_dir.join("main.ring");
if script.exists() {
let _ = std::env::set_current_dir(bundle_dir);
return script;
}
}
}
PathBuf::from("resources/main.ring")
}| File | Required | Description |
|---|---|---|
ring-slint-ios |
Yes | Compiled binary |
Info.plist |
Yes | iOS app metadata |
main.ring |
Yes | Ring app entry point |
slint.ring |
Yes | Ring Slint bindings (copy from src/slint.ring) |
Place your Ring scripts in ios/resources/. They are copied into the .app bundle during build.
resources/main.ring:
load "slint.ring"
oApp = new SlintApp {
loadUIString('
import { Button, VerticalBox } from "std-widgets.slint";
export component App inherits Window {
title: "My iOS App";
callback tapped();
in-out property <string> message: "Hello from Ring on iOS!";
VerticalBox {
padding: 20px;
padding-top: 60px; // Safe area for notch/Dynamic Island
Text {
text: message;
font-size: 24px;
horizontal-alignment: center;
}
Button {
text: "Tap Me!";
clicked => { tapped(); }
}
}
}
', "app.slint")
setCallback("tapped", :onTapped)
show()
run()
}
func onTapped
oApp.setString("message", "Tapped!")On iOS, account for the notch/Dynamic Island by adding top padding:
VerticalBox {
padding-top: 60px; // Clears the notch/Dynamic Island
// ... your content
}if ismacosx() // Also true on iOS
// iOS/macOS specific logic
ok| Category | Available | Not Available |
|---|---|---|
| UI | All Slint components | - |
| Properties | All property methods | - |
| Callbacks | All callback methods | - |
| Timers | All timer methods | - |
| Models | All model methods | - |
| Styles | All styles | - |
| Window | Basic window management | windowDrag, windowSetAlwaysOnTop, windowSetIcon |
| File Dialogs | - | All file dialog methods |
| Message Boxes | - | All message box methods |
| Notifications | - | All notification methods |
| Clipboard | - | All clipboard methods |
| Hotkeys | - | All hotkey methods |
| System Tray | - | All tray methods |
All unavailable methods are excluded at compile time and will cause "Calling Function without definition" errors if called.
# Boot a simulator
xcrun simctl boot "iPhone 16"
open -a Simulator
# Install and launch
xcrun simctl install booted RingSlint.app
xcrun simctl launch booted dev.ring.slint.app- Build for simulator:
./build.sh --simulator - Create zip:
cd ios && zip -r RingSlint.zip RingSlint.app - Upload to appetize.io/upload
- Select ARM simulator build when prompted
You can install on a real device without a paid Apple Developer account using sideloading tools like AltStore or Sideloadly. These tools re-sign the app with your free Apple ID.
- Build for device:
./build.sh - Create an IPA from the
.appbundle:mkdir -p Payload cp -r RingSlint.app Payload/ zip -r RingSlint.ipa Payload rm -rf Payload
- Install using Sideloadly:
- Open Sideloadly and connect your iOS device
- Drag
RingSlint.ipainto Sideloadly - Enter your Apple ID (a free account works)
- Click Start
Note: Free Apple ID signing expires after 7 days. You'll need to re-sideload periodically. A paid Developer account ($99/year) gives 1 year signing.
With a paid Apple Developer account ($99/year), you can sign and install directly:
- Build for device:
./build.sh - Create a provisioning profile in Apple Developer Portal:
- Register your app's Bundle ID (
dev.ring.slint.app) - Register your test device's UDID
- Create a Development provisioning profile
- Register your app's Bundle ID (
- Sign with your certificate and profile:
# Copy provisioning profile into bundle cp ~/Library/MobileDevice/Provisioning\ Profiles/YOUR_PROFILE.mobileprovision RingSlint.app/embedded.mobileprovision # Sign with your development identity codesign --force --sign "Apple Development: your@email.com" \ --entitlements entitlements.plist \ RingSlint.app
- Install via
ios-deploy:ios-deploy --bundle RingSlint.app
Tip: Using Xcode is often easier for device deployment — it handles signing automatically. Generate an Xcode project with
xcodegen generateand build from there.
error: target 'aarch64-apple-ios-sim' not found
Solution:
rustup target add aarch64-apple-ios aarch64-apple-ios-sim/Library/Developer/PrivateFrameworks/CoreSimulator.framework/... No such file
Solution: Run first launch setup:
sudo xcodebuild -runFirstLaunchNo devices available for runtime
Solution: Install iOS Simulator runtime via Xcode Settings → Platforms, or:
xcrun simctl runtime add /path/to/iOS_Simulator_Runtime.dmg-
Check the simulator console:
xcrun simctl spawn booted log stream --predicate 'process == "ring-slint-ios"' -
Verify all
.ringfiles are in the.appbundle:ls -la RingSlint.app/
-
Ensure
main.ringstarts withload "slint.ring".
- Add eprintln! in main.rs for Rust-side debugging
- Use
? "debug message"in Ring code for print debugging - Check bundle contents:
ls -la RingSlint.app/ - Test locally first: Run on local simulator before Appetize
resources/main.ring:
load "slint.ring"
cSlintSource = '
import { Button, LineEdit, VerticalBox, HorizontalBox } from "std-widgets.slint";
export component App inherits Window {
title: "Ring Slint on iOS";
callback greet(string);
callback update-message(string);
callback clear-form();
in-out property <string> greeting: "Enter your name and tap Greet!";
VerticalBox {
padding: 16px;
padding-top: 60px;
spacing: 12px;
HorizontalLayout {
alignment: center;
spacing: 8px;
Path {
width: 24px;
height: 24px;
viewbox-width: 24;
viewbox-height: 24;
fill: #f59e0b;
commands: "M13 2L3 14h9l-1 8 10-12h-9l1-8z";
}
Text {
text: "Ring Slint on iOS";
font-size: 24px;
font-weight: 700;
vertical-alignment: center;
}
}
Rectangle {
vertical-stretch: 1;
Text {
text: greeting;
font-size: 18px;
horizontal-alignment: center;
vertical-alignment: center;
wrap: word-wrap;
}
}
VerticalBox {
spacing: 8px;
name-input := LineEdit {
placeholder-text: "Enter your name...";
font-size: 16px;
}
Button {
text: "Greet";
primary: true;
clicked => { greet(name-input.text); }
}
HorizontalBox {
spacing: 8px;
Button {
text: "Hello";
clicked => { update-message("Hello!"); }
}
Button {
text: "Goodbye";
clicked => { update-message("Goodbye!"); }
}
Button {
text: "Clear";
clicked => { clear-form(); }
}
}
}
}
}
'
oApp = new SlintApp {
loadUIString(cSlintSource, "dynamic://app.slint")
setCallback("greet", :onGreet)
setCallback("update-message", :onUpdateMessage)
setCallback("clear-form", :onClearForm)
show()
run()
}
func onGreet
cName = oApp.callbackArg(1)
if len(cName) > 0
oApp.setString("greeting", "Hello, " + cName + "!")
else
oApp.setString("greeting", "Please enter your name first!")
ok
func onUpdateMessage
cMessage = oApp.callbackArg(1)
oApp.setString("greeting", cMessage)
func onClearForm
oApp.setString("greeting", "Enter your name and tap Greet!")Build and run:
cd ios/
./build.sh --simulator
xcrun simctl install booted RingSlint.app
xcrun simctl launch booted dev.ring.slint.app