{"$schema": "https://raw.githubusercontent.com/guitaripod/wwdc-sessions/master/schema/transcript.schema.json", "id": "wwdc2026-289", "language": "eng", "source": "https://developer.apple.com/videos/play/wwdc2026/289", "wordCount": 2486, "segments": [{"start": 7.0, "text": "Hi!"}, {"start": 8.0, "text": "I'm Ujjaini, a Mac UI frameworks engineer, and this is \"Modernize Your AppKit App\"."}, {"start": 14.0, "text": "A modern app takes advantage of how AppKit interfaces with Mac,"}, {"start": 18.0, "text": "so that its form and function feel in harmony with the rest of the system."}, {"start": 24.0, "text": "That harmony shows up in three places:"}, {"start": 27.0, "text": "in how people drive your app, in how the system manages it,"}, {"start": 33.0, "text": "and in how it looks and feels on the screen."}, {"start": 36.0, "text": "Today I'll share tips for all three."}, {"start": 39.0, "text": "I'll begin with modern precision input methods that your app should adapt to."}, {"start": 45.0, "text": "Then, I'll discuss the importance of preserving continuity across launches."}, {"start": 50.0, "text": "That is, how your app can terminate gracefully when the system needs it to,"}, {"start": 54.0, "text": "and come back to where it was left off."}, {"start": 57.0, "text": "And finally, I'll go through updates to the look and feel in macOS 27,"}, {"start": 61.0, "text": "many of which you'll notice without having to rebuild your app."}, {"start": 66.0, "text": "I'll start where the Mac started: at the input device."}, {"start": 70.0, "text": "Precision input devices have been at the heart of the mac since the very beginning."}, {"start": 75.0, "text": "The first Mac shipped with a keyboard and mouse!"}, {"start": 79.0, "text": "Over time, the APIs for handling these various kinds of input have evolved"}, {"start": 84.0, "text": "and become far more convenient."}, {"start": 87.0, "text": "mouseDown and tracking loops have been the go-to pattern,"}, {"start": 91.0, "text": "for implementing interactive behaviors in AppKit apps."}, {"start": 94.0, "text": "However, AppKit isn't the only framework on Mac."}, {"start": 98.0, "text": "SwiftUI, UIKit through Mac Catalyst,"}, {"start": 101.0, "text": "and AppKit work together to create the Mac experience."}, {"start": 106.0, "text": "They rely on Gesture Recognizers,"}, {"start": 108.0, "text": "to provide a common event handling language across all three frameworks."}, {"start": 114.0, "text": "Gesture recognizers empower AppKit to provide advanced behaviors,"}, {"start": 119.0, "text": "without having to build that yourself."}, {"start": 122.0, "text": "The modern way of dealing with events is gesture recognizers."}, {"start": 126.0, "text": "I'll explore three solutions that interface well with them:"}, {"start": 129.0, "text": "view-based APIs, control events, and custom gesture recognizers themselves."}, {"start": 134.0, "text": "These offer the same customizability as tracking loops and mouseDown overrides,"}, {"start": 139.0, "text": "and support cross-framework compatibility."}, {"start": 142.0, "text": "I'll tell you when to reach for each."}, {"start": 145.0, "text": "Common mouseDown overrides enable tracking selection,"}, {"start": 148.0, "text": "showing context menus, drag-and-drop, and text selection."}, {"start": 153.0, "text": "If you're currently overriding mouseDown,"}, {"start": 155.0, "text": "AppKit has dedicated APIs that handle these behaviors more reliably,"}, {"start": 161.0, "text": "and take full advantage of the modern Mac platform."}, {"start": 165.0, "text": "mouseDown is often overridden to track selection."}, {"start": 169.0, "text": "Instead,"}, {"start": 170.0, "text": "observe the selected property on types like NSCollectionViewItem and NSTableRowView."}, {"start": 176.0, "text": "Or, use the delegate callbacks that are notified when selection changes,"}, {"start": 180.0, "text": "like NSTableViewDelegate, and NSOutlineViewDelegate."}, {"start": 184.0, "text": "To show context menus from a view, you have a few options."}, {"start": 189.0, "text": "Use the class property .defaultMenu on NSView"}, {"start": 193.0, "text": "where all instances of the view will show the same menu."}, {"start": 197.0, "text": "Use the instance property .menu on NSResponder,"}, {"start": 200.0, "text": "to provide a different menu for every responder."}, {"start": 204.0, "text": "Or, use the instance method .menuForEvent on NSView,"}, {"start": 208.0, "text": "to dynamically create the menu based on the event."}, {"start": 212.0, "text": "If your app uses collection container views,"}, {"start": 215.0, "text": "use modern dragging delegate methods, like tableView pasteboardWriterForRow."}, {"start": 221.0, "text": "Create a pasteboardItem, set the data on it, and return it."}, {"start": 225.0, "text": "Similar methods exist on NSCollectionView, NSOutlineView and NSBrowser."}, {"start": 232.0, "text": "If you need text selection behavior outside of NSTextView,"}, {"start": 236.0, "text": "use NSTextSelectionManager."}, {"start": 239.0, "text": "It's a new API in macOS 27, that takes advantage of gesture recognizers"}, {"start": 245.0, "text": "and brings classic macOS text selection behaviors to any view."}, {"start": 250.0, "text": "Attach it to a view and set up a text selection data source."}, {"start": 253.0, "text": "You'll then have support for bidirectional selection,"}, {"start": 256.0, "text": "drag and drop with text, toggling, and much more."}, {"start": 262.0, "text": "The next solution might seem a little familiar,"}, {"start": 264.0, "text": "if you know control events from UIKit."}, {"start": 266.0, "text": "Control events are now in AppKit!"}, {"start": 269.0, "text": "Control events can be added to standard Mac controls,"}, {"start": 272.0, "text": "like buttons or sliders."}, {"start": 274.0, "text": "They enable your code to react to user-driven tracking state changes,"}, {"start": 280.0, "text": "rather than having to implement complex mouseDown tracking logic."}, {"start": 284.0, "text": "AppKit calls the registered target and action when the control event is triggered."}, {"start": 289.0, "text": "Most of these control events have been made available from OS 10.11!"}, {"start": 295.0, "text": "Here is an example of NSControlEvents."}, {"start": 297.0, "text": "Instantiate the button and register a target and action for a control event."}, {"start": 301.0, "text": "Note that you don't even have to subclass NSButton to get this to work!"}, {"start": 307.0, "text": "For more control over interactions in your views,"}, {"start": 309.0, "text": "add standard gesture recognizers."}, {"start": 312.0, "text": "For even more flexibility, create your own custom gesture recognizer subclasses."}, {"start": 317.0, "text": "Learn more in the \"Gestures\" documentation."}, {"start": 320.0, "text": "Because gesture recognizers operate on a view and its sub-views,"}, {"start": 324.0, "text": "overlapping sibling views can silently block mouse events."}, {"start": 328.0, "text": "If a control doesn't seem to be responding to a click,"}, {"start": 331.0, "text": "make sure you don't have some sibling view that overlaps that button."}, {"start": 335.0, "text": "To address this issue, re-size the view so it doesn't overlap."}, {"start": 340.0, "text": "If the view should not be re-sized because it is an overlay,"}, {"start": 344.0, "text": "override hitTest and return nil,"}, {"start": 346.0, "text": "so hit testing can fall through to content underneath."}, {"start": 351.0, "text": "Now, I'll focus on keyboard navigation."}, {"start": 355.0, "text": "Your app needs to respond to keyboard input seamlessly,"}, {"start": 359.0, "text": "to enable speed and accessibility."}, {"start": 363.0, "text": "Keyboard navigation for controls can be turned on in system settings."}, {"start": 367.0, "text": "When it's on, focus moves between controls using tab or shift-tab."}, {"start": 372.0, "text": "The key view loop is the order in which controls are cycled through,"}, {"start": 376.0, "text": "when the Tab key is pressed."}, {"start": 378.0, "text": "To automatically recalculate the loop,"}, {"start": 381.0, "text": "every time a view is added or removed in the hierarchy,"}, {"start": 384.0, "text": "enable .autorecalculatesKeyViewLoop on the window."}, {"start": 387.0, "text": "If you do not set this value,"}, {"start": 389.0, "text": "you are in charge of creating and maintaining the key view loop."}, {"start": 394.0, "text": "Keyboard navigation also reaches beyond your app's windows,"}, {"start": 398.0, "text": "into the menu bar and status items."}, {"start": 401.0, "text": "Navigating across status items is a little different from main menu items."}, {"start": 406.0, "text": "Status items that show a menu when clicked,"}, {"start": 408.0, "text": "already behave like menus on the menu bar."}, {"start": 411.0, "text": "But status items can also be triggers,"}, {"start": 413.0, "text": "for actions or display some kind of transient UI."}, {"start": 417.0, "text": "To trigger an action, modify NSStatusItem's button property"}, {"start": 421.0, "text": "to include a target and action, and optionally an image."}, {"start": 425.0, "text": "This behaves like a regular button and the action fires automatically"}, {"start": 429.0, "text": "when Return is pressed during keyboard navigation."}, {"start": 432.0, "text": "To use a custom view for your status menu item,"}, {"start": 435.0, "text": "use status items view property to set the view."}, {"start": 439.0, "text": "Then add a target and action to the status item,"}, {"start": 442.0, "text": "to enable performing that action."}, {"start": 444.0, "text": "Status items can also be triggers to show custom windows, for example!"}, {"start": 449.0, "text": "When a status item shows its window,"}, {"start": 451.0, "text": "AppKit needs to know when that UI is active,"}, {"start": 454.0, "text": "so that keyboard focus can behave correctly."}, {"start": 457.0, "text": "Track the life cycle of your custom UI, using the expanded interface session API."}, {"start": 463.0, "text": "First set a delegate,"}, {"start": 465.0, "text": "when the item is created, that will receive begin and end calls"}, {"start": 469.0, "text": "to display or dismiss your window."}, {"start": 472.0, "text": "In the delegate, implement statusItem didBegin ExpandedInterfaceSession"}, {"start": 476.0, "text": "and statusItemDidEnd_ ExpandedInterfaceSession."}, {"start": 480.0, "text": "These methods are called by AppKit,"}, {"start": 482.0, "text": "to manage the life cycle of an expanded interface session."}, {"start": 485.0, "text": "In the didBegin call, show the window."}, {"start": 487.0, "text": "In the didEnd call, order the window out."}, {"start": 491.0, "text": "When it is time to dismiss the session,"}, {"start": 493.0, "text": "for example, because an action has been selected,"}, {"start": 496.0, "text": "call .cancel on the .expandedInterfaceSession?."}, {"start": 499.0, "text": "Note that the session might be canceled for you,"}, {"start": 502.0, "text": "if focus naturally moves somewhere else."}, {"start": 506.0, "text": "SwiftUI menu bar extras do a lot of this work for you!"}, {"start": 510.0, "text": "Check out the WWDC26 video \"Use SwifUI with AppKit and UIKit\""}, {"start": 515.0, "text": "to learn how an AppKit app can use a SwiftUI menu bar extra."}, {"start": 520.0, "text": "Making sure your app works just as well with the keyboard as with a mouse,"}, {"start": 525.0, "text": "is especially important for the many power users who choose the Mac."}, {"start": 530.0, "text": "Providing a seamless transition,"}, {"start": 532.0, "text": "within and outside of your app, is one more way to enable them."}, {"start": 537.0, "text": "Speaking of seamless transitions,"}, {"start": 540.0, "text": "a great Mac app seamlessly quits and quickly restores."}, {"start": 543.0, "text": "It quits without pushback,"}, {"start": 545.0, "text": "and comes back as if it was never quit in the first place!"}, {"start": 548.0, "text": "People should be able to quit their apps at any time."}, {"start": 552.0, "text": "Sometimes because they want to,"}, {"start": 553.0, "text": "sometimes because the system needs to reboot,"}, {"start": 556.0, "text": "which might happen during an overnight software update."}, {"start": 559.0, "text": "So your app should only block quit when it genuinely needs to."}, {"start": 564.0, "text": "When your app is presenting a sheet, the window might not be able to close."}, {"start": 569.0, "text": "And when a window can't close, the app can't quit."}, {"start": 572.0, "text": "The NSWindow property"}, {"start": 574.0, "text": "preventsApplicationTerminationWhenModal defaults to true,"}, {"start": 577.0, "text": "and for good reason!"}, {"start": 579.0, "text": "It's important to make sure your app doesn't lose data,"}, {"start": 582.0, "text": "when a document needs to be saved, for example."}, {"start": 585.0, "text": "Set this property to false,"}, {"start": 587.0, "text": "for all modals or sheets that don't strictly require intervention,"}, {"start": 591.0, "text": "to allow more graceful application termination."}, {"start": 595.0, "text": "With graceful termination handled, the next step is restoration."}, {"start": 599.0, "text": "Use NSWindowRestoration to customize how your app comes back."}, {"start": 604.0, "text": "State restoration requires 3 steps: opting into state restoration,"}, {"start": 609.0, "text": "encoding the UI state, and decoding the state to restore windows and UI."}, {"start": 614.0, "text": "I'll go through some code that uses NSWindowRestoration."}, {"start": 618.0, "text": "First, set an identifier for the window in the window controller."}, {"start": 622.0, "text": "For common windows, like your main window or a preferences window,"}, {"start": 626.0, "text": "set an autosave name."}, {"start": 629.0, "text": "This helps restore your window to an active space with the same frame."}, {"start": 633.0, "text": "There is no need to set an autosave name for document windows."}, {"start": 638.0, "text": "Then ensure window.isRestorable is set to true,"}, {"start": 642.0, "text": "so AppKit can call encodeRestorableState and restoreState on your windows."}, {"start": 648.0, "text": "This also lets Appkit automatically restore window state,"}, {"start": 651.0, "text": "like which window was minimized, which was frontmost,"}, {"start": 654.0, "text": "and which was full screen."}, {"start": 656.0, "text": "Also, set a window.restorationClass,"}, {"start": 659.0, "text": "which will be invoked when the app is re-launched,"}, {"start": 662.0, "text": "to restore the window itself."}, {"start": 664.0, "text": "Use encodeRestorableState to preserve everything you need"}, {"start": 667.0, "text": "to recreate your window's state."}, {"start": 670.0, "text": "Call super's implementation as well, so your state is restored correctly."}, {"start": 675.0, "text": "In this example,"}, {"start": 677.0, "text": "the selected item's identifier is encoded with the productIdentifier key."}, {"start": 683.0, "text": "Avoid encoding data that lives in your document or database."}, {"start": 687.0, "text": "The goal of state restoration is to be able to reconstruct the state of the UI,"}, {"start": 692.0, "text": "not to re-serialize the whole app."}, {"start": 695.0, "text": "All NSResponders have an encodeRestorable_ State method that you can override,"}, {"start": 700.0, "text": "so manage state for your views as well."}, {"start": 704.0, "text": ".encodeRestorableState is only called when state for the object has been invalidated."}, {"start": 710.0, "text": "Every time there is a change to your view hierarchy"}, {"start": 712.0, "text": "that should alter the saved state, call .invalidateRestorableState( )."}, {"start": 717.0, "text": "In this example,"}, {"start": 718.0, "text": "this method is called when a different product is selected in the sidebar."}, {"start": 723.0, "text": "At a later time,"}, {"start": 724.0, "text": "encodeRestorableState will be called on everything that was invalidated."}, {"start": 729.0, "text": "That's what your app needs to save the state of its UI before it has quit."}, {"start": 734.0, "text": "When the app is re-launched,"}, {"start": 735.0, "text": "you'll need to decode all that information to restore the UI."}, {"start": 739.0, "text": "First restore your windows,"}, {"start": 741.0, "text": "and then restore the state on those windows!"}, {"start": 745.0, "text": "In the window restoration class,"}, {"start": 746.0, "text": "implement the method restoreWindow withIdentifier,"}, {"start": 750.0, "text": "to recreate the windows in your app."}, {"start": 753.0, "text": "This method is called for every window that's being restored."}, {"start": 757.0, "text": "Its parameters include the window's identifier,"}, {"start": 760.0, "text": "and the completionHandler that needs to be called with the corresponding window."}, {"start": 766.0, "text": "Using the identifier, recreate the window controller and windows."}, {"start": 770.0, "text": "The .mainWindow is already available on the app delegates .mainWindowController."}, {"start": 775.0, "text": "Call the completionHandler with the existing .window."}, {"start": 779.0, "text": "For other windows, instantiate the window controller,"}, {"start": 782.0, "text": "and pass in its .window to the completionHandler."}, {"start": 785.0, "text": "If window creation fails, still call the completionHandler with the error."}, {"start": 790.0, "text": "AppKit waits on every restorable window, so always call the completionHandler."}, {"start": 795.0, "text": "If you can't call it from within this method,"}, {"start": 798.0, "text": "save the handler and call it later."}, {"start": 800.0, "text": "Bu be absolutely sure to call it!"}, {"start": 803.0, "text": "Once the windows have been restored,"}, {"start": 806.0, "text": "the last step is to restore the UI for each window."}, {"start": 809.0, "text": "In the window controller's restoreState method,"}, {"start": 812.0, "text": "AppKit will hand you the same coder object containing the keys you encoded before."}, {"start": 817.0, "text": "This is the place to fetch any data required to reconstruct your app's state."}, {"start": 822.0, "text": "Decode the identifiers and hand them to the corresponding view controllers."}, {"start": 827.0, "text": "When you're done, your windows should be in the same state as before."}, {"start": 831.0, "text": "Enhancing quit and relaunch to feel uninterrupted,"}, {"start": 836.0, "text": "helps people pick up right where they left off,"}, {"start": 838.0, "text": "whether they quit the app or restarted their Mac."}, {"start": 842.0, "text": "To learn state restoration in practice,"}, {"start": 844.0, "text": "check out the code sample \"Restoring your app's state with AppKit\"."}, {"start": 849.0, "text": "With input and restoration in hand,"}, {"start": 851.0, "text": "there's one more area where your app and the Mac meet: the UI."}, {"start": 855.0, "text": "The Liquid Glass material, introduced in macOS 26,"}, {"start": 858.0, "text": "continues to evolve."}, {"start": 860.0, "text": "Your app will benefit from many of the updates automatically."}, {"start": 864.0, "text": "If you adopted Liquid Glass in macOS 26,"}, {"start": 867.0, "text": "your app will pick up a few changes when you run it on macOS 27."}, {"start": 871.0, "text": "The automatic NSScrollEdgeEffectStyle resolves to a hard-edge effect,"}, {"start": 876.0, "text": "when there is free-floating text, like the window title in the title bar."}, {"start": 882.0, "text": "Sidebars extend to the window's edges,"}, {"start": 885.0, "text": "selection in the sidebar uses a semi-bold text style for emphasis."}, {"start": 892.0, "text": "And content still flows behind them."}, {"start": 896.0, "text": "Bordered toolbar items over the sidebar adopt Liquid Glass as well."}, {"start": 901.0, "text": "New in macOS 27, there is an effect that can be added to glass."}, {"start": 906.0, "text": "Where the glass subtly bounces when clicked,"}, {"start": 908.0, "text": "giving a sense that the control is responding to interaction."}, {"start": 912.0, "text": "Maps uses this for a few of their custom controls."}, {"start": 919.0, "text": "This would not apply to all uses of glass in your app:"}, {"start": 922.0, "text": "use this effect with controls and buttons,"}, {"start": 924.0, "text": "or glass containers of interactive controls."}, {"start": 927.0, "text": "A little goes a long way!"}, {"start": 931.0, "text": "Rounded rectangles have been a signature of the Apple ecosystem for decades,"}, {"start": 936.0, "text": "from hardware bezels to controls and containers across macOS."}, {"start": 941.0, "text": "AppKit has new API for concentricity."}, {"start": 944.0, "text": "Content meant for a corner can adapt to the shape of its container,"}, {"start": 948.0, "text": "instead of feeling at odds with the rest of the window."}, {"start": 952.0, "text": "For example, the local weather view in Maps is concentric with the window."}, {"start": 958.0, "text": "When a view sits near the corner of its container,"}, {"start": 960.0, "text": "its own rounded corners should follow the curve of that container."}, {"start": 964.0, "text": "The closer the view is to the container's corner, the more its radius should match."}, {"start": 971.0, "text": "To make your button or view concentric, use the cornerConfiguration API."}, {"start": 976.0, "text": "First, create a custom view subclass."}, {"start": 980.0, "text": "On the custom view, override cornerConfiguration,"}, {"start": 983.0, "text": "to return an NSViewCornerConfiguration?."}, {"start": 987.0, "text": "For the radius, use .containerConcentric on NSViewCornerRadius."}, {"start": 992.0, "text": "This calculates a radius based on the container view."}, {"start": 996.0, "text": "Set a minimum value as well,"}, {"start": 998.0, "text": "so that every corner is always rounded."}, {"start": 1002.0, "text": "You can choose from many different kinds of factory methods for the configuration."}, {"start": 1006.0, "text": "To maintain a roundedRect with the same radii across all 4 corners,"}, {"start": 1011.0, "text": "use .uniformCorners."}, {"start": 1014.0, "text": "These are a few pointers that can help make your app harmonious on modern macOS."}, {"start": 1019.0, "text": "I'll leave you with a quick recap of where to start."}, {"start": 1023.0, "text": "Identify places in which you are overriding mouseDown in your app,"}, {"start": 1027.0, "text": "and instead use view APIs, control events, or gesture recognizers."}, {"start": 1032.0, "text": "Prioritize user intent over tracking loops."}, {"start": 1037.0, "text": "Make sure your app works just as well from the keyboard as from the mouse."}, {"start": 1042.0, "text": "Make quit and relaunch feel seamless,"}, {"start": 1045.0, "text": "so your app picks up exactly where people left off."}, {"start": 1049.0, "text": "And evaluate your view hierarchies to adopt concentricity in views and buttons."}, {"start": 1056.0, "text": "Thank you so much for watching."}, {"start": 1059.0, "text": "Whether your app is for the students at school learning how to use a computer"}, {"start": 1062.0, "text": "or the power users who build some of the world's most important tools and art,"}, {"start": 1067.0, "text": "your apps have played a central role in this experience."}, {"start": 1070.0, "text": "Keep on creating!"}]}