KeyWindow a better way of exposing values from the Key window


In AppKit and UIKit there is a concept of a key window this is the window that is responsible for handling keyboard shortcuts and populating the Menu Bar in macOS. In my last post Reading from the Window in a SwiftUI lifecycle app I investigate how to detect which window is currently the key window. Building on this have I writen a Swift Package KeyWindow that provides an api similar to the FocusedValue api but does not require the user to be focused in a text field. In this post I will provide some examples of how to use KeyWindow to expose values from the current key window to views in the .commands block.


Adding KeyWindow to your App


In Xcode you can add Swift Packages from the File ⇾ Swift Packages ⇾ Add Package Dependancy menu item. Then paste the package url: https://github.com/LostMoa/KeyWindow. If your building a cross platform app Xcode 12.3 will only add this to your iOS target so to ensure that it is also included in the macOS target you should navigate to General builds settings for your macOS target and scroll down to the Frameworks, Libraries and Embedded Content session here use the + to add KeyWindow.


Using KeyWindow within your App.

Firstly in your top level App in the body were your WindowGroup or DocumentGroup creates the ContentView add .observeWindow().


import SwiftUI
import KeyWindow


@main
struct ExampleKeyWindowAppApp: App {
    
    var body: some Scene {
        DocumentGroup(newDocument: ExampleKeyWindowAppDocument()) { file in
            ContentView(
                document: file.$document
            ).observeWindow()
        }
    }
}


This will allow the framework to track the window as it becomes the key window.


Providing values


Like the Preferences, FocusedValue and EnvironmentValues to provide KeyWindow values we need to first declare type that conforms to KeyWindowValueKey.


This does not need to be a new type, it can be an extension on an existing type if you only plan on exposing one instance of that type per window. For example in the case of a document application it makes sense to use the document type as our key:


extension ExampleKeyWindowAppDocument: KeyWindowValueKey {
    public typealias Value = Binding<Self>
} 

But you might also set other keys for example

struct SelectedProjectTitleWindowValueKey: KeyWindowValueKey {
    typealias Value = String
} 


With these defined we can now expose them to the framework by using the new .keyWindow modifier.


struct ContentView: View {
    @Binding var document: ExampleKeyWindowAppDocument

    var body: some View {
        TextEditor(text: $document.text).keyWindow(
            ExampleKeyWindowAppDocument.self,
            $document
        )
    }
}


Reading values


In any View of your app you can read these values out, the values set by views from the current Key window will be exposed to all views within your app, this includes views in the .commands block.


For example to read a value that is a Binding use the @KeyWindowValueBinding property wrapper:


struct ClearDocument: View {
    
    @KeyWindowValueBinding(ExampleKeyWindowAppDocument.self)
    var document: ExampleKeyWindowAppDocument?
    
    var body: some View {
        Button(action: {
            document?.text = ""
        }, label: {
            Text("Clear \(document?.text.count ?? 0) chars")
        }).keyboardShortcut(KeyEquivalent("r"), modifiers: .command)
    }
}


To read a non Binding type you can use:

@KeyWindowValue(SelectedProjectTitleWindowValueKey.self)
var title: String