Module 1

The mental model shift

You've shipped apps. You understand components, state, async, layout, builds. Roughly 70% of what you know transfers directly. The remaining 30% is where the friction lives, and it's worth naming precisely, because the difficulty is not that native development is harder — it's that it's differently shaped.

What transfers untouched

Declarative UI is declarative UI. SwiftUI's View is Flutter's Widget and React's function component, with the same core insight: describe what the UI should look like for a given state, let the framework figure out the diff. If you understand why you don't manually mutate the DOM in React, you already understand SwiftUI.

Async is async. Swift's async/await is close enough to JavaScript's that you'll write it correctly on day one. Dart's Future maps cleanly too.

Composition, state lifting, unidirectional data flow, the instinct to keep views dumb and logic testable — all of it applies.

What's genuinely new

The compiler is an adversary that's on your side. This is the big one. In TypeScript or Dart, the type system is advisory and the runtime is forgiving; a race condition shows up as a weird bug at 2am. Swift moves an enormous amount of that into compile time. Data races, nil dereferences, and thread-safety violations become build errors. The first week feels like fighting the compiler. Then it flips, and you realise it's been catching things you'd otherwise ship.

Value types are the default. In JavaScript, objects are references and you develop a sixth sense for accidental mutation. Swift inverts this: struct is the default and it copies. This eliminates a whole category of bug and introduces a smaller, more tractable one (knowing when you actually need reference semantics).

The platform is a first-class API surface, not a plugin. In React Native, touching the microphone means finding a package, checking its maintenance status, and hoping. Here, AVAudioEngine is just there — documented, versioned with the OS, and considerably more capable than any bridge wrapper. Spoke has zero third-party dependencies. That's not asceticism; there's nothing worth adding.

Permissions are enforced by the OS, not by you. The user's mic, their keyboard events, their screen — each is gated by a system-level consent mechanism you cannot work around. This is genuinely more restrictive than the web and more restrictive than mobile in some ways. Module 9 covers it.

Hot reload is worse. Be honest with yourself about this one. Xcode Previews are decent for isolated views and useless for anything involving permissions, audio, or system events. You will rebuild. Builds are fast, but it's a real regression from Flutter's workflow and it's the thing you'll miss most.

From Flutter/RN

You knowSwift equivalentWatch out
Widget / JSX componentstruct V: ViewViews are structs — recreated constantly, cheap to make, cannot hold mutable state directly
setState / useState@StateMust live in the view that owns the data
Provider / Redux / Zustand@Observable classFiner-grained: tracks individual property reads
Future / Promiseasync/awaitNearly identical
Calling async from syncTask { }You cannot await in a sync function
(no equivalent)@MainActorThe genuinely new concept
Mutex / careful shared stateactorCompiler-enforced serialisation
null / undefinedOptional (T?)Stricter; must be unwrapped explicitly
package.jsonSwift Package ManagerYou need zero packages for this project
Hot reload#PreviewMeaningfully worse. Accept it.
Spoke never sends your voice, your text, or anything else off this Mac. Not a promise in a privacy policy — a property of the binary.