How to create a SwiftUI floating window in macOS 15

Sponsored
RevenueCat logo
Relax, you can roll back your mobile release

No one is immune from shipping critical bugs to production, but Runway helps you limit the amount of havoc that can cause.

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)
        }
    }
}