makoni/swift-adwaita
An imperative Swift 6 wrapper for [GTK4](https://docs.gtk.org/gtk4/) and [libadwaita](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/latest/), designed for building native GNOME desktop applications.
Apps built with swift-adwaita
Demo app
<img alt="Swift Adwaita" src="https://spaceinbox.me/images/swift-adwaita-demo.gif">
Features
- Imperative API — no declarative DSL; create and configure widgets directly
- 176 widget wrappers — 74 auto-generated Adwaita + 102 hand-written GTK widgets
- Zero raw pointers in public API — all
OpaquePointer/gpointerhidden behind Swift types - Type-safe enums —
SignalName,PropertyName,CSSClass,IconNameinstead of raw strings - Fluent setters — method chaining:
Label("Hi").halign(.center).cssClass(.title1) - Type-safe signals — 50+ signal signatures with
@MainActorclosures - Async/await —
FileDialog.open(),UriLauncher.launch(),Clipboard.readText() - Keyboard shortcuts — enum-based
Key+KeyModifiersAPI - Property bindings —
GObjectRef.bind()for reactive connections - Container protocol — unified
append()/remove()for Box, ListBox, FlowBox, WrapBox, Carousel - Convenience initializers —
SwitchRow(title:),PreferencesGroup(title:description:), etc. - Menus & actions —
GMenuRef,SimpleAction,MenuButton - Drag & drop —
DragSource,DropTarget - CSS support —
CSSProvider+ type-safeCSSClassenum - Animations —
TimedAnimation,SpringAnimationwith callbacks - Drawing —
DrawingAreawithCairoContextwrapper - Text attributes —
TextAttributesfor styling entry text (bold, italic, color) - Media playback —
MediaStream,Video,MediaControls - Localization — gettext integration via
localized()andString.localized - @Setting property wrapper — type-safe GSettings binding
- Adaptive layout —
Breakpoint.minWidth(),Breakpoint.maxWidth()helpers - Swift 6 concurrency — full
@MainActorisolation,Sendabletypes - 1139 tests, 78 demo examples, CI with GitHub Actions
Requirements
- Swift 6.2+
- libadwaita 1.5+ development headers
- GtkSourceView 5 development headers
- Linux
Ubuntu/Debian
sudo apt install libadwaita-1-dev libgtksourceview-5-devFedora
sudo dnf install libadwaita-devel gtksourceview5-develInstallation
Install the system packages above first, then add this package to your Package.swift:
dependencies: [
.package(url: "https://github.com/makoni/swift-adwaita.git", branch: "main"),
],
targets: [
.executableTarget(
name: "MyApp",
dependencies: [
.product(name: "Adwaita", package: "swift-adwaita"),
]
),
]Quick Start
import Adwaita
@MainActor
func buildApp() {
let app = Application(id: "com.example.HelloWorld")
app.onActivate {
let window = ApplicationWindow(application: app)
window.title = "Hello"
window.defaultWidth = 400
window.defaultHeight = 300
let box = Box(orientation: .vertical, spacing: 12)
box.setMargins(24)
let label = Label("Hello from swift-adwaita!")
.cssClass(.title1)
box.append(label)
let button = Button(label: "Click Me")
.cssClass(.suggestedAction)
.cssClass(.pill)
.halign(.center)
button.onClicked {
label.text = "Button clicked!"
}
box.append(button)
window.setContent(box)
window.present()
}
app.run()
}
buildApp()Architecture
Three-layer design:
CAdwaita System library (pkg-config: libadwaita-1)
│
GObjectSupport GObject lifecycle (ARC), signals, Variant, GValue
│
Adwaita Widget wrappers (74 generated + 90 hand-written)Key Types
| Type | Description | |------|-------------| | GObjectRef | Base class — GObject lifecycle with ARC | | Widget | Base for all GTK/Adwaita widgets | | SignalConnection | Handle for signal connections | | Application | App entry point (AdwApplication) | | ApplicationWindow | Main window |
Protocols
| Protocol | Purpose | Conforming Types | |----------|---------|-----------------| | ListModelConvertible | Pass models to list views | ListStore, StringList, FilterListModel, SortListModel, MapListModel, FlattenListModel, TreeListModel, SelectionFilterModel | | SelectionModelConvertible | Pass selection to views | SingleSelection, MultiSelection, NoSelection | | Container | Widgets with append/remove | Box, ListBox, FlowBox, WrapBox, Carousel | | Swipeable | Swipe gesture target | Carousel, NavigationView, OverlaySplitView |
Type-Safe Enums
| Enum | Replaces | Example | |------|----------|---------| | SignalName | "clicked" | .clicked, .changed, .notify("title") | | PropertyName | "active" | .active, .title, .custom("my-prop") | | CSSClass | "suggested-action" | .suggestedAction, .pill, .title1 | | IconName | "go-next-symbolic" | .goNext, .dialogError, .custom("my-icon") |
Widget Categories
Layout: Box, Stack, Grid, Overlay, FlowBox, Clamp, Paned, WrapBox, CenterBox, Fixed
Navigation: NavigationView, NavigationSplitView, OverlaySplitView, TabView, ViewSwitcher, Notebook, Carousel
Input: Button, Entry, Switch, CheckButton, ToggleButton, Scale, SpinRow, SearchEntry, DropDown, Calendar, ToggleGroup
Display: Label, Image, Picture, Spinner, ProgressBar, LevelBar, Avatar, Banner, Separator, Video
Lists: ListBox, ActionRow, ExpanderRow, ComboRow, SwitchRow, ButtonRow, PreferencesGroup
Virtualized Lists: ListView, GridView, ColumnView + ListStore, StringList, SignalListItemFactory, TreeListModel, FilterListModel, SortListModel
Containers: ScrolledWindow, ToolbarView, HeaderBar, BottomSheet, Frame, Expander, Revealer, ActionBar
Dialogs: AlertDialog, Dialog, AboutDialog, PreferencesDialog, FileDialog
Menus: MenuButton, PopoverMenu, PopoverMenuBar, SplitButton, GMenuRef, SimpleAction
Event Controllers: GestureClick, GestureDrag, GestureLongPress, GestureSwipe, EventControllerKey, EventControllerMotion, EventControllerScroll, EventControllerFocus, DragSource, DropTarget, ShortcutController
Feedback: Toast, ToastOverlay, EmojiChooser
Styling: CSSProvider, CSSClass, StyleManager, TextAttributes
Animation: TimedAnimation, SpringAnimation, CallbackAnimationTarget, PropertyAnimationTarget
Media: MediaStream, Video, MediaControls
Drawing: DrawingArea, CairoContext
System: Clipboard, Display, Monitor, UriLauncher, Settings
Examples
Fluent Setters
let label = Label("Welcome")
.halign(.center)
.vexpand()
.margins(24)
.cssClass(.title1)
.tooltip("A greeting label")
let button = Button(icon: .goNext)
.cssClass(.suggestedAction)
.cssClass(.circular)Type-Safe Icons and CSS
let img = Image(icon: .dialogInformation)
let btn = Button(icon: .documentSave, onClicked: { print("Saved!") })
label.addCSSClass(.dimLabel)
list.addCSSClass(.boxedList)
button.addCSSClass(.destructiveAction)Dialogs, clipboard, URI launching
Every async-looking surface in swift-adwaita ships in two shapes:
- A callback form —
…(parent: window) { result in … }. The closure runs on the main actor from the GLib main loop. Use this inside a running GTK application (any handler called fromonClicked/onActivate/ a GTK signal in general). - An
asyncform —try await …. Convenient in tests, macOS CLIs, or anywhere something else is draining Swift'sDispatchQueue.main. Don't use it inside ag_application_runapp.
Why the split
Swift's default MainActor executor is DispatchQueue.main, and the GLib main loop does not drain it — so a Task { @MainActor in await dialog.open(...) } kicked off from a button click just sits there and the dialog never appears. The callback forms side-step Swift Concurrency entirely and go through a GLib-native GAsyncReadyCallback, which GLib's loop does dispatch.
Callback form (prefer this in GTK apps)
let dialog = FileDialog()
dialog.title = "Open a File"
openButton.onClicked {
dialog.open(parent: window) { result in
switch result {
case let .success(path?): print("Selected: \(path)")
case .success(nil): print("User cancelled")
case let .failure(error): print("Error: \(error.message)")
}
}
}
// Clipboard — same idea, no Result wrapping because there's no error domain.
widget.clipboard.readText { text in
label.text = text ?? ""
}
// URI launcher.
UriLauncher(uri: "https://gnome.org").launch(parent: window) { success in
print("Launched: \(success)")
}The same shape is available on FileDialog.save/selectFolder, ColorDialog.chooseRGBA, FontDialog.chooseFont, Clipboard.readTexture, and Texture.load(from:completion:).
Async form (tests / non-GTK)
let path = try await dialog.open(parent: window) // ok in XCTest, don't do this inside onClicked.Adaptive Layout
let bp = Breakpoint.maxWidth(500)
bp.addSetter(box, property: .custom("orientation"), value: "vertical")
bp.onApply { sidebar.visible = false }
bp.onUnapply { sidebar.visible = true }
window.addBreakpoint(bp)Drawing
let da = DrawingArea()
da.contentWidth = 200
da.contentHeight = 200
da.setDrawFunc { cr, width, height in
cr.setSourceRGB(0.2, 0.4, 0.8)
cr.roundedRectangle(x: 10, y: 10, width: 180, height: 180, radius: 20)
cr.fill()
}Keyboard Shortcuts
button.addKeyboardShortcut(key: .s, modifiers: .control) {
print("Save!")
return true
}
widget.addKeyboardShortcut(key: .z, modifiers: [.control, .shift]) {
print("Redo!")
return true
}Menus & Actions
let menu = GMenuRef()
menu.append("Cut", action: "win.cut")
menu.append("Copy", action: "win.copy")
let menuBtn = MenuButton()
menuBtn.iconName = "open-menu-symbolic"
menuBtn.setMenuModel(menu)
let action = SimpleAction(name: "cut")
action.onActivate { print("Cut!") }
window.addAction(action)Localization
setTextDomain("myapp")
let greeting = localized("Hello")
let label = Label("Welcome".localized)Virtualized Lists
var items = ["Apple", "Banana", "Cherry"]
let store = ListStore()
for _ in items { store.appendPlaceholder() }
let factory = SignalListItemFactory()
factory.onSetup { listItem in
listItem.child = Label("")
}
factory.onBind { listItem in
listItem.child?.cast(Label.self).text = items[listItem.position]
}
let selection = SingleSelection(model: store)
let listView = ListView(model: selection, factory: factory)Drag & Drop
let drag = DragSource()
drag.setTextContent("Hello!")
sourceWidget.addController(drag)
let drop = DropTarget.forText()
drop.onDrop { text in
if let text { label.text = text }
return true
}
targetWidget.addController(drop)Custom CSS
CSSProvider.loadGlobal("""
.my-widget {
background: linear-gradient(135deg, @accent_bg_color, @headerbar_bg_color);
border-radius: 12px;
padding: 24px;
}
""")
widget.addCSSClass("my-widget")Demo App
An interactive gallery with 78 examples showcasing every widget:
swift run DemoAppFeatures sidebar navigation with search, source code viewer, and windowed demos for navigation/window-level widgets.
Building
swift build # Build library
swift test --no-parallel # Run the test suite safely for GTK/libadwaita
swift run DemoApp # Launch demo galleryDistribution with Flatpak
Flatpak is the recommended way to distribute GTK4/libadwaita apps on Linux. The GNOME runtime provides GTK4 and libadwaita, and the Swift SDK extension provides the compiler — your app only ships its own binary.
Prerequisites
Install Flatpak tools and runtimes:
# Install flatpak-builder
sudo apt install flatpak-builder # Ubuntu/Debian
sudo dnf install flatpak-builder # Fedora
# Install GNOME SDK and Swift extension
flatpak install flathub org.gnome.Sdk//48 org.freedesktop.Sdk.Extension.swift6//24.08Flatpak Manifest
Create a manifest file (e.g., com.example.MyApp.yml):
app-id: com.example.MyApp
runtime: org.gnome.Platform
runtime-version: "48"
sdk: org.gnome.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.swift6
command: MyApp
finish-args:
- --share=ipc
- --socket=fallback-x11
- --socket=wayland
- --device=dri
build-options:
append-path: /usr/lib/sdk/swift6/bin
prepend-ld-library-path: /usr/lib/sdk/swift6/lib
modules:
- name: MyApp
buildsystem: simple
sources:
- type: dir
path: .
build-commands:
- swift build -c release --product MyApp --static-swift-stdlib
- install -Dm755 .build/release/MyApp /app/bin/MyApp
- install -Dm644 com.example.MyApp.desktop /app/share/applications/com.example.MyApp.desktop
- install -Dm644 com.example.MyApp.metainfo.xml /app/share/metainfo/com.example.MyApp.metainfo.xml
- install -Dm644 com.example.MyApp.svg /app/share/icons/hicolor/scalable/apps/com.example.MyApp.svgKey points:
--static-swift-stdliblinks the Swift runtime statically — the SDK extension is only needed at build time- The GNOME runtime provides GTK4 and libadwaita at runtime
- You also need a
.desktopfile,metainfo.xml, and an app icon
Build and Run
# Build and install locally
flatpak-builder --force-clean --user --install build-dir com.example.MyApp.yml
# Run
flatpak run com.example.MyAppDemo App Flatpak
The included DemoApp has a complete Flatpak setup in the flatpak/ directory:
flatpak-builder --force-clean --user --install build-dir flatpak/io.github.makoni.SwiftAdwaitaDemo.yml
flatpak run io.github.makoni.SwiftAdwaitaDemoFor more details, see the FlatpakDistribution guide.
License
MIT License. See LICENSE.txt.
Package Metadata
Repository: makoni/swift-adwaita
Default branch: main
README: README.md