Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2024 App ServicesSwiftUI & UI Frameworks

WWDC24 · 20 min · App Services / SwiftUI & UI Frameworks

What’s new in AppKit

Discover the latest advances in Mac app development. Get an overview of the new features in macOS Sequoia, and how to adopt them in your app. Explore new ways to integrate your existing code with SwiftUI. Learn about the improvements made to numerous AppKit controls, like toolbars, menus, text input, and more.

Watch at developer.apple.com ↗

Transcript all transcripts

Chapters

Code shown on screen · 17 snippets

Adding the Image Playground experience swift · at 2:09 ↗
extension DocumentCanvasViewController {

    @IBAction
    func importFromImagePlayground(_ sender: Any?) {
        // Initialize the playground, get set up to be notified of lifecycle events.
        let playground = ImagePlaygroundViewController()
        playground.delegate = self
    
        // Seed the playground with concepts and source imagery. (Optional)
        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
        } else {
            logger.error("Could not read image at \(resultingImageURL)")
        }
        dismiss(imagePlaygroundViewController)
    }

}
Using window resize increments swift · at 5:50 ↗
window.resizeIncrements = NSSize(width: characterWidth, height: characterHeight)
Build menus with SwiftUI swift · at 7: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)
Get animated with SwiftUI swift · at 7:43 ↗
NSAnimationContext.animate(with: .spring(duration: 0.3)) {
    drawer.isExpanded.toggle()
}
Get animated with SwiftUI swift · at 7:55 ↗
class PaletteView: NSView {

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

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

}
Text highlighting swift · at 10:31 ↗
let attributes: [NSAttributedString.Key: Any] = [
    .textHighlight: NSAttributedString.TextHighlightStyle.systemDefault,
    .textHighlightColorScheme: NSAttributedString.TextHighlightColorScheme.pink,
]
SF Symbols effects swift · at 11:11 ↗
imageView.addSymbolEffect(.wiggle)
imageView.addSymbolEffect(.rotate)
imageView.addSymbolEffect(.breathe)
SF Symbols playback (periodic) swift · at 11:24 ↗
imageView.addSymbolEffect(.wiggle, options: .repeat(.periodic(3, delay: 0.5)))
SF Symbols playback (continuous) swift · at 11:30 ↗
imageView.addSymbolEffect(.wiggle, options: .repeat(.continuous))
SF Symbols magic replace swift · at 11:37 ↗
imageView.setSymbolImage(badgedSymbolImage, contentTransition: .replace)
Save panel content types swift · at 12:19 ↗
extension ImageViewController: NSOpenSavePanelDelegate {
    
    @MainActor
    @IBAction
    internal func saveDocument(_ sender: Any?) {
        Task {
            let savePanel = NSSavePanel()
            savePanel.delegate = self
            savePanel.identifier = NSUserInterfaceItemIdentifier("ImageExport")
            savePanel.showsContentTypes = true
            savePanel.allowedContentTypes = [.png, .jpeg]
            let result = await savePanel.beginSheetModal(for: window)
            switch result {
                case .OK:
                    let url = savePanel.url
                    // Save the document to 'url'. It already has the appropriate extension.
                case .cancel: break
                default: break
            }
        }
    }
    
    func panel(_ panel: Any, displayNameFor type: UTType) -> String? {
        switch type {
            case .png:
                NSLocalizedString("PNG (Greater Quality)", comment: <#Comment#>)
            case .jpeg:
                NSLocalizedString("JPG (Smaller File Size)", comment: <#Comment#>)
            default:
                nil
        }
    }
    
}
Frame-resize cursors swift · at 13:34 ↗
let cursor = NSCursor.frameResize(position: .bottomRight, directions: .all)
Column and row resize cursors swift · at 14:20 ↗
let cursor = NSCursor.columnResize(directions: .left)
let cursor = NSCursor.rowResize(directions: .up)
Zoom in and out cursors swift · at 14:29 ↗
let cursor = NSCusor.zoomIn
let cursor = NSCusor.zoomOut
Display mode customizable toolbar swift · at 15:57 ↗
let toolbar = NSToolbar(identifier: NSToolbar.Identifier("ViewerWindow"))
toolbar.allowsDisplayModeCustomization // Defaults to `true`.
Hidden toolbar items swift · at 16:57 ↗
let downloadsToolbarItem: NSToolbarItem
downloadsToolbarItem.isHidden = downloadsManager.downloads.isEmpty
Text entry suggestions swift · at 17:49 ↗
class MYViewController: NSViewController {
    
    let museumTextField = NSTextField(string: "")
    
    let museumTextSuggestionsController = MuseumTextSuggestionsController()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.museumTextField.suggestionsDelegate = self.museumTextSuggestionsController
    }
    
}

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: "The title of suggestion results section containing favorite museums."),
            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)
        }
    }
    
}

Resources