Get from a fresh Mac to a working on-device LLM chat app in under 30 minutes.
Physical iPhone required. The iOS Simulator has no Metal GPU and cannot run inference. You need an iPhone 15 or later with at least 6 GB of RAM.
⚠️ iOS 17–26 required. iOS 27 beta is not yet supported — the app crashes at launch on the iOS 27 beta (under investigation).
- Prerequisites
- Install Xcode
- Create a free Apple Developer account
- Clone and open the project
- Configure signing
- Enable Developer Mode on your iPhone
- Connect your iPhone
- Build and run
- Download your first model
- Start chatting
- First things to customise
- What's next
- Troubleshooting
- Learning resources
Before you start, make sure you have:
| Requirement | Minimum version | Notes |
|---|---|---|
| Mac | macOS 14 Sonoma | Needed to run Xcode 16 |
| Xcode | 16.0 | Free from the App Store |
| Apple ID | Any | A paid developer account is not required to run on your own device |
| iPhone | iPhone 15 (any model) | 6 GB RAM minimum; physical device only |
| iOS | 17.0 – 26.x | iOS 27 beta is not supported (app crashes at launch) |
| Cable | USB-A to Lightning or USB-C to USB-C | For the initial trust handshake; wireless debugging works afterward |
| Wi-Fi | 2.4 GHz or 5 GHz | For downloading the ≈ 860 MB model file |
| Disk space | ≈ 2 GB free on iPhone | The model + app + system headroom |
Xcode is Apple's free IDE for iOS development. It bundles the Swift compiler, Simulator, Instruments, and the toolchain for signing and deploying to devices.
- Open the App Store on your Mac and search for Xcode, or visit developer.apple.com/xcode.
- Click Get (it's free, but large — around 15 GB).
- After installation, launch Xcode once. It will install additional components. This takes a few minutes.
- Accept the Xcode license agreement when prompted (or run
sudo xcodebuild -license acceptin Terminal).
Command-line tools: If you plan to use the
xcodebuildCLI, also run:xcode-select --install
You need an Apple ID linked to Apple's developer portal to sign and install apps on your own device. A free personal team is sufficient — you do not need the paid $99/year Apple Developer Program to run on a single device you own.
- Open developer.apple.com and sign in with your Apple ID.
- Agree to the Apple Developer Agreement if prompted.
- That's it. Xcode will pick up your account automatically.
Paid program vs free account:
| Free | Paid ($99/year) | |
|---|---|---|
| Run on your own device | ✓ | ✓ |
| Distribute on App Store | ✗ | ✓ |
| Push notifications (production) | ✗ | ✓ |
| App validity before re-signing | 7 days | 1 year |
For development and forking Onyx, the free account is all you need.
→ Learn more: Apple Developer Program
git clone https://github.com/your-org/Onyx.git
cd Onyx
open Onyx/Onyx.xcodeprojXcode opens the project. The first time, it automatically resolves the Swift package dependencies (mlx-swift-lm and swift-transformers). This can take 1–3 minutes on a fast connection.
You'll see a progress spinner in the bottom status bar — wait for it to finish before trying to build.
What are Swift packages? They're Xcode's native dependency manager (similar to npm or CocoaPods). The packages are declared in
Package.swiftand fetched from GitHub automatically.
→ Learn more: Adding Package Dependencies to Your App
iOS apps must be cryptographically signed before they can run on a device. Xcode handles this for you — you just need to set your team.
- In the Xcode Project Navigator (left sidebar), click the Onyx project file (the blue icon at the top).
- Select the Onyx target under TARGETS.
- Click the Signing & Capabilities tab.
- Under Team, open the dropdown and select your Apple ID / personal team.
- Xcode will auto-generate a provisioning profile. If it shows a red error about the bundle ID being taken, change the Bundle Identifier from
kiraa.Onyxto something unique — e.g.com.yourname.Onyx.
Once the team is set and the bundle ID is unique, the signing error disappears.
What is code signing? Apple requires every app to be signed by an identity tied to a developer account. This prevents tampering and lets Apple track which developer built what. Xcode manages certificates and provisioning profiles for you.
→ Learn more: Code Signing Guide
iOS 16 and later requires you to opt in to Developer Mode before Xcode can install apps on your device. You only do this once per device.
- On your iPhone, open Settings → Privacy & Security.
- Scroll to the bottom and tap Developer Mode.
- Toggle it on.
- Tap Restart when prompted.
- After restart, a banner appears — tap Turn On and enter your passcode.
Developer Mode stays enabled until you manually turn it off or restore the device.
→ Learn more: Enabling Developer Mode on a Device
- Plug your iPhone into your Mac using a Lightning or USB-C cable.
- On your iPhone, tap Trust when the "Trust This Computer?" prompt appears, then enter your passcode.
- Your iPhone appears in Xcode's device list (top-left scheme picker).
After the initial USB trust, you can deploy wirelessly:
- In Xcode, go to Window → Devices and Simulators.
- Select your iPhone and tick Connect via Network.
- Unplug the cable. Your device now shows a globe icon — wireless debugging is active.
Wireless requires the Mac and iPhone to be on the same Wi-Fi network. Build times are slightly slower wirelessly, but it's handy for running the app while the phone is away from the desk.
→ Learn more: Running Your App in Simulator or on a Device
- In the Xcode scheme picker (top-left toolbar), make sure your iPhone is selected — not a Simulator.
- Press ⌘R (or Product → Run).
- Xcode compiles the Swift sources, links the MLX frameworks, and installs the app on your device.
First build takes 2–5 minutes because Xcode compiles the MLX Swift packages from source. Subsequent builds are incremental and much faster.
If you see "Untrusted Developer" on your iPhone:
Go to Settings → General → VPN & Device Management, tap your developer account, and tap Trust.
Once the app launches, you'll land directly on the Chat tab.
Onyx ships with one pre-configured model: Llama 3.2 1B Instruct (4-bit). The weights aren't bundled in the app — you download them once, publicly, with no HuggingFace account or token.
- Tap the Models tab (bottom navigation bar, stack icon).
- Find Llama 3.2 1B Instruct (4-bit) — marked "Recommended".
- Tap Download. The download is ≈ 860 MB — connect to Wi-Fi.
- The progress bar starts moving immediately and shows live percentage.
- When the download completes, the model activates automatically — no extra tap needed.
Where does the model go? It's stored at
<AppSupport>/Onyx/Models/mlx-community/Llama-3.2-1B-Instruct-4bit/inside the app's sandbox. You can browse it in the Files app → On My iPhone → Onyx.
What is 4-bit quantisation? The original model weights are stored in float32 or bfloat16. Quantisation compresses them to 4 bits per weight, shrinking a multi-GB model to a fraction of its size with a modest quality trade-off. This is what makes LLMs fit in 6 GB of iPhone RAM.
→ Learn more: mlx-community on HuggingFace — the community that maintains these quantised models.
- Tap the Chat tab.
- You'll see the model name in the navigation bar title area, with a green status dot indicating the model is loaded (or will load on first message).
- Type a message in the text field and tap the send button (or press Return).
- The three-dot thinking indicator appears while the model loads on the first turn (5–15 seconds on iPhone 15).
- Tokens stream in real time as the model generates its response.
Token speed: Expect 15–35 tokens/second on iPhone 15 base, and 30–50+ tokens/second on iPhone 15 Pro / iPhone 16 Pro with A18 Pro.
What is token streaming? Instead of waiting for the full response, the app receives one token (roughly one word or word-fragment) at a time via an
AsyncStream<String>. The UI appends each token to the message bubble as it arrives, giving the typewriter effect.
→ Learn more: AsyncStream
Tap the Settings tab (bottom navigation bar, gear icon). It has two sections:
| Section | What you can change |
|---|---|
| Assistant | System prompt — injected before every conversation |
| Developer | Clear incomplete download cache |
Changes save when you leave the tab. The system prompt takes effect on the next message.
ChatProvider.shared.systemPrompt = "You are a senior Swift engineer. Answer only in Swift code."Every outgoing prompt is logged to the Xcode console by default. Look for:
📨 [Onyx] outgoing prompt — 2026-06-12T...
To disable in code:
OnyxSettings.shared.logPrompts = falseTo simulate a different hardware tier without changing devices, set an environment variable in your Xcode scheme:
- Product → Scheme → Edit Scheme (⌘<)
- Select Run → Arguments → Environment Variables
- Add
CHATM_HARDWARE_TIER=pro(orbase/max/ultra)
This lets you test the memory gate behaviour on a device with more RAM than your smallest target.
Open ChatModelCatalog.swift and append to ChatModelCatalog.all:
ChatModelDescriptor(
id: "mlx-community/gemma-2-2b-it-4bit",
displayName: "Gemma 2 2B Instruct (4-bit)",
family: .other,
approxSizeBytes: Int64(1.5 * 1_073_741_824), // ≈ 1.5 GB
filePatterns: ChatModelCatalog.defaultFilePatterns,
summary: "Google's compact 2B instruction-tuned model."
)Rebuild and the model appears in the Models tab automatically.
Browse available models at huggingface.co/mlx-community — filter by "4bit" for iPhone-compatible sizes.
Conversations reset on restart by design (keeps the skeleton simple). To save and restore them:
// Save — call after each assistant message:
let turns = await ChatProvider.shared.history.turns
let data = try JSONEncoder().encode(turns)
try data.write(to: OnyxPaths.baseDirectory().appending(path: "history.json"))
// Restore — call at app launch:
let saved = try Data(contentsOf: OnyxPaths.baseDirectory().appending(path: "history.json"))
let turns = try JSONDecoder().decode([MLXConversationHistory.Turn].self, from: saved)You can build and run on the Simulator to iterate on UI without a device:
xcodebuild build -project Onyx/Onyx.xcodeproj -scheme Onyx \
-destination 'platform=iOS Simulator,name=iPhone 17'The Simulator launches the app and renders the full UI. Model downloads work too — only chatting hits the Metal unavailability error, because the Simulator has no GPU. This is the expected behaviour.
- Make sure your iPhone is plugged in and unlocked.
- Check that Developer Mode is enabled (Settings → Privacy & Security → Developer Mode).
- Try a different cable — some third-party cables are charge-only.
Go to Settings → General → VPN & Device Management, find your Apple ID entry, and tap Trust.
- iOS 27 beta? Onyx does not yet support the iOS 27 beta — it crashes before the app's own code runs. Use a device on iOS 17–26 until this is resolved.
- Otherwise, open the Xcode console (View → Debug Area → Activate Console) and look for the crash log. Common causes:
- Metal unavailable — you accidentally ran on a Simulator; switch to your physical device.
- Signing error — rebuild after fixing the team/bundle ID.
- The downloader logs events to
<AppSupport>/Onyx/Models/.cache/download-log.txt. View it via Files app → On My iPhone → Onyx → Models → .cache. - Downloads are resumable — kill the app and relaunch, then tap Download again from where it left off.
- If HuggingFace is slow, try again later. The
mlx-communityrepos are public and rate-limit burst downloads.
This usually means the model's chat template wasn't applied correctly. Check that MLXConversationHistory.buildMessages(systemPrompt:) is being called and the messages array is non-empty before generation. Look in ChatProvider.swift → buildGenerationStream().
Swift package resolution can get confused after dependency updates. Try:
# In Terminal, from the project root:
xcodebuild -project Onyx/Onyx.xcodeproj -scheme Onyx \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-resolvePackageDependenciesOr in Xcode: File → Packages → Reset Package Caches.
| Resource | What you'll learn |
|---|---|
| The Swift Programming Language | Swift syntax, types, generics, optionals — the official book, free online |
| Swift Concurrency | async/await, actors, AsyncStream — essential for understanding Onyx's architecture |
| SwiftUI Tutorials | Building declarative UIs — official Apple tutorial series |
| @Observable | The new observation framework used by ChatProvider (iOS 17+) |
| Running on a Device | Provisioning, wireless debugging, Xcode device manager |
| Enabling Developer Mode | The iOS 16+ opt-in step |
| Resource | What you'll learn |
|---|---|
| mlx-swift-lm on GitHub | The Swift package powering Onyx's inference engine |
| swift-transformers on GitHub | HuggingFace tokenizers in Swift — handles chat templates |
| mlx-community on HuggingFace | Catalogue of iPhone-compatible 4-bit quantised models |
| Apple Metal | The GPU API MLX uses under the hood on Apple Silicon |
| Core ML vs MLX | Why Onyx uses MLX instead of Core ML for LLMs |
| Resource | What you'll learn |
|---|---|
| AsyncStream | How token streaming works in Swift |
| Increased Memory Limit Entitlement | The entitlement that lets Onyx hold a 2 GB model on 6 GB devices |
Clone git clone … && open Onyx/Onyx.xcodeproj
Sign Targets → Signing & Capabilities → set Team + unique Bundle ID
Trust iPhone: Settings → Privacy & Security → Developer Mode → On
Build ⌘R (device selected in scheme picker)
Trust app Settings → General → VPN & Device Management → Trust
Download Models tab → Download (auto-activates when done)
Chat Chat tab → type message → send
Log Xcode console: 📨 [Onyx] lines
Add model ChatModelCatalog.swift → append ChatModelDescriptor to .all