The Mental Model: Five Boundaries Behind One Install Button
For iloader, the hard design problem is not drawing an installer screen; it is deciding what crosses from that screen into native code, what state remains available afterward, and how the result returns to the user. This chapter focuses on that responsibility model. Once you can trace a request through the React interface, Tauri commands, managed Rust state, event updates, and external services, you can understand why iloader behaves differently from a simple file copier—and why transient ownership matters when more than one action is possible.
From a button to a set of responsibilities
The visible interface is a useful starting point because it reveals the jobs iloader chooses to expose. The main screen groups the account, device, management, installer, and settings surfaces. The account area deals with Apple ID login and saved login entries. The device area lists connected devices and identifies the active one. Management opens views for pairing files, certificates, and App IDs. Installer buttons choose a SideStore or LiveContainer build, or open a file picker for an IPA. Settings hold choices such as the anisette server, language, keyring behavior, and logs.
These controls are not themselves the protocol implementation. They collect a small set of user decisions, check obvious prerequisites, and invoke named native commands. A user pressing an installer button does not cause browser code to speak directly to usbmuxd or to implement Apple's developer services. The request crosses the Tauri boundary, where Rust code can use the device communication and sideloading libraries, access the operating system's storage facilities, and coordinate the longer operation.
That division gives each side a different kind of responsibility. The React side owns immediate interaction state: which modal is open, which device appears selected, which operation steps have started, and which preference the user has chosen. It also owns presentation logic such as toasts, confirmation dialogs, localized labels, and the choice to show a short error or its full details. The native side owns the state that represents capabilities outside the browser: the active sideloader created from an Apple developer session, the selected device together with its pairing bytes, and the cancellation token for an in-flight pairing request.
The distinction is easy to miss because the two sides use the same words. “Logged in” in the interface is a display state, but the native process holds the sideloader that later commands need. “Selected” in the interface is a display state, but the native process commits the device only after pairing generation succeeds. “Installing” in the interface is a modal state, but the native process is the side that downloads an asset, invokes the sideloading library, and reports a phase transition. A robust mental model follows the state that can make the next action possible, not only the label currently visible on the screen.
The relationship can be represented like this:
This is a responsibility map, not a claim that each box is an independent process. The source registers native commands for accounts, devices, sideloading, pairing, certificates, App IDs, storage controls, and cancellation. It also wires Tauri plugins for dialogs, stores, process control, URL opening, and updates. The diagram groups those details by what they do at the boundary. The important question is always: what information crosses, and who remains responsible for its meaning?
For example, the interface sends an email, password, anisette-server choice, and save-credentials decision for a new login. Native code turns that request into an account login, handles the two-factor callback, creates a developer session, and stores a configured sideloader. The interface receives success or a typed error; it does not receive authority to sign merely because an email field was filled in. Similarly, the interface sends an IPA path for arbitrary import, but the native side validates that a device and sideloader are available before invoking the delegated installer.
This split also limits what can be concluded by inspecting the iloader interface. The source clearly shows when it calls the idevice and isideload integrations. It does not show the internal protocol guarantees of those crates or the policies enforced by Apple's services. The native boundary is therefore both a capability boundary and an evidence boundary: it tells us where iloader coordinates work, but not that every dependency will accept the request or that every external policy will remain stable.
State is the control flow
A long operation needs more than a function call because its parts have different owners and different failure consequences. iloader represents the active sideloader, selected device, and pairing cancellation token in managed native state. That choice turns state ownership into a control-flow mechanism. A command can ask whether a device exists, take the sideloader needed for mutable work, perform an asynchronous operation without holding the shared mutex across network calls, and restore the sideloader when the guard is dropped.
Temporarily taking the sideloader is not just an implementation trick. It expresses an exclusivity rule: the same mutable sideloader should not be handed to two operations at once. The guard removes it from shared state, so a second command that tries to take it during that interval receives a not-logged-in error. That error name is not a perfect description of the real situation—the account may still be valid—but it is the available signal for “the sideloader is not currently obtainable.” The design protects the object from simultaneous mutable use, while leaving the interface without a dedicated “busy” state in this path.
That trade-off is worth understanding. Keeping a lock held for the full duration of a device or network operation could block unrelated access and make the state harder to manage. Taking the object out allows the asynchronous call to proceed without a long-held mutex, but it creates a transient absence visible to competing commands. A more elaborate design could queue operations or report busy explicitly. iloader's observed design is narrower: obtain exclusive access, do the work, and restore it on exit. The benefit is a clear ownership boundary; the cost is that concurrency behavior is expressed through an error rather than through scheduling.
The selected device follows a related lifecycle. The interface can list several devices, but native selection does not immediately replace the active state. It obtains the provider and generates pairing material first. Only when that work returns successfully does it store the device information and pairing bytes together. If pairing fails or is canceled, the interface clears its waiting state and does not present the failed choice as active. This prevents a displayed device from implying that the later installation path has everything it needs.
The cancellation token adds another transition. When a new device selection begins, the native code replaces the existing token and cancels the older one. The pairing operation checks that token while generating the lockdown and remote-pairing material. The interface can also invoke cancellation from the pairing modal. Cancellation is useful here because pairing may wait for the user to unlock the device and accept a Trust prompt, but it should not be confused with rollback. If an external service has already created a record or a file has already been transferred, stopping the waiting task does not automatically reverse that side effect. The source demonstrates cancellation of the in-flight pairing path, not a transaction spanning the whole device.
The frontend has a separate defense against late results. Each selection receives a request number. If the user starts another selection before the first one completes, a success or failure from the older request is ignored when its number no longer matches the current request. This prevents a slow response from changing the screen back to an earlier device. It does not cancel every possible native action by itself; that is the cancellation token's job. Conversely, native cancellation does not by itself explain how a UI should handle a response that arrives after the user's intent has changed. iloader needs both because the race exists in two places.
Operation events make these state transitions readable. Native code creates an operation with an identifier and emits a started, finished, or failed update for each defined step. The frontend subscribes to the corresponding event, adds step IDs to its local started and completed lists, and records failures with their error details. The bundled installer therefore has a visible sequence of download, installation, and pairing, while arbitrary IPA import has a single installation step. A failed download can leave installation and pairing visibly unstarted instead of leaving the user to infer what happened from a generic error dialog.
This is useful progress, but it is deliberately limited progress. The source does not report the number of bytes downloaded or a percentage for the sideload call. The interface knows that a phase started and whether it finished; it does not receive a durable job record that could reconstruct the operation after the application exits. Nor does an event imply that an external side effect is reversible. A “finished” pairing step means that iloader's awaited function returned successfully, not that every later action in the companion app is guaranteed to work.
State names and event transitions are therefore more informative than a catalogue of components. “Account authenticated,” “device selected with pairing,” “sideloader temporarily owned,” and “pairing phase failed” describe conditions that change what the user can do. A list of screens can tell us where a control is drawn, but it cannot tell us whether the native capability behind the control exists, whether another operation currently owns it, or whether a failure occurred before or after an external side effect.
This model also explains why iloader repeats some checks at different layers. The interface refuses to start an install without a logged-in account and selected device, giving the user immediate guidance. Native functions check their own state as well: the sideload path returns a no-device-selected error when no device is committed, and the SideloaderGuard returns not logged in when no sideloader is available. The duplication is valuable because a command boundary should not rely only on a particular screen having behaved correctly. A caller, a stale UI, or a future interface can reach the command through a different path, so the native side still has to protect its assumptions.
The result is a system that is easier to operate precisely because it does not pretend that all state is the same. Preferences can be loaded from a store. Account labels can be remembered. A developer session can exist only in the running process. Pairing material can be cached and also held with the selected device. An operation can be active but not durable. Each kind of state has a different owner, lifetime, and failure response.
The provider structure in the interface reinforces those lifetimes. A preference store is initialized before the main application is rendered, so controls such as the anisette server and language can read a consistent starting value. The error and dialog providers centralize interactions that must interrupt the current flow, while the log provider accumulates native records for a later inspection view. These are not interchangeable state containers. A preference can be changed without contacting a phone; a selected-device change can trigger pairing work; an error modal can describe a failed operation without changing the underlying account or device state. Treating them as one global “app state” would make it harder to know whether a retry needs a new request, a fresh login, or merely a clearer explanation.
The command boundary also acts as a second line of defense when the interface and native state diverge. Suppose a user starts an install, then changes the selected device or dismisses a screen. The interface may update immediately, but the native operation still has its own references and event channel. Precondition checks at the screen provide quick feedback, while checks inside the Rust functions prevent a command from proceeding without the device or sideloader it requires. That does not solve every race—an external service can still respond late, and a completed side effect cannot be rewound by changing a screen—but it keeps an invalid UI assumption from becoming an unchecked native request.
This is also why names for states and transitions are more useful than a list of screens. “Logged in” can mean that the UI displays an email, or that the native process holds a usable developer session. “Selected” can mean that a card has focus, or that device communication and pairing have completed. “Installing” can mean that a modal is open, or that a particular native phase has emitted a started event. When those meanings are separated, failures become easier to place: an unavailable sideloader is an ownership problem, a missing device is a prerequisite problem, and a late event is a synchronization problem. iloader's design does not remove those categories, but it gives them different places to appear.
Once those distinctions are visible, the next question is no longer “which button installs SideStore?” It is how the selected device becomes trusted enough for that command to use, and how iloader prevents a merely visible device from being mistaken for a usable one.