From Senior Frontend to Shipping Native Apps

11 min read
careerswiftswiftuicsharpelectronfrontendfull-stackindie-hacking

The Setup

I have been a frontend developer for 11 years. React since 0.14. Next.js since version 9. TypeScript, Tailwind, Zustand, TanStack, Vercel. The full modern web stack. I led frontend teams at a digital agency with clients like Din Tai Fung, The Well, and Burlington. By every reasonable metric, I was a senior frontend developer.

Then I needed to build something the browser could not do.

DropVox, my macOS menu bar app for local AI transcription, needed to run Whisper models on the Neural Engine, access the system menu bar, manage audio devices, and start at login. A web app could not do this. An Electron wrapper could, but shipping Chromium for a menu bar utility that should use 30 MB of RAM felt fundamentally wrong.

So I learned Swift.

And then, for different reasons, I learned C#.

And then I circled back to Electron for a project where it was genuinely the right tool.

This post is about what happened when a senior frontend developer stepped into native development. What skills transferred directly, what skills did not transfer at all, and what surprised me about the experience.

Learning Swift and SwiftUI

What Transferred

The moment I opened SwiftUI documentation, something clicked. I had seen this before.

struct TranscriptionView: View {
    @State private var isTranscribing = false
    @State private var progress: Double = 0

    var body: some View {
        VStack(spacing: 12) {
            if isTranscribing {
                ProgressView(value: progress)
                Text("Transcribing...")
            } else {
                Button("Start") {
                    startTranscription()
                }
            }
        }
        .padding()
    }
}

Compare this to React:

function TranscriptionView() {
    const [isTranscribing, setIsTranscribing] = useState(false);
    const [progress, setProgress] = useState(0);

    return (
        <div className="flex flex-col gap-3 p-4">
            {isTranscribing ? (
                <>
                    <ProgressBar value={progress} />
                    <p>Transcribing...</p>
                </>
            ) : (
                <button onClick={startTranscription}>Start</button>
            )}
        </div>
    );
}

The mental model is identical. A view is a function of state. When state changes, the view re-renders. Compose small views into larger views. Pass data down. Lift state up when siblings need to share it.

@State is useState. @ObservedObject is useContext or a Zustand store subscription. @Environment is React's Context API. The names are different. The pattern is the same.

Component composition works identically. In React, I build <Button>, <Card>, <Modal> components and compose them. In SwiftUI, I build ButtonView, CardView, ModalView and compose them the same way. The hierarchy of views mirrors the hierarchy of components.

Conditional rendering is conditional rendering. Whether it is {condition && <Component />} in JSX or if condition { ComponentView() } in SwiftUI, the concept is the same.

These are not superficial similarities. They represent the same paradigm -- declarative UI programming -- expressed in different languages. Eleven years of thinking in components prepared me for SwiftUI in a way I did not expect.

What Did Not Transfer

There is no npm. This sounds trivial until you need a library. In JavaScript, I run npm install thing and I have it. In Swift, the package ecosystem is smaller by orders of magnitude. Many problems that have five npm packages each have zero Swift packages. You write more code yourself, or you use Apple's frameworks, which are powerful but have steeper learning curves.

Swift Package Manager works well for what it does. But the selection of packages available is a fraction of what npm offers. I found myself writing utilities that I would never write in JavaScript because someone on npm already wrote them better.

Hot reload is not the same. React's hot module replacement lets me change a component and see the result in under a second, in the context of my running application, with all state preserved. Xcode Previews are the SwiftUI equivalent, and they are impressive for what they are, but they are not the same. Previews reset state. They sometimes fail to build. They do not work for all view hierarchies. And when they break, the feedback is a cryptic error in the canvas, not a clear console message.

I spent my first week in Swift expecting the same iteration speed I had in Next.js. I did not get it. I adapted, but the slower feedback loop was the single biggest productivity hit.

