WWDC Quick Look 💓 By SwiftGGTeam
What's new in AppKit

What's new in AppKit

Watch original video

Highlight

macOS Sequoia takes AppKit’s SwiftUI integration further: NSHostingMenu shares menu definitions, NSView supports SwiftUI Animation types for interruptible animations, and new standard components include Text Entry Suggestions, system cursors, and Save Panel file format pickers.


Core Content

Mac apps have long faced a tension: AppKit delivers full system-level control, while SwiftUI brings declarative development efficiency—but the boundary between them has never been smooth. Menus had to be built by hand with NSMenu, animations relied on legacy NSAnimationContext APIs, and text input suggestions were implemented differently in every app. These gaps slowed development and increased maintenance cost.

Sequoia fills them one by one. NSHostingMenu lets you define menu content with SwiftUI views, sharing the same menu definition across AppKit and SwiftUI. NSAnimationContext.animate(with:) now accepts SwiftUI Animation types, including custom animations, and animations naturally support interruption and redirection. NSTextField adds suggestionsDelegate, a standard protocol for custom input suggestions in search fields and text fields. System cursors (frame resize, column/row resize, zoom in/out) are finally exposed in the SDK—no more drawing your own.

On the system side, Writing Tools work automatically in standard NSTextView, Genmoji appear as inline images in text flows, and Image Playground integrates in a few lines via ImagePlaygroundViewController. Window Tiling applies automatically to all existing apps, but developers should verify that window min/max size constraints are reasonable.


Detailed Content

Image Playground Integration (02:09)

extension DocumentCanvasViewController {

    @IBAction
    func importFromImagePlayground(_ sender: Any?) {
        let playground = ImagePlaygroundViewController()
        playground.delegate = self

        playground.concepts = [.text("birthday card")]
        playground.sourceImage = NSImage(named: "balloons")

        presentAsSheet(playground)
    }

}

extension DocumentCanvasViewController: ImagePlaygroundViewController.Delegate {

    func imagePlaygroundViewController(
        _ imagePlaygroundViewController: ImagePlaygroundViewController,
        didCreateImageAt resultingImageURL: URL
    ) {
        if let image = NSImage(contentsOf: resultingImageURL) {
            imageView.image = image
        }
        dismiss(imagePlaygroundViewController)
    }

}

Key points:

  • ImagePlaygroundViewController is the entry point for the whole experience; set a delegate to listen for lifecycle events
  • concepts accepts text concept descriptions; sourceImage provides a visual reference—both are optional starting points for the user
  • When generation completes, the callback returns a file URL in a sandbox temp directory—you read and persist it yourself
  • Present with presentAsSheet(_:) and dismiss in the delegate callback when creation finishes

Window Tiling and Window Sizing (05:50)

Window Tiling fills half or quarter of the screen when you drag a window to an edge; Option + drag previews snap to the nearest edge. Two side-by-side windows resize in sync. New windows use cascadingReferenceFrame to get a non-tiled frame from an existing window for cascade offset. For terminal-style apps, resizeIncrements constrain resizing to character width and height steps:

window.resizeIncrements = NSSize(width: characterWidth, height: characterHeight)

NSHostingMenu: SwiftUI Menus for AppKit (07:05)

struct ActionMenu: View {

    var body: some View {
        Toggle("Use Groups", isOn: $useGroups)
        Picker("Sort By", selection: $sortOrder) {
            ForEach(SortOrder.allCases) { Text($0.title) }
        }.pickerStyle(.inline)
        Button("Customize View…") { <#Action#> }
    }

}

let menu = NSHostingMenu(rootView: ActionMenu())

let pullDown = NSPopUpButton(image: image, pullDownMenu: menu)

Key points:

  • Menu content uses standard SwiftUI declarations: Toggle for switches, Picker for selection, Button for actions
  • NSHostingMenu is a new NSMenu subclass that wraps a SwiftUI view into an NSMenu
  • The wrapped menu works in any AppKit context that accepts NSMenu, such as an NSPopUpButton pull-down menu

SwiftUI Animations on NSView (07:43)

NSAnimationContext.animate(with: .spring(duration: 0.3)) {
    drawer.isExpanded.toggle()
}

Pair with the @Invalidating property wrapper so property changes automatically trigger relayout:

class PaletteView: NSView {

