How to create a SwiftUI floating window in macOS 15

Sponsored
RevenueCat logo
Releases so easy your work will never pile up

Runway handles the release coordination and busywork so you can focus on building great apps. You do the building, we'll do the shipping.

Up until WWDC 24, SwiftUI had no built-in way of creating floating windows or, in other words, windows that stay on top of everything on the user’s screen. This was a limitation that I faced when building QReate’s latest feature and that I had to rely on AppKit to implement:

During the What’s new in SwiftUI session, Apple introduced a new set of APIs for window management that, among other things, allow you to set the floating level of a specific window in your app through the use of view modifiers:

App.swift
import SwiftUI

@main
struct QReateApp: App {
    var body: some Scene {
        // ...
        WindowGroup(id: "floating-qr-code-window", for: UUID.self) { $qrCodeId in
            if let qrCodeId = qrCodeId {
                FloatingPanelQRCode(id: qrCodeId)
            }
        }
        // macOS 15.0, iOS unavailable, tvOS unavailable, watchOS unavailable, visionOS unavailable
        .windowManagerRole(.associated)
        // iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0
        .windowLevel(.floating)
        .windowStyle(.plain)
        .windowResizability(.contentSize)
    }
}

There is also a brand new modifier that allows you to decide the default position of the window when it is created:

App.swift
import SwiftUI

@main
struct QReateApp: App {
    var body: some Scene {
        // ...
        WindowGroup(id: "floating-qr-code-window", for: UUID.self) { $qrCodeId in
            if let qrCodeId = qrCodeId {
                FloatingPanelQRCode(id: qrCodeId)
            }
        }
        // macOS 15.0, iOS unavailable, tvOS unavailable, watchOS unavailable, visionOS unavailable
        .windowManagerRole(.associated)
        // iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0
        .windowLevel(.floating)
        .windowStyle(.plain)
        .windowResizability(.contentSize)
        // macOS 15.0, visionOS 2.0, iOS unavailable, tvOS unavailable, watchOS unavailable
        .defaultWindowPlacement { content, context in
            let displayBounds = context.defaultDisplay.visibleRect
            let size = content.sizeThatFits(.unspecified)
            let position = CGPoint(
                x: displayBounds.midX - (size.width / 2),
                y: displayBounds.maxY - size.height - 20
            )
            return WindowPlacement(position, size: size)
        }
    }
}