Memory management is real. In JavaScript, I allocate objects and the garbage collector handles the rest. In Swift, ARC (Automatic Reference Counting) handles most cases, but retain cycles are a real problem that requires actual thought. When a closure captures self and self holds a reference to the closure's owner, you get a memory leak. The [weak self] pattern is something I type constantly now.

// This leaks memory
engine.onProgress = { progress in
    self.updateProgress(progress) // Strong reference cycle
}

// This does not
engine.onProgress = { [weak self] progress in
    self?.updateProgress(progress)
}

I had never thought about memory ownership in 11 years of frontend development. In Swift, I think about it multiple times per day.

Concurrency is fundamentally different. JavaScript has one thread and an event loop. Everything is non-blocking by convention. Swift has real threads, real parallelism, and the complexity that comes with it. Data races, thread safety, deadlocks -- these are problems that do not exist in JavaScript's single-threaded model.

Swift's actor system is elegant and prevents many concurrency bugs at compile time. But understanding when to use actors, when to use @MainActor, when to use Task, and how Sendable conformance works is a significant learning curve that has no equivalent in frontend development.

What Surprised Me

Speed. Not incrementally faster. Fundamentally different. DropVox's Swift version starts in under a second. The Python prototype took 4-5 seconds. The UI responds to interactions in single-digit milliseconds. Animations are smooth at all times because the main thread is not competing with a JavaScript engine, a garbage collector, and a layout engine.

I knew native apps were faster. I did not understand how much faster until I built one. The difference is visceral. Every tap, every scroll, every transition -- it all feels immediate in a way that even the best-optimized web apps cannot quite match.

The standard library is enormous. Apple's frameworks cover an astonishing range of functionality. AVFoundation for audio, CoreML for machine learning, Security framework for keychain access, UserNotifications for system notifications, ServiceManagement for login items. In the web world, each of these would be an npm package maintained by a random developer. In Apple's world, they are first-party frameworks with (mostly) good documentation and guaranteed platform compatibility.

The community is smaller but deeper. The Swift/macOS development community is a fraction of the JavaScript ecosystem in size, but the average depth of knowledge is higher. When I found answers on Swift forums or Apple Developer Forums, they were typically thorough and correct. The Stack Overflow experience for Swift questions is significantly better than for JavaScript questions, where the top answer is often outdated or wrong.

Then C# and WinUI 3

After shipping DropVox on macOS, I explored bringing the concept to Windows. This meant learning C# and WinUI 3.

XAML is surprisingly familiar. If JSX is "HTML in JavaScript," XAML is "XML for UI." The declarative model is the same. You define views, bind them to data, and compose them hierarchically.

<StackPanel Spacing="12" Padding="16">
    <ProgressBar Value="{x:Bind Progress, Mode=OneWay}"
                 Visibility="{x:Bind IsTranscribing}" />
    <TextBlock Text="Transcribing..."
               Visibility="{x:Bind IsTranscribing}" />
    <Button Content="Start"
            Click="StartTranscription"
            Visibility="{x:Bind IsNotTranscribing}" />
</StackPanel>

The data binding syntax is more verbose than React's JSX or SwiftUI's property wrappers, but the concept is identical. State flows to the view. View dispatches actions to the model. The model updates state. The cycle repeats.

The .NET ecosystem is mature. NuGet packages are well-maintained. The tooling is solid. Documentation is comprehensive. Coming from the JavaScript ecosystem where half the packages have been abandoned and the other half have breaking changes every month, .NET felt refreshingly stable.

WinUI 3 has rough edges. It is Microsoft's modern UI framework for Windows desktop apps, but it is newer and less polished than SwiftUI. Some controls behave unexpectedly. The designer tooling is not as mature as Xcode's Interface Builder or even VS Code's React extensions. The community is smaller than both the Swift and React communities.

