{ "$schema": "https://raw.githubusercontent.com/guitaripod/wwdc-sessions/master/schema/session.schema.json", "schemaVersion": 1, "id": "wwdc2026-272", "event": "wwdc2026", "eventName": "WWDC26", "year": 2026, "eventContentId": 272, "title": "Use SwiftUI with AppKit and UIKit", "description": "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.", "url": "https://developer.apple.com/videos/play/wwdc2026/272", "sosumiURL": "https://sosumi.ai/videos/play/wwdc2026/272", "type": "Video", "publishedAt": "2026-06-08T14:00:00-07:00", "platforms": [ "iOS", "iPadOS", "macOS" ], "keywords": [ "screenshots" ], "topics": [ "App Services", "SwiftUI & UI Frameworks" ], "primaryTopic": "SwiftUI & UI Frameworks", "duration": 843, "hasTranscript": true, "transcriptWordCount": 1893, "availableLanguages": [ "eng", "fra", "jpn", "kor", "por", "spa", "zho" ], "media": { "hls": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/272/5/e1e4aa9a-cbe2-4f83-9cea-3dcaae19afd6/cmaf.m3u8", "downloadHLS": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/272/5/e1e4aa9a-cbe2-4f83-9cea-3dcaae19afd6/cmaf-download.m3u8", "downloadSD": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/272/5/e1e4aa9a-cbe2-4f83-9cea-3dcaae19afd6/downloads/wwdc2026-272_sd.mp4", "downloadHD": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/272/5/e1e4aa9a-cbe2-4f83-9cea-3dcaae19afd6/downloads/wwdc2026-272_hd.mp4", "duration": 843 }, "codeSnippets": [ { "title": "Observation in AppKit", "language": "swift", "startTimeSeconds": 219, "code": "// Observation in AppKit\n\nimport Observation\n\n@Observable @MainActor\nfinal class ColorModel {\n var hue: Double = 0.6\n var saturation: Double = 1.0\n var brightness: Double = 1.0\n}" }, { "title": "Circular color picker", "language": "swift", "startTimeSeconds": 388, "code": "// Circular color picker\n\nimport SwiftUI\nimport Observation\n\n@Observable @MainActor\nfinal class ColorModel {\n var hue: Double = 0.6\n var saturation: Double = 1.0\n var brightness: Double = 1.0\n}\n\n// MARK: - Picker View\n\n@Animatable\nstruct HSBColorPicker: View {\n var hue: Double\n var saturation: Double\n var brightness: Double\n @AnimatableIgnored var model: ColorModel\n\n init(model: ColorModel) {\n self.model = model\n self.hue = model.hue\n self.saturation = model.saturation\n self.brightness = model.brightness\n }\n\n var body: some View {\n Canvas { context, size in\n let metrics = PickerMetrics(size: size)\n drawPicker(in: &context, metrics: metrics, hue: hue, saturation: saturation, brightness: brightness)\n }\n .contentShape(Circle())\n .modifier(ColorPickerDragGesture(model: model))\n .aspectRatio(1, contentMode: .fit)\n }\n}\n\n// MARK: - Drag Gesture\n\nprivate struct ColorPickerDragGesture: ViewModifier {\n var model: ColorModel\n\n private enum Ring { case hue, saturation, brightness }\n @State private var draggedRing: Ring?\n\n func body(content: Content) -> some View {\n GeometryReader { proxy in\n content.gesture(\n DragGesture(minimumDistance: 0, coordinateSpace: .local)\n .onChanged { onDrag(to: $0.location, size: proxy.size) }\n .onEnded { _ in draggedRing = nil }\n )\n }\n }\n\n private func onDrag(to location: CGPoint, size: CGSize) {\n let metrics = PickerMetrics(size: size)\n let point = CGPoint(x: location.x - metrics.mid.x, y: location.y - metrics.mid.y)\n if draggedRing == nil {\n let distance = hypot(point.x, point.y)\n if distance >= metrics.radius - metrics.ringWidth - metrics.gap / 2 {\n draggedRing = .hue\n } else if distance >= metrics.radius - metrics.ringWidth * 2 - metrics.gap {\n draggedRing = point.x > 0 ? .brightness : .saturation\n }\n }\n switch draggedRing {\n case .hue: model.hue = (angle0To2Pi(point) / (2 * .pi) + 0.25).truncatingRemainder(dividingBy: 1)\n case .saturation: model.saturation = leftSemicircleValue(point)\n case .brightness: model.brightness = 1 - rightSemicircleValue(point)\n case nil: break\n }\n }\n}\n\n// MARK: - Metrics\n\nstruct PickerMetrics {\n let mid: CGPoint\n let radius: CGFloat\n let ringWidth: CGFloat\n let gap: CGFloat = 8\n\n init(size: CGSize) {\n let border: CGFloat = 1 // reserve room so the outer ring's stroke isn't clipped\n mid = CGPoint(x: size.width / 2, y: size.height / 2)\n radius = (min(size.width, size.height) - 2 * border) / 2\n ringWidth = radius / 3\n }\n\n var diameter: CGFloat { radius * 2 }\n var innerRadius: CGFloat { (diameter - 2 * ringWidth - gap) / 2 }\n var centerRadius: CGFloat { radius - 2 * ringWidth - gap }\n}\n\n// MARK: - Geometry Helpers\n\nfunc angle0To2Pi(_ point: CGPoint) -> CGFloat {\n let a = atan2(point.y, point.x)\n return a >= 0 ? a : a + 2 * .pi\n}\n\nfunc rightSemicircleValue(_ point: CGPoint) -> CGFloat {\n let angle = atan2(point.y, point.x)\n return point.x >= 0 ? (angle + .pi / 2) / .pi : (point.y >= 0 ? 1 : 0)\n}\n\nfunc leftSemicircleValue(_ point: CGPoint) -> CGFloat {\n guard point.x <= 0 else { return point.y >= 0 ? 1 : 0 }\n return (atan2(point.y, -point.x) + .pi / 2) / .pi\n}\n\nprivate extension Path {\n /// A circle whose stroke of `lineWidth` lands inside `radius`.\n init(ring radius: CGFloat, center: CGPoint, lineWidth: CGFloat) {\n let inset = radius - lineWidth / 2\n self.init(ellipseIn: CGRect(x: center.x - inset, y: center.y - inset, width: inset * 2, height: inset * 2))\n }\n}\n\n// MARK: - Drawing\n\nprivate func drawPicker(in context: inout GraphicsContext, metrics: PickerMetrics, hue: Double, saturation: Double, brightness: Double) {\n drawHueRing(in: &context, metrics: metrics, hue: hue, saturation: saturation, brightness: brightness)\n drawValueRings(in: &context, metrics: metrics, hue: hue, saturation: saturation, brightness: brightness)\n drawCenter(in: &context, metrics: metrics, hue: hue, saturation: saturation, brightness: brightness)\n}\n\nprivate func drawHueRing(in context: inout GraphicsContext, metrics: PickerMetrics, hue: Double, saturation: Double, brightness: Double) {\n let ring = Path(ring: metrics.radius, center: metrics.mid, lineWidth: metrics.ringWidth)\n // A custom metal shader would be work great here as well\n let colors = stride(from: 0.0, through: 1, by: 1.0 / 64).map { Color(hue: $0, saturation: saturation, brightness: brightness) }\n context.stroke(ring, with: .conicGradient(Gradient(colors: colors), center: metrics.mid, angle: .degrees(-90)), lineWidth: metrics.ringWidth)\n context.stroke(ring.strokedPath(StrokeStyle(lineWidth: metrics.ringWidth)), with: .color(.black), lineWidth: 1)\n // Tick marks are left as a fun exercise for the reader.\n drawKnob(in: &context, metrics: metrics, radius: metrics.radius, rotation: 2 * .pi * hue + .pi)\n}\n\nprivate func drawValueRings(in context: inout GraphicsContext, metrics: PickerMetrics, hue: Double, saturation: Double, brightness: Double) {\n drawSemicircle(in: &context, metrics: metrics, start: .degrees(90), conicAngle: .degrees(0), stops: (0...1).map {\n Gradient.Stop(color: Color(hue: hue, saturation: 1 - Double($0), brightness: brightness), location: 0.25 + Double($0) * 0.5)\n })\n drawSemicircle(in: &context, metrics: metrics, start: .degrees(270), conicAngle: .degrees(180), stops: (0...1).map {\n Gradient.Stop(color: Color(hue: hue, saturation: saturation, brightness: 1 - Double($0)), location: 0.25 + Double($0) * 0.5)\n })\n drawKnob(in: &context, metrics: metrics, radius: metrics.innerRadius, rotation: .pi * (1 - saturation))\n drawKnob(in: &context, metrics: metrics, radius: metrics.innerRadius, rotation: .pi * (1 - brightness) + .pi)\n}\n\nprivate func drawSemicircle(in context: inout GraphicsContext, metrics: PickerMetrics, start: Angle, conicAngle: Angle, stops: [Gradient.Stop]) {\n var path = Path()\n path.addArc(center: metrics.mid, radius: metrics.innerRadius - metrics.ringWidth / 2, startAngle: start, endAngle: start + .degrees(180), clockwise: false)\n let band = path.strokedPath(StrokeStyle(lineWidth: metrics.ringWidth))\n context.fill(band, with: .conicGradient(Gradient(stops: stops), center: metrics.mid, angle: conicAngle))\n context.stroke(band, with: .color(.black), lineWidth: 1)\n // Tick marks are left as a fun exercise for the reader.\n}\n\nprivate func drawCenter(in context: inout GraphicsContext, metrics: PickerMetrics, hue: Double, saturation: Double, brightness: Double) {\n let r = metrics.centerRadius\n let disc = Path(ellipseIn: CGRect(x: metrics.mid.x - r, y: metrics.mid.y - r, width: r * 2, height: r * 2))\n context.fill(disc, with: .color(Color(hue: hue, saturation: saturation, brightness: brightness)))\n context.stroke(disc, with: .color(.black))\n}\n\nprivate func drawKnob(in context: inout GraphicsContext, metrics: PickerMetrics, radius: CGFloat, rotation: CGFloat) {\n let lineWidth: CGFloat = 5\n let inset: CGFloat = 3 + lineWidth / 2\n var path = Path()\n path.move(to: CGPoint(x: 0, y: radius - metrics.ringWidth + inset))\n path.addLine(to: CGPoint(x: 0, y: radius - inset))\n path = path.applying(CGAffineTransform(rotationAngle: rotation))\n path = path.applying(CGAffineTransform(translationX: metrics.mid.x, y: metrics.mid.y))\n context.stroke(path, with: .color(.black.opacity(0.8)), style: StrokeStyle(lineWidth: lineWidth + 1, lineCap: .round))\n context.stroke(path, with: .color(.white), style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))\n}\n\n#Preview {\n @Previewable @State var model = ColorModel()\n HSBColorPicker(model: model)\n .frame(width: 320, height: 320)\n .padding()\n}" }, { "title": "Hosting SwiftUI in AppKit", "language": "swift", "startTimeSeconds": 441, "code": "// Hosting SwiftUI in AppKit\n\nNSHostingView(\n rootView: HSBColorPicker(model: model)\n)" }, { "title": "Mix NSGestureRecognizer with SwiftUI", "language": "swift", "startTimeSeconds": 494, "code": "// Mix NSGestureRecognizer with SwiftUI\n\nimport SwiftUI\nimport AppKit\n\n@Observable @MainActor\nfinal class ColorModel {\n var hue: Double = 0.6\n var saturation: Double = 1.0\n var brightness: Double = 1.0\n}\n\nstruct ForceClickReset: NSGestureRecognizerRepresentable {\n var model: ColorModel\n\n func makeNSGestureRecognizer(context: Context) -> ForceClickGestureRecognizer {\n ForceClickGestureRecognizer()\n }\n\n func handleNSGestureRecognizerAction(_ recognizer: ForceClickGestureRecognizer, context: Context) {\n withAnimation {\n model.saturation = 1\n model.brightness = 1\n }\n }\n}\n\nfinal class ForceClickGestureRecognizer: NSGestureRecognizer {\n private var didActivate = false\n\n override func pressureChange(with event: NSEvent) {\n if event.stage >= 2 && !didActivate {\n didActivate = true\n state = .ended\n }\n }\n\n override func mouseDown(with event: NSEvent) {\n didActivate = false\n state = .possible\n }\n\n override func mouseUp(with event: NSEvent) {\n didActivate = false\n state = .possible\n }\n}" }, { "title": "Adding ColorMenu to the Main Menu", "language": "swift", "startTimeSeconds": 582, "code": "// Adding ColorMenu to the Main Menu\n\nimport AppKit\nimport SwiftUI\nimport Observation\n\n@Observable @MainActor\nfinal class ColorModel {\n var hue: Double = 0.6\n var saturation: Double = 1.0\n var brightness: Double = 1.0\n}\n\n// Menu definition in SwiftUI.\nstruct ColorMenu: View {\n var model: ColorModel\n\n private static let hues: [(name: String, hue: Double)] = [\n (\"Red\", 0), (\"Yellow\", 0.17), (\"Green\", 0.33), (\"Cyan\", 0.5), (\"Blue\", 0.67), (\"Purple\", 0.83),\n ]\n\n var body: some View {\n Button(\"Full Intensity\") {\n withAnimation {\n model.saturation = 1\n model.brightness = 1\n }\n }\n .keyboardShortcut(.upArrow, modifiers: [.command, .shift])\n\n Button(\"Blackout\") {\n withAnimation {\n model.brightness = 0\n }\n }\n .keyboardShortcut(.downArrow, modifiers: [.command, .shift])\n\n Divider()\n\n Button(\"Brighten\") {\n withAnimation {\n model.brightness = min(1, model.brightness + 0.1)\n }\n }\n .keyboardShortcut(.upArrow, modifiers: .command)\n\n Button(\"Dim\") {\n withAnimation {\n model.brightness = max(0, model.brightness - 0.1)\n }\n }\n .keyboardShortcut(.downArrow, modifiers: .command)\n\n Divider()\n\n Picker(\"Color\", selection: Bindable(model).hue) {\n ForEach(Self.hues, id: \\.hue) { entry in\n Label(entry.name, systemImage: \"circle.fill\")\n .tint(Color(hue: entry.hue, saturation: 1, brightness: 1))\n .tag(entry.hue)\n }\n }\n .pickerStyle(.palette)\n }\n}\n\n@MainActor\nclass AppDelegate: NSObject, NSApplicationDelegate {\n let colorModel = ColorModel()\n\n func setupMainMenu() {\n let mainMenu = NSMenu()\n\n let colorMenu = NSHostingMenu(rootView: ColorMenu(model: colorModel))\n colorMenu.title = \"Color\"\n\n let colorMenuItem = NSMenuItem()\n colorMenuItem.submenu = colorMenu\n mainMenu.addItem(colorMenuItem)\n }\n}\n\n#Preview {\n Menu(\"Color\") {\n ColorMenu(model: ColorModel())\n\n }.padding()\n}" }, { "title": "Adding SwiftUI scenes dynamically", "language": "swift", "startTimeSeconds": 696, "code": "// Adding SwiftUI scenes dynamically\n\nimport AppKit\nimport SwiftUI\nimport Observation\n\n@MainActor\nclass AppDelegate: NSObject, NSApplicationDelegate {\n let model = AppModel()\n var openSettingsAction: (() -> Void)?\n\n func applicationWillFinishLaunching(_ notification: Notification) {\n let scenes = NSHostingSceneRepresentation {\n LightMenuBarExtra(appModel: model)\n LightSettings(appModel: model)\n }\n NSApplication.shared.addSceneRepresentation(scenes)\n openSettingsAction = {\n scenes.environment.openSettings()\n }\n }\n\n @IBAction func openSettings(_ sender: Any?) {\n openSettingsAction?()\n }\n}\n\n@Observable @MainActor\nfinal class ColorModel {\n var hue: Double = 0.6\n var saturation: Double = 1.0\n var brightness: Double = 1.0\n\n var color: Color {\n Color(hue: hue, saturation: saturation, brightness: brightness)\n }\n}\n\n@Observable @MainActor\nfinal class AppModel {\n var showMenuBarExtra: Bool = true\n\n var colorModel = ColorModel()\n\n var startUniverse: Int = 1\n var numberOfPixels: Int = 50\n\n var maxBrightness: Double = 1.0\n var isConnected: Bool = false\n}\n\nstruct LightMenuBarExtra: Scene {\n var appModel: AppModel\n\n var body: some Scene {\n MenuBarExtra(\"Light Mix\", systemImage: \"lightbulb.fill\", isInserted: Bindable(appModel).showMenuBarExtra) {\n MenuBarContent(appModel: appModel)\n }\n .menuBarExtraStyle(.window)\n }\n}\n\n\nstruct MenuBarContent: View {\n @Bindable var appModel: AppModel\n\n var body: some View {\n // TODO: Use HSBColorPicker\n VStack {\n RoundedRectangle(cornerRadius: 10)\n .fill(appModel.colorModel.color)\n .frame(height: 80)\n .overlay(RoundedRectangle(cornerRadius: 10).stroke(.black.opacity(0.1)))\n\n LabeledContent(\"Brightness\") {\n Slider(value: $appModel.colorModel.brightness)\n .frame(width: 140)\n }\n }\n .padding()\n .frame(width: 280)\n }\n}\n\nstruct LightSettings: Scene {\n var appModel: AppModel\n\n var body: some Scene {\n Settings {\n SettingsView(appModel: appModel)\n }\n }\n}\n\nstruct SettingsView: View {\n var appModel: AppModel\n\n var body: some View {\n TabView {\n Tab(\"General\", systemImage: \"gearshape\") {\n GeneralTab(appModel: appModel)\n }\n Tab(\"Output\", systemImage: \"antenna.radiowaves.left.and.right\") {\n OutputTab(appModel: appModel)\n }\n Tab(\"About\", systemImage: \"info.circle\") {\n AboutTab()\n }\n }\n .formStyle(.grouped)\n .scrollDisabled(true)\n .frame(width: 460)\n .fixedSize(horizontal: false, vertical: true)\n }\n}\n\nstruct GeneralTab: View {\n @Bindable var appModel: AppModel\n\n var body: some View {\n Form {\n Section(\"Appearance\") {\n Toggle(\"Show in Menu Bar\", isOn: $appModel.showMenuBarExtra)\n }\n Section(\"DMX Configuration\") {\n LabeledContent(\"Start Universe\") {\n TextField(\"\", value: $appModel.startUniverse, format: .number)\n .textFieldStyle(.roundedBorder)\n .frame(width: 80)\n }\n LabeledContent(\"Number of Pixels\") {\n TextField(\"\", value: $appModel.numberOfPixels, format: .number)\n .textFieldStyle(.roundedBorder)\n .frame(width: 80)\n }\n }\n }\n }\n}\n\nstruct OutputTab: View {\n @Bindable var appModel: AppModel\n\n var body: some View {\n Form {\n Section(\"Output\") {\n LabeledContent(\"Max Brightness\") {\n HStack {\n Slider(value: $appModel.maxBrightness, in: 0...1)\n Text(\"\\(Int((appModel.maxBrightness * 100).rounded()))%\")\n .monospacedDigit()\n .foregroundStyle(.secondary)\n .frame(width: 40, alignment: .trailing)\n }\n }\n }\n }\n }\n}\n\nstruct AboutTab: View {\n var body: some View {\n VStack(spacing: 16) {\n Image(systemName: \"lightbulb.fill\")\n .font(.system(size: 48))\n .foregroundStyle(.yellow.gradient)\n\n Text(\"Light Mix\")\n .font(.title2.bold())\n\n Text(\"WWDC26 — Bring SwiftUI to your AppKit and UIKit App\")\n .multilineTextAlignment(.center)\n .foregroundStyle(.secondary)\n }\n }\n}\n\n#Preview(\"Menu Bar\") {\n MenuBarContent(appModel: AppModel())\n}\n\n#Preview(\"Settings\") {\n SettingsView(appModel: AppModel())\n}" } ], "resources": [ { "title": "Updating views automatically with observation tracking in AppKit", "url": "https://developer.apple.com/documentation/AppKit/updating-views-automatically-with-observation-tracking-in-appkit", "type": "documentation", "description": "Use Swift Observation and automatic tracking to update your views in response to model data updates.", "sosumiURL": "https://sosumi.ai/documentation/AppKit/updating-views-automatically-with-observation-tracking-in-appkit", "doccJSON": "https://developer.apple.com/tutorials/data/documentation/AppKit/updating-views-automatically-with-observation-tracking-in-appkit.json" }, { "title": "Updating views automatically with observation tracking in UIKit", "url": "https://developer.apple.com/documentation/UIKit/updating-views-automatically-with-observation-tracking-in-uikit", "type": "documentation", "description": "Use Swift Observation and automatic tracking to update your views in response to model data updates.", "sosumiURL": "https://sosumi.ai/documentation/UIKit/updating-views-automatically-with-observation-tracking-in-uikit", "doccJSON": "https://developer.apple.com/tutorials/data/documentation/UIKit/updating-views-automatically-with-observation-tracking-in-uikit.json" } ], "path": "sessions/wwdc2026/272-use-swiftui-with-appkit-and-uikit/", "files": { "metadata": "sessions/wwdc2026/272-use-swiftui-with-appkit-and-uikit/metadata.json", "readme": "sessions/wwdc2026/272-use-swiftui-with-appkit-and-uikit/README.md", "transcript": "sessions/wwdc2026/272-use-swiftui-with-appkit-and-uikit/transcript.md", "transcriptJSON": "sessions/wwdc2026/272-use-swiftui-with-appkit-and-uikit/transcript.json" } }