View in English

  • Apple Developer
    • Get Started

    Explore Get Started

    • Overview
    • Learn
    • Apple Developer Program

    Stay Updated

    • Latest News
    • Hello Developer
    • Platforms

    Explore Platforms

    • Apple Platforms
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    • App Store

    Featured

    • Design
    • Distribution
    • Games
    • Accessories
    • Web
    • Home
    • CarPlay
    • Technologies

    Explore Technologies

    • Overview
    • Xcode
    • Swift
    • SwiftUI

    Featured

    • Accessibility
    • App Intents
    • Apple Intelligence
    • Games
    • Machine Learning & AI
    • Security
    • Xcode Cloud
    • Community

    Explore Community

    • Overview
    • Meet with Apple events
    • Community-driven events
    • Developer Forums
    • Open Source

    Featured

    • WWDC
    • Swift Student Challenge
    • Developer Stories
    • App Store Awards
    • Apple Design Awards
    • Apple Developer Centers
    • Documentation

    Explore Documentation

    • Documentation Library
    • Technology Overviews
    • Sample Code
    • Human Interface Guidelines
    • Videos

    Release Notes

    • Featured Updates
    • iOS
    • iPadOS
    • macOS
    • watchOS
    • visionOS
    • tvOS
    • Xcode
    • Downloads

    Explore Downloads

    • All Downloads
    • Operating Systems
    • Applications
    • Design Resources

    Featured

    • Xcode
    • TestFlight
    • Fonts
    • SF Symbols
    • Icon Composer
    • Support

    Explore Support

    • Overview
    • Help Guides
    • Developer Forums
    • Feedback Assistant
    • Contact Us

    Featured

    • Account Help
    • App Review Guidelines
    • App Store Connect Help
    • Upcoming Requirements
    • Agreements and Guidelines
    • System Status
  • Quick Links

    • Events
    • News
    • Forums
    • Sample Code
    • Videos
 

Videos

Open Menu Close Menu
  • Collections
  • All Videos
  • About

