Streaming is available in most browsers,
and in the Developer app.
-
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.
Chapters
- 0:00 - Introduction
- 0:49 - New macOS features
- 0:52 - Writing Tools, Genmoji, and Image Playground
- 3:31 - Window Tiling
- 6:21 - More SwiftUI integrations
- 6:41 - Build menus with SwiftUI
- 7:39 - Get animated with SwiftUI
- 8:20 - API refinements
- 8:44 - Context menu refinements
- 9:42 - Text highlighting
- 11:00 - SF Symbols
- 11:59 - Save Panel refinements
- 13:04 - Cursors refinements!
- 15:21 - Toolbar refinements
- 17:22 - Text entry suggestions
Resources
Related Videos
WWDC24
- Bring expression to your app with Genmoji
- Enhance your UI animations and transitions
- Get started with Writing Tools
- What’s new in SF Symbols 6
WWDC23
-
Download
Hello! I’m Matt Zanchelli, an engineer on the AppKit team and this is What’s new in AppKit.
If you love AppKit and the Mac as much as I do, then you’re in the right place. I’m going to show you many improvements made in macOS Sequoia, and let you know how you can make your AppKit apps even better.
In this video, I’ll cover a broad range of topics, starting with some of new system-wide features and how your app can adopt them.
Then, I’ll show you some changes made to AppKit that will help you when using SwiftUI.
And lastly, I’ll share with you many of the great new API refinements in the framework.
Let’s start with new macOS features.
With Writing Tools, macOS can now not only help you with spelling & grammar, but more sophisticated writing concepts like structure, clarity, and tone. We’ve worked hard to bring these writing tools system-wide, and your apps get these intelligence features automatically. For the best writing experience in your app, consider the interaction behaviors they bring. If your app handles a lot of text input, or does advanced things with text views, be sure to watch "Get started with Writing Tools".
I’ve been having a lot of fun creating some emoji I’ve always wanted. I can now express very specific emotions, objects, and scenes, and then share them with friends, in apps like Messages and Notes. I’m excited to be able to create and use these new emoji in your app, too. The new emoji you create are images and aren’t just Unicode characters, so there may be a small amount of adoption needed to display and store these images inline with text in your app.
Watch "Bring expression to your app with Genmoji", to learn how to adopt custom emoji in your app.
In addition to custom emoji, people can now create full images, in the new Image Playground app.
You can enable this magical image creation in your app too, by adopting the new Image Playground experience. Here’s how to add the Image Playground experience to your app. First, initialize an instance of the Image Playground view controller and assign its delegate.
The delegate will hear about important lifecycle events, like when the image creation has finished or cancelled.
If there is some specific context in your app leading into the Image Playground experience, your app can optionally set up the view controller with some initial concepts and source imagery. Concepts describe the expected contents of the output image, and the source image acts as a graphical reference for the created image. These two properties give the person viewing the sheet a jump start in creating their image. They can still choose different images and concepts inside the image creation sheet.
Then, present the view controller as a sheet to get creating.
When the image has been created, the view controller’s delegate receives a callback with a reference to the image’s file URL. This file URL is located in the app’s sandboxed temporary directory. Use that file URL to insert the image into your user interface and then dismiss the playground sheet.
If your app allows the insertion of images from the photo library, the Finder, or Continuity Camera, consider adding Image Playground as another source of images. And that’s just how easy it is to integrate the Image Playground experience into your app.
I cannot wait to share with you one of my favorite new features in macOS Sequoia: Window Tiling. I just gotta show it to you now.
Window Tiling makes it fast to move your windows into some common arrangements. I’ll start moving this window, and put my cursor to the right edge of the screen.
Dropping the window here fills that half of the screen.
If I drag this window out of its tiled position, it goes back to its untiled size.
This is great when I want a window prominently displayed for a little while, and then back to its regular size when I’m done. I don’t even have to move my cursor all the way to the screen’s edge to tile it. Holding Option while dragging immediately shows a preview of the nearest tile, and I can drop it right there. I can also access these Window Tiling options in the Window > Move & Resize menu.
Here, I can also see all their keyboard shortcuts. With those, I can really quickly move my windows around.
BOOM. BOOM.
BOOM.
And If I want to see these two windows side-by-side, there’s an arrangement for that as well.
In addition to the Window menu, I can now conveniently access these options in the window titlebar, too.
When two windows are tiled side-by-side like this, I can resize both at the same time, to get the proportions as I see fit.
When opening a new window, it appears at an untiled size.
To arrange all three of these windows, I can select a three-window arrangement.
How cool is that? Window Tiling is awesome and it works with all your apps on macOS Sequoia.
To make your apps work best with Window Tiling, consider a few things.
Your window’s minimum and maximum size.
Window Tiling lets people fill windows to a half or quarter of their screen. If your app’s window sizes are flexible enough, people can avoid overlapping windows. Check your window’s layout constraints that contribute to its minimum size.
If a window should only be resized in fixed width and height increments, use the resizeIncrements property. This can be used to resize a window in increments of a single character’s width or height, like in Terminal.
When opening new windows, consider how they are positioned relative to existing windows.
Use the new cascadingReferenceFrame property to get an existing window’s untiled frame. Cascade newly-opened windows relative to that frame. Or, if you’re using NSWindowController, this already happens by default in macOS Sequoia. I love using SwiftUI in my Mac apps, as it’s a fantastic way to build user interfaces. It’s been designed from the beginning to work alongside AppKit, so that it can be adopted incrementally. We’ve been deepening that integration for many years, and have taken it even further in macOS Sequoia.
Just like you can use SwiftUI views inside your AppKit app using NSHostingView, you can now use SwiftUI menus, too. This allows you share menu definitions between the parts of your app that use AppKit and the parts that use SwiftUI. You use SwiftUI menus in AppKit contexts using NSHostingMenu, which is a new NSMenu subclass. Doing so is easy.
Create your menu definition using a SwiftUI View. In the body, use the SwiftUI views that best describe the data relationship. Use a Toggle to switch a value on and off. A Picker to select one value from a list, and a Button to perform an action.
Initialize an NSHostingMenu with that SwiftUI view. And then use it in any AppKit context that accepts an NSMenu, like the new NSPopUpButton initializer with a pull-down menu parameter. In macOS Sequoia, AppKit gets animated with SwiftUI! You can now use a SwiftUI Animation type to animate NSViews! This lets you use the full set of powerful SwiftUI Animation types, including SwiftUI CustomAnimations! To animate your NSViews, use NSAnimationContext, passing in a SwiftUI animation type, and adjust your layout or drawing. SwiftUI animations are even interruptible and re-targetable.
For a more in-depth dive into SwiftUI animations with UIKit and AppKit, check out the video “Enhance your UI animations and transitions”.
Next, I’ll share some of the fantastic AppKit API refinements.
There’s a new way to open context menus, new capabilities in the text system and SF symbols, a new convenience when saving documents, some new cursors, more control over toolbars, and an exciting new API to assist with text input.
New in macOS Sequoia, is the ability to use the keyboard to open a context menu for the currently-focused UI element. People can use this feature to more quickly or comfortably access an app’s functionality. This shortcut is Control-return by default, but can be customized in System Settings.
But when a context menu is presented with the keyboard, instead of the mouse, where does the menu present from? I’ll show you how to influence the positioning of these context menus.
If your view has a value for the menu property, the menu is positioned automatically over the view’s bounds.
If your view draws a custom selection, implement the new NSViewContentSelectionInfo protocol to provide geometry information about the selection. The view’s menu will then be positioned appropriately near the selection. That’s how you control the position of a keyboard-presented context menu.
The next new API refinement I’ll share is Text highlighting.
Highlights can be used to emphasize text with a background color and contrasting foreground color.
This works in any NSTextView that supports rich text. First, select a range of text. Then, right-click and use the Font menu, and navigate to the Highlight submenu. You can choose from a number of highlight color schemes, or use the app’s accent color. For TextEdit, that’s blue. While support for this is automatic on rich text text views, you may wish to implement this new feature if your app has custom text attribute controls.
Text highlighting is controlled by attributed string attributes. The new .textHighlight attribute corresponds to a text highlight style. Set this to .systemDefault to indicate that a range of text should be highlighted. The colors used are based off of the app’s accent color. To control the colors, use the new .textHighlightColorScheme attribute, and associate it with one of the system-provided color schemes, like pink. macOS Sequoia comes with SF Symbols 6, which includes over 800 new symbols covering a wide variety of subjects.
It also includes even more effects, like wiggle, rotate, and breathe.
There are also new playback options for symbol effects, like repeating an effect a specific number of times or playing an animation in a continuous loop.
And my favorite addition is the ability to magically replace a symbol’s badge or slash. The new symbol effects are really cool! For more information on them, and how to author custom symbols, check out “What’s new in SF Symbols 6”. Also watch “Animate symbols in your app” for more information on how to use these expressive animations.
Next, an enhancement to the Save Panel.
When saving a document, it’s often convenient to be able to select the format of file you’d like to save, right in the save panel.
In macOS Sequoia, there’s now a standard file format picker, so you don’t have to create a custom accessory view just for this. Using the standard file format picker is as easy as setting the showsContentTypes property on the save panel to true. The picker contains an option for each of the allowed content types that the save panel supports, as specified by the existing allowedContentTypes property. By default, each menu item uses the content type’s localized description. To provide custom display names, implement the panel(_ displayNameFor type:) delegate function. In this function, return content type names that are appropriate for the context of your app.
I hope this new Save Panel enhancement saves you some development time.
And now, finally making their first public appearance in the macOS SDK, after years of following your mouse around, you guessed it! Cursors! The system’s cursors are now available to use in the macOS Sequoia SDK.
Let’s start with frame-resize cursors.
They’re used to resize an element from its edges and corners. It takes two parameters. The first is the position. This is the edge or corner that the cursor is interacting with. The second is the directions in which the element can be resized. Handle the case where the element is at its minimum or maximum size. Frame resize cursors are intended to be used when resizing a single element. But if you’re moving a separator between two elements, use the columnResize and rowResize cursors. The bar that’s perpendicular to the arrow in the cursor artwork depicts the separator that’s being resized. These cursors are useful when resizing the widths of table columns, or the heights of rows in a spreadsheet. Specify in which directions the separator can be moved to handle the cases where it’s at its limits.
For zooming in and out, there’s the new zoomIn and zoomOut cursors. Use these cursors when a click would cause the app’s contents to be magnified larger or smaller. Those are the system cursors newly available in AppKit. Using the system cursors gets you the standard appearance, for consistency across apps. If your app’s custom cursors aren’t communicating something that the system ones don’t, question whether the custom ones are really needed. Not only is it easier to make use of the standard cursors, and not have to draw them yourself, but they also support the larger accessibility sizes, as set in the Accessibility section in System Settings. As well as the settings for adjusting the pointer’s colors for those rely on a more unique color to see and locate the cursor effectively.
Next up, three enhancements to NSToolbar to give you more control over display mode, displayed items, and item visibility. NSToolbar supports displaying items with and without text labels. While your app’s preferred style might be icon-only, some people find it easier to locate a toolbar item if they can scan through the text labels as well, so it’s good to offer the choice. In macOS Sequoia, you can now offer that choice of style, even if the contents of your toolbar aren’t otherwise customizable, using the allowsDisplayModeCustomization property. It’s enabled by default. Make sure that your toolbar has an identifier so that AppKit can save the style preference, and double-check that all your toolbar items have good labels. As a convenience over NSToolbar’s existing items property, you can use the new itemIdentifiers property. Setting a toolbar’s item identifiers automatically makes the minimal additions and removals for you. Keep in mind that if allowsUserCustomization is enabled, changing values here will override any customizations. So only use this for dynamic, non-customizable toolbars. Prefer simply disabling items that aren’t applicable for the current selection. Only programmatically remove items from a toolbar when there’s a change in a window’s mode, like between document editing and viewing modes.
You can also conditionally hide and show toolbar items using the new isHidden property. Hidden items still appear in customization, so people can choose where they want it to appear when they’re made visible. Use this when an item isn’t applicable regardless of the current selection in the window.
For example, an app can hide the item that shows downloads until the first download starts.
Next, I want to share a brand-new API: text entry suggestions. It allows your app to provide custom suggestions as people type, in a system standard suggestions menu. This common pattern seen across many apps is now being standardized in AppKit in macOS Sequoia. It works on any NSTextField, including subclasses like NSSearchField. Get started by setting the suggestionsDelegate property on the text field. The delegate is asked for suggestions as text is typed, and can respond with results both synchronously, and asynchronously. It can also optionally customize the text completions on highlight and selection, too.
Here are some design tips when using text entry suggestions. Ensure that the suggestions displayed at any given point in time are relevant to the typed text, as people expect the interface to keep up with how fast they type. Provide consistent and predictable suggestions, to preserve muscle memory and build trust in the results. When asynchronous suggestions are provided, place those after the immediate suggestions you already provided. And keep it simple. Don’t provide anything but the most important results and details, to make it easy to find the right result quickly.
Those are just some of the AppKit enhancements in macOS Sequoia. There’s a lot more, which you can read all about in the release notes on developer.apple.com.
Start using the new intelligence features and adopt the APIs to integrate them seamlessly in your app.
Get rid of your window panes with window tiling. Ensure your apps windows are ready to be cleaned and tidied. Continue incrementally adopting SwiftUI with the new menu and animation APIs. Adopt the new system-standard components, like content type pickers, cursors, and text entry suggestions. Let people fly through your app with keyboard-presented context menus, and ensure your toolbars support all display modes.
Thank you so much for watching and developing great apps for the Mac. If these APIs are the paint, then Xcode is your paintbrush, and the Mac is your canvas! Go on and show the world what you can create!
-
-
2:09 - Adding the Image Playground experience
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) } }
-
5:50 - Using window resize increments
window.resizeIncrements = NSSize(width: characterWidth, height: characterHeight)
-
7:05 - Build menus with SwiftUI
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)
-
7:43 - Get animated with SwiftUI
NSAnimationContext.animate(with: .spring(duration: 0.3)) { drawer.isExpanded.toggle() }
-
7:55 - Get animated with SwiftUI
class PaletteView: NSView { @Invalidating(.layout) var isExpanded: Bool = false private func onHover(_ isHovered: Bool) { NSAnimationContext.animate(with: .spring) { isExpanded = isHovered layoutSubtreeIfNeeded() } } }
-
10:31 - Text highlighting
let attributes: [NSAttributedString.Key: Any] = [ .textHighlight: NSAttributedString.TextHighlightStyle.systemDefault, .textHighlightColorScheme: NSAttributedString.TextHighlightColorScheme.pink, ]
-
11:11 - SF Symbols effects
imageView.addSymbolEffect(.wiggle) imageView.addSymbolEffect(.rotate) imageView.addSymbolEffect(.breathe)
-
11:24 - SF Symbols playback (periodic)
imageView.addSymbolEffect(.wiggle, options: .repeat(.periodic(3, delay: 0.5)))
-
11:30 - SF Symbols playback (continuous)
imageView.addSymbolEffect(.wiggle, options: .repeat(.continuous))
-
11:37 - SF Symbols magic replace
imageView.setSymbolImage(badgedSymbolImage, contentTransition: .replace)
-
12:19 - Save panel content types
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 } } }
-
13:34 - Frame-resize cursors
let cursor = NSCursor.frameResize(position: .bottomRight, directions: .all)
-
14:20 - Column and row resize cursors
let cursor = NSCursor.columnResize(directions: .left) let cursor = NSCursor.rowResize(directions: .up)
-
14:29 - Zoom in and out cursors
let cursor = NSCusor.zoomIn let cursor = NSCusor.zoomOut
-
15:57 - Display mode customizable toolbar
let toolbar = NSToolbar(identifier: NSToolbar.Identifier("ViewerWindow")) toolbar.allowsDisplayModeCustomization // Defaults to `true`.
-
16:57 - Hidden toolbar items
let downloadsToolbarItem: NSToolbarItem downloadsToolbarItem.isHidden = downloadsManager.downloads.isEmpty
-
17:49 - Text entry suggestions
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) } } }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.