    @Invalidating(.layout)
    var isExpanded: Bool = false

    private func onHover(_ isHovered: Bool) {
        NSAnimationContext.animate(with: .spring) {
            isExpanded = isHovered
            layoutSubtreeIfNeeded()
        }
    }

}

Key points:

  • NSAnimationContext.animate(with:) accepts any SwiftUI Animation type, including CustomAnimation
  • Animations are naturally interruptible and redirectable—changing direction mid-animation won’t stutter
  • @Invalidating(.layout) marks the view for relayout when the property changes

Text Highlighting (10:31)

let attributes: [NSAttributedString.Key: Any] = [
    .textHighlight: NSAttributedString.TextHighlightStyle.systemDefault,
    .textHighlightColorScheme: NSAttributedString.TextHighlightColorScheme.pink,
]

Available automatically in rich-text NSTextView; the Font menu’s Highlight submenu lets users pick a color scheme.

Text Entry Suggestions (17:49)

class MuseumTextSuggestionsController: NSTextSuggestionsDelegate {

    typealias SuggestionItemType = Museum

    func textField(
        _ textField: NSTextField,
        provideUpdatedSuggestions responseHandler: @escaping ((ItemResponse) -> Void)
    ) {
        let searchString = textField.stringValue

        func museumItem(_ museum: Museum) -> Item {
            var item = NSSuggestionItem(representedValue: museum, title: museum.name)
            item.secondaryTitle = museum.address
            return item
        }

        let favoriteMuseums = Museum.favorites.filter({
            $0.matches(searchString)
        })
        let favorites = NSSuggestionItemSection(
            title: NSLocalizedString("Favorites", comment: ""),
            items: favoriteMuseums.map(museumItem(_:))
        )
        var response = NSSuggestionItemResponse(itemSections: [favorites])
        response.phase = .intermediate
        responseHandler(response)

        Task {
            let otherMuseums = await Museum.allMatching(searchString)
            let nonFavorites = NSSuggestionItemSection(items: otherMuseums.map(museumItem(_:)))
            var response = NSSuggestionItemResponse(itemSections: [favorites, nonFavorites])
            response.phase = .final
            responseHandler(response)
        }
    }

}

Key points:

  • Set textField.suggestionsDelegate to enable; works with NSTextField and subclasses like NSSearchField
  • You can return results in two phases: synchronous .intermediate first, then async .final
  • NSSuggestionItem supports secondaryTitle for extra info; NSSuggestionItemSection supports grouping

Core Takeaways

  • What to do: Add Text Entry Suggestions to NSTextField for search suggestions or autocomplete. Why it’s worth it: Users expect instant feedback while typing; this API is the system-standard approach with sync + async phased results, more consistent than custom popovers. How to start: Implement NSTextSuggestionsDelegate, set textField.suggestionsDelegate, return .intermediate local results first, then async .final remote results.

  • What to do: Migrate menu definitions to SwiftUI with NSHostingMenu, shared across AppKit/SwiftUI. Why it’s worth it: Maintain menu logic in one place; declarative Toggle, Picker, and Button are clearer than manually building NSMenuItem. How to start: Create a SwiftUI view as the menu body, wrap with NSHostingMenu(rootView:), and replace existing NSMenu construction code.

  • What to do: Replace legacy NSAnimationContext animations with SwiftUI Animation. Why it’s worth it: Spring and custom animations in one declaration, with natural interruption and redirection for better interaction. How to start: Replace NSAnimationContext.runAnimationGroup with NSAnimationContext.animate(with: .spring(duration:)), and use @Invalidating(.layout) on properties that should animate.

  • What to do: Integrate Image Playground as an image input source. Why it’s worth it: If your app already supports inserting images from Photos or Finder, Image Playground is a zero-friction third path—ImagePlaygroundViewController opens the generation UI in a few lines. How to start: Create ImagePlaygroundViewController, set delegate and optional concepts/sourceImage, present as a sheet, and read the file URL in the delegate callback.

  • What to do: Replace custom-drawn cursors with system cursor APIs. Why it’s worth it: System cursors automatically support accessibility large sizes and pointer color customization; custom cursors need extra handling. How to start: Use NSCursor.frameResize(position:directions:), NSCursor.columnResize(directions:), NSCursor.zoomIn/.zoomOut instead of custom cursor images.


Comments

GitHub Issues · utterances