Back to WWDC26

  • About
  • Summary
  • Transcript
  • Code
  • Use SwiftUI with AppKit and UIKit

    Discover how to incrementally adopt SwiftUI in your existing AppKit or UIKit app. We'll show you how to use the Observation framework to automatically update your views, integrate SwiftUI components into an existing view hierarchy, and bring gesture recognizers into SwiftUI. We'll also explore how to add complete SwiftUI scenes to your app without changing your overall architecture.

    Chapters

    • 0:00 - Introduction
    • 2:33 - Observation in AppKit
    • 5:41 - Hosting SwiftUI in AppKit
    • 7:48 - AppKit gestures in SwiftUI
    • 9:16 - SwiftUI in the main menu
    • 11:30 - SwiftUI scenes in AppKit
    • 13:04 - Next steps

    Resources

    • Updating views automatically with observation tracking
    • Updating views automatically with observation tracking
      • HD Video
      • SD Video

    Related Videos

    WWDC26

    • Compose advanced graphics effects with SwiftUI

    WWDC25

    • What’s new in UIKit

    WWDC22

    • Bring multiple windows to your SwiftUI app
    • Use SwiftUI with AppKit
    • Use SwiftUI with UIKit

    WWDC21

    • Add rich graphics to your SwiftUI app
  • Search this video…

    Hello, I'm David Nadoba, an engineer on the UI Frameworks team. Today, I'm excited to talk to you about using SwiftUI with your existing AppKit or UIKit app. SwiftUI was designed from the beginning to work great alongside AppKit and UIKit. Just like Swift was designed to work together with Objective-C. This is ideal for incremental adoption without the need to rewrite everything or start from scratch. Apple has used this strategy throughout the years. Logic Pro is using SwiftUI for plugins like the Quantec Room Simulator, or the Beat Breaker plugin for both macOS and iPadOS. The Coding Assistant in Xcode was from start using SwiftUI and in Xcode 27 is expanding from the sidebar to the editor. Even without explicit adoption, most apps use SwiftUI implicitly nowadays. The UI Frameworks team used the new design as an opportunity to implement Controls in SwiftUI. Now, even if you use AppKit types like NSSlider, NSSwitch, and NSSegmentedControl, SwiftUI is used under the hood to render these views and more. Liquid Glass used in those controls and in other parts of the OS is also using SwiftUI to share large parts of the implementation across frameworks and platforms. In this video I will share how you can start adopting SwiftUI in more places too. I will focus on macOS, but the concepts apply to all other Apple platforms as well. First, I will show how you can use @Observable to automatically update your NSView, even before using any SwiftUI. Next, I will talk about when it is a good time to consider using SwiftUI and integrate it in an NSView hierarchy. I will also show you how you can add an NSGestureRecognizer directly to your SwiftUI View. Then, I will create menu items in SwiftUI and add them to the existing main menu. Finally, I will cover how you can use SwiftUI Scenes from your existing NSApplicationDelegate. Over the course of this presentation I will use a reduced version of an existing AppKit app that I made.

    This app can control lights, like this addressable ring lamp on my desk.

    It has controls to change the color, and run animations.

    I'll walk you through how the sliders work and then demonstrate how the @Observable macro can help. The app uses a color picker that is similar to the system color panel or color well, but is displayed inline to keep the controls always within reach. The color is controlled through 3 sliders with a custom track gradient and knob. As I move the knob of one slider, it redraws itself with the newly selected color. At the same time, all other sliders also update accordingly.

    A slider automatically redraws itself when its own value changes. In my case, the changed value also influences the appearance of the other sliders, but AppKit doesn't automatically redraw them.

    I currently need to manually tell AppKit to redraw the saturation and brightness slider whenever the hue value changes. This is done by setting needsDisplay to true. This also needs to be implemented similarly for value changes of all the remaining sliders and other external changes. AppKit also supports automatic Observation of properties from @Observable types. Leverage this by adding the @Observable macro to a Swift class. All mutable variables then participate in the observation system.

    The sliders are implemented as subclasses of NSSliderCell and customize the appearance by overriding certain draw methods like drawKnob. I only need to access the properties of my new ColorModel inside the drawKnob method. AppKit tracks each access and redraws whenever any accessed properties change. No need for manually setting needsDisplay to true anymore.

    This works for any draw method that is called as part of NSView draw like the drawKnob or this drawBar method from NSSliderCell. NSView.draw(_:) is only one method that supports observation. updateConstraints(), layout(), updateLayer(), and NSViewController equivalents support Observation too.

    UIKit has even more methods that extend beyond UIView and UIViewController to UIButton, UICollectionViewCell and more. You can back-deploy the integration to macOS 15 by adding NSObservationTrackingEnabled to your Info.plist. And to iOS 18 by adding UIObservationTrackingEnabled. It is enabled by default with the 2026 releases and later. For a closer look at Observation Tracking in UIKit watch "What's new in UIKit" from WWDC25. Okay, here it is in action. I will increase the brightness.

    And change the hue to red.

    Great, all sliders update and the new color is sent to the light over the network.

    Adopting @Observable is a great start to get automatic updates in your NSView and NSViewController. It also makes it easier to move to SwiftUI when you want to implement something new. Speaking of something new, I have an idea for a different Color Picker design.

    Hue starts with red, progresses through all the colors, and returns back to red. I would like to represent this as a circular slider.

    I can represent Saturation and Brightness as two Semicircles inside the outer hue ring. Right in the middle I want to draw a preview of the resulting color as a circle. The whole drawing code and interaction will completely change, so this is a great time to move to SwiftUI. I can reuse the same @Observable ColorModel from the previous NSSlider based Color Picker. In the view's body I use the Canvas view, which gives me access to an immediate mode drawing API. Canvas is very similar to drawRect in AppKit or UIKit. Each redraw calls your closure with a fresh GraphicsContext, and you issue draw commands like strokes, fills, transforms and filters, directly against it. You can also reuse your existing CoreGraphics drawing code in SwiftUI by calling the withCGContext API. For an introduction to Canvas, watch "Add rich graphics to your SwiftUI app" from WWDC21.

    If you want to know how you can combine SwiftUI with your own Metal Shaders watch "Compose advanced graphics effects with SwiftUI" from WWDC26. I still have a lot of places where the color picker is embedded in an NSView hierarchy. I can wrap my SwiftUI view in an NSHostingView, which is a subclass of NSView.

    Because I have already moved my model to @Observable, this is really all I need to do. For an in-depth tour of NSHostingView and related types, watch "Use SwiftUI with AppKit" and "Use SwiftUI with UIKit" from WWDC 2022. Before I show you this new Color Picker in action, I want to add one more feature. I want to quickly reset the Brightness and Saturation to 100% with a single force click, which is a firm press on the trackpad. I already have an NSGestureRecognizer for this that I use in other parts of the app. I can bring this to a new SwiftUI View using NSGestureRecognizerRepresentable.

    I start by creating a new struct that conforms to the NSGestureRecognizerRepresentable protocol. In makeNSGestureRecognizer I initialize and return my NSGestureRecognizer subclass. ForceClickGestureRecognizer is the type that I use in other parts of my app. It recognizes when the pressure stage 2 is reached, which indicates that enough pressure has been applied to trigger a force click.

    handleNSGestureRecognizerAction is called when the gesture is recognized. This is the right place to reset the saturation and brightness to 100%. Back in the HSBColorPicker SwiftUI view, I can now add this gesture with the .gesture modifier, just like a SwiftUI Gesture.

    The ForceClickReset gesture works together with the existing drag gesture without any other changes. SwiftUI also comes with more representable protocols like NSViewRepresentable that allows you to embed NSViews into your SwiftUI views. A Force Click is not possible with all input devices, like the Magic Mouse or the trackpad of the MacBook Neo. To make sure everyone can take advantage of this shortcut, I need to add a different way to access this feature. In this case I will add a menu item with a keyboard shortcut. My app is using AppKit's NSMenu for the main menu. I will explain how to add the new menu item using SwiftUI. I start by creating a new struct that conforms to the View protocol. It has access to the shared ColorModel. In the view's body I create a Button with a label and an action closure which resets the brightness and saturation to 100%.

    Wrapping the modification in withAnimation makes SwiftUI animate the change.

    To give quick access, I am adding a keyboardShortcut. I have also added a Picker with the paletteStyle, to precisely select common colors.

    I now need to add this SwiftUI View to the main menu.

    I initialize an NSHostingMenu with the ColorMenu view for that. NSHostingMenu is a subclass of NSMenu and therefore has properties like the title to configure the menu. All that is left to do is to create an NSMenuItem, set that colorMenu as its submenu, and add that item to the mainMenu. Now it is time to try it out. I will turn it on.

    And circle through all the hues to green.

    I will press the Keyboard shortcut to decrease the brightness a couple times.

    And then use the menu item to turn it off completely.

    When I force click, my NSGestureRecognizer resets the brightness.

    I incrementally added this custom SwiftUI control to my app.

    The rest of my AppKit app continues to work, just like it did before. As a final step, here is how you can bring complete SwiftUI Scenes to your app using your existing app delegate. I always wanted to give people quick access to change the color or brightness of their lights. For this, I can add a menu bar extra item. SwiftUI's MenuBarExtra scene makes this possible with just a few lines. NSHostingSceneRepresentation wraps a SwiftUI scene and allows it to be added dynamically from an existing AppKit app.

    A good place to add a scene is applicationWillFinishLaunching in your NSApplicationDelegate. Call addSceneRepresentation with your scenes, and SwiftUI will do the rest.

    If you have a MenuBarExtra scene, it is also a good idea to make it possible for people to remove and insert it again. A Settings scene is the perfect place to add a Toggle that controls whether the MenuBarExtra scene is inserted. NSHostingSceneRepresentation has an environment property that exposes the openSettings() action.

    It can be used from an @IBAction to open the settings window programmatically.

    I'm opening the settings from the apps main menu.

    And enable the menu bar extra item.

    Let me quickly open the color picker.

    And turn the light on one last time.

    To learn more about SwiftUI scenes, watch "Bring multiple windows to your SwiftUI app" from WWDC22. I have shown how you can mix SwiftUI and AppKit in different ways. The right way to combine them depends on your app and the problem you are solving.

    All APIs I have talked about today are available already on the 2026 releases or earlier. A great first step is to try out @Observable to keep your model and NSViews automatically in sync and make the transition to SwiftUI seamless. Consider SwiftUI when you implement a new component or rewrite an existing one.

    Add your existing gesture recognizer subclasses to SwiftUI views. Start with SwiftUI for new scenes even in your existing apps. And remember, there are no expectations that an app needs to be entirely SwiftUI in order to take advantage of it. Thank you for watching and thank you for building great apps!

    • 3:39 - Observation in AppKit

      // Observation in AppKit
      
      import Observation
      
      @Observable @MainActor
      final class ColorModel {
          var hue: Double = 0.6
          var saturation: Double = 1.0
          var brightness: Double = 1.0
      }
    • 6:28 - Circular color picker

      // Circular color picker
      
      import SwiftUI
      import Observation
      
      @Observable @MainActor
      final class ColorModel {
          var hue: Double = 0.6
          var saturation: Double = 1.0
          var brightness: Double = 1.0
      }
      
      // MARK: - Picker View
      
      @Animatable
      struct HSBColorPicker: View {
          var hue: Double
          var saturation: Double
          var brightness: Double
          @AnimatableIgnored var model: ColorModel
      
          init(model: ColorModel) {
              self.model = model
              self.hue = model.hue
              self.saturation = model.saturation
              self.brightness = model.brightness
          }
      
          var body: some View {
              Canvas { context, size in
                  let metrics = PickerMetrics(size: size)
                  drawPicker(in: &context, metrics: metrics, hue: hue, saturation: saturation, brightness: brightness)
              }
              .contentShape(Circle())
              .modifier(ColorPickerDragGesture(model: model))
              .aspectRatio(1, contentMode: .fit)
          }
      }
      
      // MARK: - Drag Gesture
      
      private struct ColorPickerDragGesture: ViewModifier {
          var model: ColorModel
      
          private enum Ring { case hue, saturation, brightness }
          @State private var draggedRing: Ring?
      
          func body(content: Content) -> some View {
              GeometryReader { proxy in
                  content.gesture(
                      DragGesture(minimumDistance: 0, coordinateSpace: .local)
                          .onChanged { onDrag(to: $0.location, size: proxy.size) }
                          .onEnded { _ in draggedRing = nil }
                  )
              }
          }
      
          private func onDrag(to location: CGPoint, size: CGSize) {
              let metrics = PickerMetrics(size: size)
              let point = CGPoint(x: location.x - metrics.mid.x, y: location.y - metrics.mid.y)
              if draggedRing == nil {
                  let distance = hypot(point.x, point.y)
                  if distance >= metrics.radius - metrics.ringWidth - metrics.gap / 2 {
                      draggedRing = .hue
                  } else if distance >= metrics.radius - metrics.ringWidth * 2 - metrics.gap {
                      draggedRing = point.x > 0 ? .brightness : .saturation
                  }
              }
              switch draggedRing {
              case .hue: model.hue = (angle0To2Pi(point) / (2 * .pi) + 0.25).truncatingRemainder(dividingBy: 1)
              case .saturation: model.saturation = leftSemicircleValue(point)
              case .brightness: model.brightness = 1 - rightSemicircleValue(point)
              case nil: break
              }
          }
      }
      
      // MARK: - Metrics
      
      struct PickerMetrics {
          let mid: CGPoint
          let radius: CGFloat
          let ringWidth: CGFloat
          let gap: CGFloat = 8
      
          init(size: CGSize) {
              let border: CGFloat = 1 // reserve room so the outer ring's stroke isn't clipped
              mid = CGPoint(x: size.width / 2, y: size.height / 2)
              radius = (min(size.width, size.height) - 2 * border) / 2
              ringWidth = radius / 3
          }
      
          var diameter: CGFloat { radius * 2 }
          var innerRadius: CGFloat { (diameter - 2 * ringWidth - gap) / 2 }
          var centerRadius: CGFloat { radius - 2 * ringWidth - gap }
      }
      
      // MARK: - Geometry Helpers
      
      func angle0To2Pi(_ point: CGPoint) -> CGFloat {
          let a = atan2(point.y, point.x)
          return a >= 0 ? a : a + 2 * .pi
      }
      
      func rightSemicircleValue(_ point: CGPoint) -> CGFloat {
          let angle = atan2(point.y, point.x)
          return point.x >= 0 ? (angle + .pi / 2) / .pi : (point.y >= 0 ? 1 : 0)
      }
      
      func leftSemicircleValue(_ point: CGPoint) -> CGFloat {
          guard point.x <= 0 else { return point.y >= 0 ? 1 : 0 }
          return (atan2(point.y, -point.x) + .pi / 2) / .pi
      }
      
      private extension Path {
          /// A circle whose stroke of `lineWidth` lands inside `radius`.
          init(ring radius: CGFloat, center: CGPoint, lineWidth: CGFloat) {
              let inset = radius - lineWidth / 2
              self.init(ellipseIn: CGRect(x: center.x - inset, y: center.y - inset, width: inset * 2, height: inset * 2))
          }
      }
      
      // MARK: - Drawing
      
      private func drawPicker(in context: inout GraphicsContext, metrics: PickerMetrics, hue: Double, saturation: Double, brightness: Double) {
          drawHueRing(in: &context, metrics: metrics, hue: hue, saturation: saturation, brightness: brightness)
          drawValueRings(in: &context, metrics: metrics, hue: hue, saturation: saturation, brightness: brightness)
          drawCenter(in: &context, metrics: metrics, hue: hue, saturation: saturation, brightness: brightness)
      }
      
      private func drawHueRing(in context: inout GraphicsContext, metrics: PickerMetrics, hue: Double, saturation: Double, brightness: Double) {
          let ring = Path(ring: metrics.radius, center: metrics.mid, lineWidth: metrics.ringWidth)
          // A custom metal shader would be work great here as well
          let colors = stride(from: 0.0, through: 1, by: 1.0 / 64).map { Color(hue: $0, saturation: saturation, brightness: brightness) }
          context.stroke(ring, with: .conicGradient(Gradient(colors: colors), center: metrics.mid, angle: .degrees(-90)), lineWidth: metrics.ringWidth)
          context.stroke(ring.strokedPath(StrokeStyle(lineWidth: metrics.ringWidth)), with: .color(.black), lineWidth: 1)
          // Tick marks are left as a fun exercise for the reader.
          drawKnob(in: &context, metrics: metrics, radius: metrics.radius, rotation: 2 * .pi * hue + .pi)
      }
      
      private func drawValueRings(in context: inout GraphicsContext, metrics: PickerMetrics, hue: Double, saturation: Double, brightness: Double) {
          drawSemicircle(in: &context, metrics: metrics, start: .degrees(90), conicAngle: .degrees(0), stops: (0...1).map {
              Gradient.Stop(color: Color(hue: hue, saturation: 1 - Double($0), brightness: brightness), location: 0.25 + Double($0) * 0.5)
          })
          drawSemicircle(in: &context, metrics: metrics, start: .degrees(270), conicAngle: .degrees(180), stops: (0...1).map {
              Gradient.Stop(color: Color(hue: hue, saturation: saturation, brightness: 1 - Double($0)), location: 0.25 + Double($0) * 0.5)
          })
          drawKnob(in: &context, metrics: metrics, radius: metrics.innerRadius, rotation: .pi * (1 - saturation))
          drawKnob(in: &context, metrics: metrics, radius: metrics.innerRadius, rotation: .pi * (1 - brightness) + .pi)
      }
      
      private func drawSemicircle(in context: inout GraphicsContext, metrics: PickerMetrics, start: Angle, conicAngle: Angle, stops: [Gradient.Stop]) {
          var path = Path()
          path.addArc(center: metrics.mid, radius: metrics.innerRadius - metrics.ringWidth / 2, startAngle: start, endAngle: start + .degrees(180), clockwise: false)
          let band = path.strokedPath(StrokeStyle(lineWidth: metrics.ringWidth))
          context.fill(band, with: .conicGradient(Gradient(stops: stops), center: metrics.mid, angle: conicAngle))
          context.stroke(band, with: .color(.black), lineWidth: 1)
          // Tick marks are left as a fun exercise for the reader.
      }
      
      private func drawCenter(in context: inout GraphicsContext, metrics: PickerMetrics, hue: Double, saturation: Double, brightness: Double) {
          let r = metrics.centerRadius
          let disc = Path(ellipseIn: CGRect(x: metrics.mid.x - r, y: metrics.mid.y - r, width: r * 2, height: r * 2))
          context.fill(disc, with: .color(Color(hue: hue, saturation: saturation, brightness: brightness)))
          context.stroke(disc, with: .color(.black))
      }
      
      private func drawKnob(in context: inout GraphicsContext, metrics: PickerMetrics, radius: CGFloat, rotation: CGFloat) {
          let lineWidth: CGFloat = 5
          let inset: CGFloat = 3 + lineWidth / 2
          var path = Path()
          path.move(to: CGPoint(x: 0, y: radius - metrics.ringWidth + inset))
          path.addLine(to: CGPoint(x: 0, y: radius - inset))
          path = path.applying(CGAffineTransform(rotationAngle: rotation))
          path = path.applying(CGAffineTransform(translationX: metrics.mid.x, y: metrics.mid.y))
          context.stroke(path, with: .color(.black.opacity(0.8)), style: StrokeStyle(lineWidth: lineWidth + 1, lineCap: .round))
          context.stroke(path, with: .color(.white), style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
      }
      
      #Preview {
          @Previewable @State var model = ColorModel()
          HSBColorPicker(model: model)
              .frame(width: 320, height: 320)
              .padding()
      }
    • 7:21 - Hosting SwiftUI in AppKit

      // Hosting SwiftUI in AppKit
      
      NSHostingView(
          rootView: HSBColorPicker(model: model)
      )
    • 8:14 - Mix NSGestureRecognizer with SwiftUI

      // Mix NSGestureRecognizer with SwiftUI
      
      import SwiftUI
      import AppKit
      
      @Observable @MainActor
      final class ColorModel {
          var hue: Double = 0.6
          var saturation: Double = 1.0
          var brightness: Double = 1.0
      }
      
      struct ForceClickReset: NSGestureRecognizerRepresentable {
          var model: ColorModel
      
          func makeNSGestureRecognizer(context: Context) -> ForceClickGestureRecognizer {
              ForceClickGestureRecognizer()
          }
      
          func handleNSGestureRecognizerAction(_ recognizer: ForceClickGestureRecognizer, context: Context) {
              withAnimation {
                  model.saturation = 1
                  model.brightness = 1
              }
          }
      }
      
      final class ForceClickGestureRecognizer: NSGestureRecognizer {
          private var didActivate = false
      
          override func pressureChange(with event: NSEvent) {
              if event.stage >= 2 && !didActivate {
                  didActivate = true
                  state = .ended
              }
          }
      
          override func mouseDown(with event: NSEvent) {
              didActivate = false
              state = .possible
          }
      
          override func mouseUp(with event: NSEvent) {
              didActivate = false
              state = .possible
          }
      }
    • 9:42 - Adding ColorMenu to the Main Menu

      // Adding ColorMenu to the Main Menu
      
      import AppKit
      import SwiftUI
      import Observation
      
      @Observable @MainActor
      final class ColorModel {
          var hue: Double = 0.6
          var saturation: Double = 1.0
          var brightness: Double = 1.0
      }
      
      // Menu definition in SwiftUI.
      struct ColorMenu: View {
          var model: ColorModel
      
          private static let hues: [(name: String, hue: Double)] = [
              ("Red", 0), ("Yellow", 0.17), ("Green", 0.33), ("Cyan", 0.5), ("Blue", 0.67), ("Purple", 0.83),
          ]
      
          var body: some View {
              Button("Full Intensity") {
                  withAnimation {
                      model.saturation = 1
                      model.brightness = 1
                  }
              }
              .keyboardShortcut(.upArrow, modifiers: [.command, .shift])
      
              Button("Blackout") {
                  withAnimation {
                      model.brightness = 0
                  }
              }
              .keyboardShortcut(.downArrow, modifiers: [.command, .shift])
      
              Divider()
      
              Button("Brighten") {
                  withAnimation {
                      model.brightness = min(1, model.brightness + 0.1)
                  }
              }
              .keyboardShortcut(.upArrow, modifiers: .command)
      
              Button("Dim") {
                  withAnimation {
                      model.brightness = max(0, model.brightness - 0.1)
                  }
              }
              .keyboardShortcut(.downArrow, modifiers: .command)
      
              Divider()
      
              Picker("Color", selection: Bindable(model).hue) {
                  ForEach(Self.hues, id: \.hue) { entry in
                      Label(entry.name, systemImage: "circle.fill")
                          .tint(Color(hue: entry.hue, saturation: 1, brightness: 1))
                          .tag(entry.hue)
                  }
              }
              .pickerStyle(.palette)
          }
      }
      
      @MainActor
      class AppDelegate: NSObject, NSApplicationDelegate {
          let colorModel = ColorModel()
      
          func setupMainMenu() {
              let mainMenu = NSMenu()
      
              let colorMenu = NSHostingMenu(rootView: ColorMenu(model: colorModel))
              colorMenu.title = "Color"
      
              let colorMenuItem = NSMenuItem()
              colorMenuItem.submenu = colorMenu
              mainMenu.addItem(colorMenuItem)
          }
      }
      
      #Preview {
          Menu("Color") {
              ColorMenu(model: ColorModel())
      
          }.padding()
      }
    • 11:36 - Adding SwiftUI scenes dynamically

      // Adding SwiftUI scenes dynamically
      
      import AppKit
      import SwiftUI
      import Observation
      
      @MainActor
      class AppDelegate: NSObject, NSApplicationDelegate {
          let model = AppModel()
          var openSettingsAction: (() -> Void)?
      
          func applicationWillFinishLaunching(_ notification: Notification) {
              let scenes = NSHostingSceneRepresentation {
                  LightMenuBarExtra(appModel: model)
                  LightSettings(appModel: model)
              }
              NSApplication.shared.addSceneRepresentation(scenes)
              openSettingsAction = {
                  scenes.environment.openSettings()
              }
          }
      
          @IBAction func openSettings(_ sender: Any?) {
              openSettingsAction?()
          }
      }
      
      @Observable @MainActor
      final class ColorModel {
          var hue: Double = 0.6
          var saturation: Double = 1.0
          var brightness: Double = 1.0
      
          var color: Color {
              Color(hue: hue, saturation: saturation, brightness: brightness)
          }
      }
      
      @Observable @MainActor
      final class AppModel {
          var showMenuBarExtra: Bool = true
      
          var colorModel = ColorModel()
      
          var startUniverse: Int = 1
          var numberOfPixels: Int = 50
      
          var maxBrightness: Double = 1.0
          var isConnected: Bool = false
      }
      
      struct LightMenuBarExtra: Scene {
          var appModel: AppModel
      
          var body: some Scene {
              MenuBarExtra("Light Mix", systemImage: "lightbulb.fill", isInserted: Bindable(appModel).showMenuBarExtra) {
                  MenuBarContent(appModel: appModel)
              }
              .menuBarExtraStyle(.window)
          }
      }
      
      
      struct MenuBarContent: View {
          @Bindable var appModel: AppModel
      
          var body: some View {
              // TODO: Use HSBColorPicker
              VStack {
                  RoundedRectangle(cornerRadius: 10)
                      .fill(appModel.colorModel.color)
                      .frame(height: 80)
                      .overlay(RoundedRectangle(cornerRadius: 10).stroke(.black.opacity(0.1)))
      
                  LabeledContent("Brightness") {
                      Slider(value: $appModel.colorModel.brightness)
                          .frame(width: 140)
                  }
              }
              .padding()
              .frame(width: 280)
          }
      }
      
      struct LightSettings: Scene {
          var appModel: AppModel
      
          var body: some Scene {
              Settings {
                  SettingsView(appModel: appModel)
              }
          }
      }
      
      struct SettingsView: View {
          var appModel: AppModel
      
          var body: some View {
              TabView {
                  Tab("General", systemImage: "gearshape") {
                      GeneralTab(appModel: appModel)
                  }
                  Tab("Output", systemImage: "antenna.radiowaves.left.and.right") {
                      OutputTab(appModel: appModel)
                  }
                  Tab("About", systemImage: "info.circle") {
                      AboutTab()
                  }
              }
              .formStyle(.grouped)
              .scrollDisabled(true)
              .frame(width: 460)
              .fixedSize(horizontal: false, vertical: true)
          }
      }
      
      struct GeneralTab: View {
          @Bindable var appModel: AppModel
      
          var body: some View {
              Form {
                  Section("Appearance") {
                      Toggle("Show in Menu Bar", isOn: $appModel.showMenuBarExtra)
                  }
                  Section("DMX Configuration") {
                      LabeledContent("Start Universe") {
                          TextField("", value: $appModel.startUniverse, format: .number)
                              .textFieldStyle(.roundedBorder)
                              .frame(width: 80)
                      }
                      LabeledContent("Number of Pixels") {
                          TextField("", value: $appModel.numberOfPixels, format: .number)
                              .textFieldStyle(.roundedBorder)
                              .frame(width: 80)
                      }
                  }
              }
          }
      }
      
      struct OutputTab: View {
          @Bindable var appModel: AppModel
      
          var body: some View {
              Form {
                  Section("Output") {
                      LabeledContent("Max Brightness") {
                          HStack {
                              Slider(value: $appModel.maxBrightness, in: 0...1)
                              Text("\(Int((appModel.maxBrightness * 100).rounded()))%")
                                  .monospacedDigit()
                                  .foregroundStyle(.secondary)
                                  .frame(width: 40, alignment: .trailing)
                          }
                      }
                  }
              }
          }
      }
      
      struct AboutTab: View {
          var body: some View {
              VStack(spacing: 16) {
                  Image(systemName: "lightbulb.fill")
                      .font(.system(size: 48))
                      .foregroundStyle(.yellow.gradient)
      
                  Text("Light Mix")
                      .font(.title2.bold())
      
                  Text("WWDC26 — Bring SwiftUI to your AppKit and UIKit App")
                      .multilineTextAlignment(.center)
                      .foregroundStyle(.secondary)
              }
          }
      }
      
      #Preview("Menu Bar") {
          MenuBarContent(appModel: AppModel())
      }
      
      #Preview("Settings") {
          SettingsView(appModel: AppModel())
      }
    • 0:00 - Introduction
    • How SwiftUI is designed to work alongside existing AppKit and UIKit apps — already used in Logic Pro plugins, Xcode's Coding Assistant, and even AppKit controls like NSSlider, NSSwitch, and NSSegmentedControl. Previews the agenda using a sample lighting-control app: Observation in AppKit, hosting SwiftUI in AppKit, AppKit gestures in SwiftUI, SwiftUI in the main menu, and SwiftUI scenes in AppKit.

    • 2:33 - Observation in AppKit
    • Replace manual needsDisplay invalidation with automatic updates by adopting @Observable on your model. AppKit (and UIKit) automatically track property reads in draw, updateConstraints, layout, updateLayer, and their NSViewController equivalents — so dependent views redraw when the model changes. Back-deployable to macOS 15 / iOS 18 via NSObservationTrackingEnabled / UIObservationTrackingEnabled, and on by default with the 2026 releases.

    • 5:41 - Hosting SwiftUI in AppKit
    • When a new feature would require very different drawing or interaction code, it's a good moment to move to SwiftUI. Reimplement the color picker as a SwiftUI Canvas — an immediate-mode drawing API similar to drawRect, with withCGContext for reusing existing CoreGraphics code — then embed the SwiftUI view in the existing AppKit hierarchy with NSHostingView.

    • 7:48 - AppKit gestures in SwiftUI
    • Reuse an existing NSGestureRecognizer subclass directly in a SwiftUI view via the new NSGestureRecognizerRepresentable protocol. Implement makeNSGestureRecognizer and handleNSGestureRecognizerAction, then attach it with the standard .gesture modifier — shown adding a Force Click to reset brightness and saturation alongside an existing drag gesture.

    • 9:16 - SwiftUI in the main menu
    • Build a menu in SwiftUI as a regular View — Buttons with actions, keyboard shortcuts, and a palette-style Picker — then add it to the AppKit main menu using NSHostingMenu (an NSMenu subclass) wrapped in an NSMenuItem. Ensures features like the Force Click reset are also available to people on input devices that don't support force gestures.

    • 11:30 - SwiftUI scenes in AppKit
    • Use NSHostingSceneRepresentation to add complete SwiftUI scenes to an app with the AppKit lifecycle. Add a MenuBarExtra for quick light controls, and a Settings scene with a Toggle that inserts or removes the MenuBarExtra dynamically — all from your existing NSApplicationDelegate.

    • 13:04 - Next steps
    • Start using @Observable to keep models and NSViews in sync, consider SwiftUI for new views, reuse existing gestures via the representable protocol, and use SwiftUI for new scenes. There's no expectation that an app needs to be entirely SwiftUI to take advantage of it.

