iOS app development
The Tilecast iOS app is a native home for Tilecast Studio. It handles the things an iPhone or iPad does better than a browser tab, such as switching between servers and native tabs, and it shows your server’s own Studio for everything else. The app isn’t in the App Store yet. You can build it from source with Xcode.
While the app connects to a server or loads Studio, it shows the animated Tilecast mark without the wordmark. Native sheets use the same loading mark. It adapts to light and dark appearance and stays still when Reduce Motion is enabled. See the iOS hosting contract for the rendering behavior.
Where a feature belongs
Section titled “Where a feature belongs”Build a new feature in Studio. The app loads Studio from each server, so a new page, a new editor, or a plugin page works in the app as soon as the server is updated, with no Swift changes and no app release.
Native code is for app chrome, system features like the camera or Share sheet, and a few small, stable workflows. The full rules are in the iOS app architecture.
Navigation comes from Studio
Section titled “Navigation comes from Studio”Use Studio’s shared useConfirm hook for confirmations. It queues overlapping requests in order and uses native iOS alerts for plain text, with the shared web dialog as a fallback. Leaving the component cancels its active and queued requests. See the alert contract for the exact lifecycle.
Once you sign in, the app replaces Studio’s sidebar with native navigation: tabs on iPhone, and a sidebar on iPad. Studio still draws everything else, including its top bar with breadcrumbs, search, notifications, and your account menu.
Studio decides what the app shows. Each navigation destination is metadata on its Studio route: an ID, a translated label, a group, an icon token, an order, and whether it prefers a tab on iPhone. Studio sends the app that list, already translated, and the app just draws it. Swift never has a list of Tilecast pages.
To add a destination, give your Studio route a navigation entry in its route handle. That’s all. Don’t touch apps/ios. After the server updates:
- On iPad, it appears in its sidebar group.
- On iPhone, it appears under More, or as a tab if you mark it
primary. - Tapping it asks Studio to open it, and your React page renders in the same Studio page.
A plugin’s secondary navigation shows up the same way. See Add Studio pages.
The app never changes Studio’s address when you tap a tab. It asks Studio, and React Router decides, so an unsaved-changes prompt still appears and nothing is lost if you stay. If the app and Studio can’t agree on native navigation, for example with an older server, Studio shows its own sidebar as it does in a browser.
The native bridge contract describes the messages exactly.
Detail pages get a native back button
Section titled “Detail pages get a native back button”When you open a page from a list, such as a screen’s details or a Layout, the app shows a native navigation bar with a back button named for the page you came from, and the page’s title. Studio sends this from its breadcrumb trail, so a new page gets it with no app change. Top-level pages have no bar. Studio hides its own breadcrumbs in the app so the trail isn’t drawn twice. In a browser, and in an older app, nothing changes.
Some screens open in a native sheet
Section titled “Some screens open in a native sheet”The app can show a Studio screen in a native iOS sheet with a native title and Close button. Studio still draws everything inside the sheet, so it’s the same React page you’d see in a browser. The screens that do this today are Live stream, the Layout and Playlist previews, a media asset’s details, an Activity incident’s details, and an update deployment’s status. Confirmations, such as “Delete this schedule?”, are native iOS alerts too. In a browser, or in an older app, they open exactly as before: a dialog, a popup, or a side panel.
The sheet is a second, prewarmed copy of Studio that uses the same sign-in as the main one. When you close a sheet, Studio refreshes the page underneath, so a renamed asset shows its new name in the library right away.
To put another screen in a sheet, add a route to presentationRoutes in apps/dashboard/src/App.tsx, and open it with useOpenNativePresentation(). Show your normal dialog when that returns false. Describe the title and buttons with usePresentationChrome(). Don’t touch apps/ios. The iOS app architecture lists which screens fit a sheet and which should stay in Studio.
System features
Section titled “System features”The app connects Studio to a few things only the phone can do. Studio decides when to use each one, and none of them moves a Tilecast screen into Swift. Each is its own bridge capability, so an older app or an older Studio keeps working, and a browser ignores them.
- Haptics. Studio can ask for a standard system haptic:
selection,success,warning,error,start, orstop. It names the meaning, and iOS decides how it feels. CalluseNativeHaptic()after a change that matters, not on every click. - Share. Studio can open the system share sheet with a title, text, and a web address. The app refuses addresses that hold a session or a token. Call
useNativeShare(), and keep your normal web behavior when it returnsfalse. - Deep links.
tilecast-ios://open?installation=<id>&path=<path>opens a path in a server you already added. It carries no secret, and a link for a server the app doesn’t know shows a notice instead of contacting anyone. The path goes through React Router, so an unsaved-changes prompt still appears. - Upload from Photos or Files. In the media upload panel, Choose files opens the system Photos and Files pickers when the app can, and the app uploads with its own credential. Studio still owns the library: it refetches when the upload ends. An older server, or a signed-out app, uses Studio’s browser uploader exactly as before. The app has no media library of its own.
If you add a Studio feature that needs one of these, use the hooks in apps/dashboard/src/native-host/. Don’t touch apps/ios. The iOS app architecture has the exact contracts.
Build and run
Section titled “Build and run”You need Xcode 26 or later. The app’s only packages are Apple’s Swift OpenAPI Generator and its runtime libraries. Xcode fetches the versions pinned in apps/ios/TilecastKit/Package.resolved. The first time you build, Xcode asks you to trust the OpenAPI Generator plugin, which builds the API client from docs/openapi.yaml.
- Open
apps/ios/Tilecast.xcodeproj. - Run the Tilecast scheme on an iOS 26 simulator.
- Tap Add Server and enter your server’s address. For a local development server on the same Mac, use
http://127.0.0.1with your server’s port.
Each server keeps its own sign-in and website data, so you can add several installations and switch between them. On iPhone, server controls are under More. On iPad, they’re at the top of the sidebar. Before you sign in, use the title menu.
To run on a device, copy apps/ios/Config/Local.xcconfig.example to Local.xcconfig and set your Apple team.
The app’s native text uses Geist, the same typeface as Studio. When you set a font in a native view, use Font.geist(.body) or another text style instead of a system font. See Typography for details.
Tabs and the iPad sidebar show the same Lucide icons as Studio’s sidebar. The icons are generated from Studio’s icon mapping, so if you change an icon in NavigationIcon.tsx, run npm run ios:icons:generate and commit the result.
swift testOn the iOS simulator, select TilecastHostTests in the Tilecast scheme. It compiles the same Core tests inside the app so real WebKit pages receive application events. The scheme starts with in-memory server profiles and credentials. A regression test fails if the iOS bridge suite runs without an application host. The separate UI tests still exercise native controls with their loopback fixture server.
To try Photos and Files upload by hand, follow Test media intake by hand. The automated tests replace the pickers with generated files.
The full simulator suite, the localization check, and the architecture check are listed in apps/ios/README.md. CI runs them on an iPhone and an iPad simulator whenever a pull request changes apps/ios/, the native bridge contract, or the OpenAPI contract. The UI tests use a small local stand-in server, so they don’t need a real Tilecast server or the internet.
Sign in
Section titled “Sign in”On sign-in, iOS shows its system authentication consent sheet for the configured server, then opens that server in the system authentication browser. Passwords, passkeys, authenticator codes, and recovery codes stay on the server’s real origin. After you approve Tilecast for iOS, the app receives an ordinary Studio session for that server and, from a current server, a credential for its own API calls. You authorize once for both. The credential stays in the device’s Keychain for that server only, and never reaches Studio’s web page.
If your Studio session expires, the app renews it without asking you to sign in again, as long as the authorization is still active. You can see and revoke Tilecast for iOS in your account’s security settings, like any other authorized app.
Sign Out, in Studio’s account menu or in the app’s server controls, signs the app out of that server completely and revokes its authorization. The app then waits for you to tap Sign In. Removing a server also deletes its credential.
An older server still works: you sign in to Studio as before, without the app’s own credential. See the iOS authentication contract.
Known limitations
Section titled “Known limitations”- Layout and Playlist previews, which Studio opens in a new window, don’t open in the app yet.
- Downloads, such as a settings export, aren’t supported. Use Safari.
- The app has one window.
Server addresses
Section titled “Server addresses”The app accepts the same addresses as Tilecast Players. Servers on the internet need HTTPS with a certificate your device trusts. Plain HTTP works only for local network addresses, such as 192.168.1.20 or tilecast.local, and the app warns you before it saves one.