I did not ship the Windows version. The market validation was not strong enough to justify maintaining two native codebases. But the experience of learning C# reinforced a pattern: the paradigm is the same everywhere. Declarative UI, state management, component composition. The syntax changes. The primitives change. The thinking does not.

Then Electron for Falavra

After going native for DropVox, I built Falavra as an Electron app. This might sound like regression, but it was a deliberate choice.

Falavra needs sherpa-onnx-node for ONNX-based speech recognition, better-sqlite3 for local database storage, and the ability to shell out to yt-dlp and ffmpeg. These are Node.js dependencies with no Swift or native equivalents. Electron was the only desktop framework that gave me access to the full Node.js runtime in the main process.

And here is the thing: the renderer in Electron is just React. After weeks of wrestling with Xcode, fighting SwiftUI preview failures, and debugging ARC retain cycles, I opened VS Code, wrote function App(), and I was home.

// Main process (Node.js)
ipcMain.handle('transcribe', async (_, audioPath: string) => {
    const recognizer = new sherpa.OfflineRecognizer(config);
    const stream = recognizer.createStream();
    stream.acceptWaveformFromFile(audioPath);
    recognizer.decode(stream);
    return stream.result.text;
});

// Renderer process (React)
const result = await window.electron.transcribe(audioPath);
setTranscription(result);

The IPC boundary between main and renderer processes is the only Electron-specific concept. Everything else is React on one side and Node.js on the other. Both are languages I have written for over a decade.

I shipped Falavra's first version in about half the time it took to ship DropVox. Not because Falavra is simpler, but because I was working in my native language again.

The Common Thread

Here is what I learned from building the same category of application -- desktop software with ML capabilities -- in three different technology stacks:

The paradigm is the same. Declarative UI, component composition, reactive state management. Whether the syntax is JSX, SwiftUI, XAML, or something else, the mental model that frontend developers build over years of React work applies directly.

The primitives are different. File system access, process management, memory ownership, threading models, audio pipelines, system integration. These are not things frontend developers encounter, and they require genuine learning. Not just syntax learning. Conceptual learning.

Frontend skills are more transferable than frontend developers think. I have met many frontend developers who describe themselves as "just a frontend developer" in a tone that implies limitation. After shipping native apps in Swift and C#, I can say with confidence: the skills that make someone a good frontend developer -- understanding UI as a function of state, thinking in components, managing complex async flows, building accessible and responsive interfaces -- are the same skills that make someone effective in any UI framework on any platform.

The platform knowledge is the hard part. Learning Swift syntax took a week. Understanding Apple's framework ecosystem, the signing and notarization pipeline, the App Sandbox, the Neural Engine integration, the actor-based concurrency model -- that took months and is still ongoing. The language is easy. The platform is hard.

The Identity Shift

I used to introduce myself as "a frontend developer." After this year, I stopped.

Not because frontend development is lesser. It is not. Building fast, accessible, responsive web interfaces at scale is genuinely hard engineering work. I am proud of the career I built doing it.

But the label was limiting my thinking. When I called myself "a frontend developer," I unconsciously filtered opportunities through that lens. Can a frontend developer build a native macOS app? Can a frontend developer integrate machine learning models? Can a frontend developer ship commercial software outside the browser?

The answer to all of those is yes, obviously, because the skills transfer. But the label made me hesitate before trying.

I build software now. Sometimes that software is a Next.js web application for a restaurant chain. Sometimes it is a native macOS utility. Sometimes it is an Electron app that wraps ML models. The technology serves the product, not the other way around.

If you are a frontend developer reading this and feeling constrained by the label, here is my advice: pick a project that requires something outside the browser. It does not have to be Swift. It could be a CLI tool in Go, a mobile app in React Native, a desktop app in Electron, a backend service in Node. The point is to discover that your skills are more portable than you think.

The jump is shorter than it looks.


I write about the intersection of frontend development, native apps, and indie hacking at helrabelo.dev. If this resonated, connect with me on LinkedIn.