Developer Footer

  • Videos
  • WWDC26
  • Use SwiftUI with AppKit and UIKit
  • Open Menu Close Menu
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    • App Store
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • Icon Composer
    • SF Symbols
    Open Menu Close Menu
    • Accessibility
    • Accessories
    • Apple Intelligence
    • Audio & Video
    • Augmented Reality
    • Business
    • Design
    • Distribution
    • Education
    • Games
    • Health & Fitness
    • In-App Purchase
    • Localization
    • Maps & Location
    • Machine Learning & AI
    • Security
    • Safari & Web
    Open Menu Close Menu
    • Documentation
    • Downloads
    • Sample Code
    • Videos
    Open Menu Close Menu
    • Help Guides & Articles
    • Contact Us
    • Forums
    • Feedback & Bug Reporting
    • System Status
    Open Menu Close Menu
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles
    • Feedback Assistant
    Open Menu Close Menu
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program
    • Mini Apps Partner Program
    • News Partner Program
    • Video Partner Program
    • Security Bounty Program
    • Security Research Device Program
    Open Menu Close Menu
    • Meet with Apple
    • Apple Developer Centers
    • App Store Awards
    • Apple Design Awards
    • Apple Developer Academies
    • WWDC
    Read the latest news.
    Get the Apple Developer app.
    Copyright © 2026 Apple Inc. All rights reserved.
    Terms of Use Privacy Policy Agreements and Guidelines