The sketch on the phone
A sketch written for the Mac runs on an iPhone or an iPad without changes. It uses the same Metal renderer, the same draw() at the display's refresh rate, and the same shaders compiled from source at launch. A finger is the pointer, so mouseX, mouseY, and mouseIsPressed read the touch with no change, and a stylus fills pressure.
You work on it with one command:
ollin phone Sketch.swift
The sketch appears on the phone, and every save on the Mac installs the new version there. The phone's parameters open in a browser on the Mac, and changes travel in both directions. Stop the command when you are done, and the app stays on the phone.
Contents
- What a save does
- The parameters on the Mac
- What comes along
- The options
- In an app of your own
- Without installing it
- What it needs
What a save does
A phone runs only code that is signed inside its app bundle. Nothing can be pushed into a running app the way the live window swaps a sketch on the Mac. So a save is a small build and a reinstall, and the times are short enough for that to work:
| Step | Time |
|---|---|
| The framework, built for the phone | about 80 s, once |
| A save: the sketch recompiled, the app relinked and signed | 5 to 7 s |
| The app installed again, over Wi-Fi | about 2.5 s |
A save reaches the screen in under ten seconds. The app writes its state down every second and reads it back when it launches. That state is the clock, the seed, every @Param value, and every @Saved property. So a reinstall carries on mid-motion. An animation keeps its phase, and a value you tuned stays tuned. The phone keeps the same behavior as --keep-clock on the Mac.
The phone has to meet two conditions. It must be unlocked for the Mac to open the app. If it is locked, the new version is installed and waits for a tap. It must also be reachable, either on the cable or awake on the same network as the Mac.
The command prints a compile error with your own file and line, and the phone keeps running the last version until the next save.
The parameters on the Mac
The app serves its @Param parameters through the remote surface, which is the same touch surface you tune an installation from. The command prints the address and opens it:
parameters http://localhost:9330 or http://Edgardos-iPhone.local:9330
Over the cable the surface is forwarded to localhost, so no network is needed between the two. Over Wi-Fi the address is the phone's own name. Drag a slider on the Mac and the phone follows between frames. Tune something on the phone and the Mac shows it. A value you settle on is carried across every later save, because it is part of the state the app writes down. To put it into the file, type it in.
Shaders in a .metal file beside the sketch come along as resources, so a shader edit is one more save.
What comes along
The sketch file is referenced where it is, never copied. Files beside it with a resource extension are copied into the app. That covers images, sounds, video, fonts, data, meshes, and .metal shaders. Bundle.module is the app, so loadImage("photo.png", in: .module) finds on the phone what it found on the Mac. Drop a new file beside the sketch and the next save takes it along.
Every import Ollin… line links that satellite into the app. Ten have been built and run on a phone: OllinRemote, OllinPhysics, OllinOSC, OllinMIDI, OllinController, OllinVideo, OllinBluetooth, OllinAudio, OllinVision, and OllinHaptics. A satellite outside that list is linked too, and the command names it, so you know what to check first if the build fails. Five satellites work only on the Mac, so the command refuses them and gives the reason. They are the virtual camera, Syphon, screen capture, and both ends of the phone-as-sensor link, because a phone cannot be its own peripheral.
The Mac features stay on the Mac. They are the export flags, the screen saver, the wallpaper, the menu-bar strip, the display wall, the detached inspector panel, and the source-editing drag. A sketch that uses none of them is portable as written.
The options
ollin phone Sketch.swift the loop: build, install, launch, watch
ollin phone Sketch.swift --once build, install, launch, and stop
ollin phone Sketch.swift --fresh start the sketch over, state forgotten
ollin phone Sketch.swift --device iPad which paired device, by part of its name
ollin phone Sketch.swift --team ABCDE12345
ollin phone --devices the paired devices
The signing team comes from the --team flag first, then from OLLIN_TEAM in the environment. If neither is set, the team is the one the reference phone app in the checkout is signed with. The project the command writes lives under ~/Library/Caches/Ollin/Phone/, with one folder per sketch. The sketches share one build folder, so the framework compiles for the phone only once.
In an app of your own
The command writes a throwaway app around the sketch, under the caches folder. For an app you keep and hand out, the project generator writes the same shape as a project of its own:
ollin new Rings --kind ios-app --team ABCDE12345
cd Rings && xcodegen generate && open Rings.xcodeproj
The folder holds the sketch, a host that owns the entry point, the property list, and the project.yml the Xcode project is written from. The .xcodeproj is never edited by hand and is not kept in git. Leave the team off and Xcode asks for it the first time you press Run, but what Xcode writes is lost when the project is generated again, so the README says to put it into project.yml. Every template fits, and so does any example that stays off the Mac-only libraries: a capability that lives on the Mac (Syphon, screen capture, the virtual camera, the phone link) is refused with its reason. A .metal file beside the sketch is copied by a copy-files phase, because the resources phase would compile it into a library the framework never reads. The generator window offers the same kind from its menu, with a field for the team.
Apps/OllinSketchApp in the checkout is the same shape, written by hand. It is an Xcode project with the framework as a local package, plus a host that puts the sketch in a SketchView.
import SwiftUI
import Ollin
@main
struct MyApp: App {
// `Scene` is spelled out: the framework has a 3D `Scene` of its own.
var body: some SwiftUI.Scene {
WindowGroup {
SketchView(Waves(), onRunner: { runner in
// Optional: the state written down and read back at launch.
runner.beginInstallation(Installation(checkpoint: .every(seconds: 30)))
})
.ignoresSafeArea()
.statusBarHidden()
}
}
}
The app owns the entry point, so @main goes on the App rather than on the sketch. The command's own host calls beginInstallation with a checkpoint every second. But a sketch that runs on a tablet in a room can use a checkpoint every minute instead.
Without installing it
There is a second way to put a sketch in your hand, and it installs nothing. The sketch keeps running on the Mac, and the phone shows its frames:
import Ollin
import OllinPhone
final class Held: Sketch {
let device = PhoneDevice()
override func setup() {
device.show(self)
}
}
The Mac compresses each frame as video and sends it down the cable to Ollin Capture, which shows it full screen. The first finger on the picture is the sketch's pointer, just as it is on this page. So mouseX, mouseY, mouseIsPressed, and pressure read the same either way. Two fingers on the shown picture also pinch and turn the sketch through pinchScale and turnAngle, as they do on the Mac's trackpad. A sketch installed on the phone reads one finger only.
The two answer different questions. Showing the picture tells you how the sketch looks and plays in the hand. An edit appears as soon as the Mac has compiled it, through the live window's own reload. Installing tells you whether the phone's own GPU keeps up, and that the sketch runs with no Mac at all. Both suit a sketch in progress: showing it while you shape it, installing it when you want to know it holds.
What it needs
- iOS 26 on the phone or tablet. The device is paired with the Mac and trusted, with Developer Mode on.
- Xcode 26, and
xcodegen(brew install xcodegen), which writes the project. - A signing team the device is registered with. The first build asks Xcode to update the profile.
- A device, not the simulator. The simulator's GPU reports itself as an older family with no cube-array textures, and the renderer binds one on every frame.