Built desktop application with SwiftUI and GTK4
This is a collection of unorganized thoughts and stories on my first serious desktop application development.
Applications' code are available at:
Contexts, Motivation
I use Roon for listening to and managing music library. For those unfamiliar,
Roon is music subscription software that organizes your entire music collection — both streaming libraries and personal music files — and lets you play it to audio devices around your home, filling your living space with sound. It provides audiophile-quality playback, detailed music information, simplified multi-room streaming capabilities, extensive sound processing tools, plus exclusive content and features.
For me, it's a music playback / library server with external streaming service integrations built-in.
My Linux mini-PC (Intel N150, 16GB RAM) runs Roon Server, their proprietary software. That mini-PC is connected to audio output devices via USB audio interface.
Roon Apps (their client applications) communicates to the Roon Server and controls playback or lets me browse libraries.

This setup works great but two things had been bothering me: they don't have Roon App for Linux and they don't use platform GUI toolkit for desktop applications.
No Linux client
This was not a problem when I started using Roon. However, after Apple's recent pushes of bloat features to macOS, Linux desktop became my primary development and personal computing machine.
You can probably run Roon App for Windows on Wine. However I don't like running apps on Wine unless absolute necessary.
Custom GUI
Roon Apps is not a "native" desktop application, as it draws GUI using custom framework at OpenGL layer (What is Roon built in? - Roon Software Discussion - Roon Labs Community, Accessibility for blind and visually impaired people - Feedback / Feature Suggestions - Roon Labs Community).
Alien look and feel is okay for me, but behavior not adhering to platform's convention is big no-no. In addition to minor inconveniences, there are major ones such as it can't sync to OS dark mode settings and toggling dark mode requires restarting Roon App.
The final straw that broke the camel's back was, on macOS, it applies hover effect even when the Roon App's window is inactive (clicking just focuses the window, does not perform an element's action.) After countless "what? I pressed the play button!" moment, I decided to ditch the official Roon App for macOS.
Fortunately, Roon have Node.js SDK for programmatic access. We can build an alternative client1, although it lacks features compared to the official Roon Apps. I am a software developer and the SDK's feature set covers my primary usage, so, why not?
Requirements
As this is the first time I write desktop application (besides small toy examples,) requirements are set simple and easy.
- Write a Roon client program that can,
- control playback
- browse library
- Use each platform's native GUI toolkit.
- Swift + SwiftUI for macOS
- Vala + GTK4 + Adwaita for Linux
- Write shared logic (application core) in Zig and use it via C API
If you know C or C API in general, it's obvious to you the last item is neither simple nor easy. And yes, that item didn't go well and I ended up removing it from the list.
Zig core + GTK4 / SwiftUI
The only "official API" is Node.js SDK. As I'm not willing to bundle JavaScript runtime just to communicate to Roon Server, I started to re-implement functionality in Zig. (BTW there are several existing third-party implementations out there, although none of them provide C API to call from Swift and Vala/C.)
While it was not smooth ride and resulting API is far from good, writing an application core in Zig was quite enjoyable and was a great learning experience. Created simple CLI that use the C API then I could use Valgrind to check memory errors. Zig not hiding things really helped me understanding how C API works2 and gradually improving API design.
However, once I completed basic functionality and started to develop actual application logic, everything started to corrupt.
It became unmaintainable mess
There are many issues such as memory management and NUL-terminate strings, but the most prominent one was the inclusion of I/O in application core.
Usage of Roon API can be split into two: server discovery and communication to the server. The former, server discovery, is performed using UDP multicast and broadcast. The latter uses WebSocket and HTTP.
These require specific parameters and headers and whatever, so past me decided to include the actual I/O in the application core. It worked perfectly and brought the nightmare so called multi-threading. I was only used to WebWorkers and similar message based concurrent system, which is close to "shared-nothing" architecture. This multi thread thing turned out polar opposite of that: it destroys everything it touches.
The situation is much worse than the other projects, as the multi-threading contexts penetrates C-API. I had to add locks to every fields an exported function touches. This does not only add code complexity, but also slows down runtime performance noticeably.
And bad things wouldn't stop there: each UI runtime does not work well with the exported I/O events. GTK4 uses GLib event loop and SwiftUI uses actor-based something (Concurrent Swift or whatever.) So, for example, I had to reschedule every incoming WebSocket message to GLib main loop.
At this point, given performance penalties and added code complexity, I couldn't see any advantages of application core approach over implementing same logic for each platforms.
So the rewrite began.
Swift rewrite
Swift rewrite took the first, because I had been using macOS more than Linux at this time period.
Swift's language features such as protocol and extension are very nice to use.
Lack of functionality on reading bytes is not cool, but it's workable.
SwiftUI, beautiful abstraction and great learning experience
SwiftUI is simply great. I don't think I've been ever frustrated on that.
Its API design is clean and intuitive to use. Sane defaults, self-descriptive names and concise documentation. Consumers (API users) value opinionated platform styles over heavy customization might be the key part of this. But still, I believe there is a lot of good wisdom in its API design and I'd recommend everyone who write UI code to play with it at least once.
Building for Apple platform
Although POSIX functions work well on macOS, Apple strongly recommends their own platform library ("Framework" as they call it). This is prominent on Swift because of its Actor-based concurrency (Swift Runtime? Concurrent Swift? I don't know its name.) Apparently those frameworks are well-made and integrates well on SwiftUI project.
For this time, however, the experience was not that great. Finding Roon Server on local network is done via UDP mutlicasting and UDP broadcasting. Apple hates UDP multicast and broadcast so the former support is very limited and the latter is not supported at all. Their suggestion is "Use Bonjour". Huh.
I ended up ignoring UDP broadcast and using SwiftNIO for multicast. Despite it focuses on server-side use case so documentation and APIs for client-side use was not great compared to the server-side counterparts, it does the job well.
Using these Apple frameworks also comes with learning cost. Especially when things like networking, which traditional method (almost) works among OSes, have different abstraction, concept, or life cycle. I'm still struggling WebSocket connection states and lifetime events. The app goes wild after device lock and sleep.
That Network framework is the only framework I used in this project.
So my experience on "build for Apple platform" this time was "being forced to use shitty fragile IDE and being (semi) forced to use weird API. I hate it."
Vala rewrite
After Swift migration is completed, I started Vala rewrite.
Learning Vala is not easy
Vala severely lacks documentation.
The one on the official website is "tutorial" which lightly explains part of language features and part of GLib functionality. Vala Documentation page on GNOME Wiki page has more useful information and lot more readable, but the website has a banner on top saying:
This site has been retired. For up to date information, see handbook.gnome.org or gitlab.gnome.org.
Combined with being a niche language, learning and writing in Vala was painful. I'll probably use Vala if I have to write a GTK app, but I can't recommend Vala to anyone without prior experience.
Go all in GLib ecosystem
While Swift too wants C API to be Swift-friendly, GTK is much worse on that.
GTK widgets are designed to used with GObject, GLib's object system for C. Using non-GObject objects are cumbersome and complex. The best way to write GTK widgets is to utilize GObject's feature fully, such as Property and Signal. For my case, simply porting Zig codebase to Vala was much better and simpler than maintaining C API, VAPI (Vala interface file) and GObject wrapper in Vala.
Don't get me wrong, writing Vala code using GLib and libraries inside GNOME ecosystem was actually great (other than unhelpful documentation that describes API internals without simply stating "what" and "how".) It's just that integrating other C API into GTK application being total nightmare. I'd go all in GLib by default for future projects.
The end
Both macOS (SwiftUI) and Linux (GTK4) Roon Remote are now complete. Although they still have "slight" bugs and problems, they work perfectly fine for my daily usage. I'll add small refactor or fix bugs, but this is now a "finished" project.
Despite the rewrites, I'd consider this a success and am quite satisfied with the result.
- I can browse my library and control playback smoothly.
- The applications have familiar elements and behave well.
- Footprint is small.
And more importantly, I learned a lot through this project.
- The whole process taught me basics of memory management, linking, C API/ABI, a.k.a., low-level stuffs.
- SwiftUI inspired me with its great API design.
- Interacting with WebSocket library in Zig / Vala let me take a look at WebSocket protocol, not just high-level APIs.
- I'm slightly more sympathetic to people reaches for a "solution" like Electron. Still hate Electron apps as a user though.
I won't use GTK or Swift for web projects, but I believe this experience and knowledge will help me design better application architecture / API and write maintainable code.
Do the project outside your domain, it's good.