diff --git a/data/wwdc/all-videos.json b/data/wwdc/all-videos.json
index db56e27..7348aa0 100644
--- a/data/wwdc/all-videos.json
+++ b/data/wwdc/all-videos.json
@@ -1,5 +1,1879 @@
{
"videos": [
+ {
+ "id": "8005",
+ "year": "2026",
+ "title": "Accessibility Technologies Group Lab",
+ "topics": [
+ "Accessibility & Inclusion"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8005/",
+ "dataFile": "videos/2026-8005.json"
+ },
+ {
+ "id": "121",
+ "year": "2026",
+ "title": "Announcing Apple’s next big step for Siri and iPhone",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/121/",
+ "dataFile": "videos/2026-121.json"
+ },
+ {
+ "id": "8010",
+ "year": "2026",
+ "title": "App Store Connect Group Lab",
+ "topics": [
+ "App Store, Distribution & Marketing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8010/",
+ "dataFile": "videos/2026-8010.json"
+ },
+ {
+ "id": "8011",
+ "year": "2026",
+ "title": "Apple Intelligence Group Lab",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8011/",
+ "dataFile": "videos/2026-8011.json"
+ },
+ {
+ "id": "297",
+ "year": "2026",
+ "title": "Best practices for integrating visual intelligence in your app",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/297/",
+ "dataFile": "videos/2026-297.json"
+ },
+ {
+ "id": "339",
+ "year": "2026",
+ "title": "Bring an LLM provider to the Foundation Models framework",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/339/",
+ "dataFile": "videos/2026-339.json"
+ },
+ {
+ "id": "356",
+ "year": "2026",
+ "title": "Bringing Cyberpunk 2077 to Mac",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/356/",
+ "dataFile": "videos/2026-356.json"
+ },
+ {
+ "id": "303",
+ "year": "2026",
+ "title": "Build a responsive camera app that launches quickly",
+ "topics": [
+ "Audio & Video",
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/303/",
+ "dataFile": "videos/2026-303.json"
+ },
+ {
+ "id": "242",
+ "year": "2026",
+ "title": "Build agentic app experiences with the Foundation Models framework",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/242/",
+ "dataFile": "videos/2026-242.json"
+ },
+ {
+ "id": "334",
+ "year": "2026",
+ "title": "Build AI-powered scripts with the fm CLI and Python SDK",
+ "topics": [
+ "Design",
+ "SwiftUI & UI Frameworks",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/334/",
+ "dataFile": "videos/2026-334.json"
+ },
+ {
+ "id": "240",
+ "year": "2026",
+ "title": "Build intelligent Siri experiences with App Schemas",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/240/",
+ "dataFile": "videos/2026-240.json"
+ },
+ {
+ "id": "338",
+ "year": "2026",
+ "title": "Build live production tools for Apple Immersive Video",
+ "topics": [
+ "Audio & Video",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/338/",
+ "dataFile": "videos/2026-338.json"
+ },
+ {
+ "id": "287",
+ "year": "2026",
+ "title": "Build next-generation experiences with visionOS 27",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/287/",
+ "dataFile": "videos/2026-287.json"
+ },
+ {
+ "id": "265",
+ "year": "2026",
+ "title": "Build real-time apps and services with gRPC and Swift",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/265/",
+ "dataFile": "videos/2026-265.json"
+ },
+ {
+ "id": "359",
+ "year": "2026",
+ "title": "Build real-time neural rendering pipelines with Metal",
+ "topics": [
+ "Graphics & Games"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/359/",
+ "dataFile": "videos/2026-359.json"
+ },
+ {
+ "id": "319",
+ "year": "2026",
+ "title": "Build with the new Apple Foundation Model on Private Cloud Compute",
+ "topics": [
+ "Machine Learning & AI",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/319/",
+ "dataFile": "videos/2026-319.json"
+ },
+ {
+ "id": "261",
+ "year": "2026",
+ "title": "Build, deliver, and automate with Xcode Cloud",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/261/",
+ "dataFile": "videos/2026-261.json"
+ },
+ {
+ "id": "8018",
+ "year": "2026",
+ "title": "Camera and Photo Technologies Group Lab",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8018/",
+ "dataFile": "videos/2026-8018.json"
+ },
+ {
+ "id": "275",
+ "year": "2026",
+ "title": "Code-along: Add persistence with SwiftData",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "Swift",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/275/",
+ "dataFile": "videos/2026-275.json"
+ },
+ {
+ "id": "271",
+ "year": "2026",
+ "title": "Code-along: Build powerful drag and drop in SwiftUI",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/271/",
+ "dataFile": "videos/2026-271.json"
+ },
+ {
+ "id": "344",
+ "year": "2026",
+ "title": "Code-along: Make your app available to Siri",
+ "topics": [
+ "App Services"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/344/",
+ "dataFile": "videos/2026-344.json"
+ },
+ {
+ "id": "8007",
+ "year": "2026",
+ "title": "Coding Intelligence for Beginners Group Lab",
+ "topics": [
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8007/",
+ "dataFile": "videos/2026-8007.json"
+ },
+ {
+ "id": "8121",
+ "year": "2026",
+ "title": "Coding Intelligence, Machine Learning & AI Group Lab",
+ "topics": [
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8121/",
+ "dataFile": "videos/2026-8121.json"
+ },
+ {
+ "id": "284",
+ "year": "2026",
+ "title": "Collaborate on structured 3D models in visionOS",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/284/",
+ "dataFile": "videos/2026-284.json"
+ },
+ {
+ "id": "251",
+ "year": "2026",
+ "title": "Communicate your brand identity on iOS",
+ "topics": [
+ "Design"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/251/",
+ "dataFile": "videos/2026-251.json"
+ },
+ {
+ "id": "322",
+ "year": "2026",
+ "title": "Compose advanced graphics effects with SwiftUI",
+ "topics": [
+ "Design",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/322/",
+ "dataFile": "videos/2026-322.json"
+ },
+ {
+ "id": "290",
+ "year": "2026",
+ "title": "Craft clear names for features and labels in your app",
+ "topics": [
+ "Design"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/290/",
+ "dataFile": "videos/2026-290.json"
+ },
+ {
+ "id": "375",
+ "year": "2026",
+ "title": "Create high-quality images using Image Playground",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/375/",
+ "dataFile": "videos/2026-375.json"
+ },
+ {
+ "id": "226",
+ "year": "2026",
+ "title": "Create live communication experiences",
+ "topics": [
+ "App Services",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/226/",
+ "dataFile": "videos/2026-226.json"
+ },
+ {
+ "id": "299",
+ "year": "2026",
+ "title": "Create robust evaluations for agentic apps",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/299/",
+ "dataFile": "videos/2026-299.json"
+ },
+ {
+ "id": "227",
+ "year": "2026",
+ "title": "Create UI prototypes using agents in Xcode",
+ "topics": [
+ "Design",
+ "Developer Tools",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/227/",
+ "dataFile": "videos/2026-227.json"
+ },
+ {
+ "id": "216",
+ "year": "2026",
+ "title": "Create web extensions for Safari",
+ "topics": [
+ "App Store, Distribution & Marketing",
+ "Safari & Web"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/216/",
+ "dataFile": "videos/2026-216.json"
+ },
+ {
+ "id": "243",
+ "year": "2026",
+ "title": "Debug and profile agentic app experiences with Instruments",
+ "topics": [
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/243/",
+ "dataFile": "videos/2026-243.json"
+ },
+ {
+ "id": "207",
+ "year": "2026",
+ "title": "Deliver workout insights with HealthKit workout zones",
+ "topics": [
+ "Health & Fitness",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/207/",
+ "dataFile": "videos/2026-207.json"
+ },
+ {
+ "id": "234",
+ "year": "2026",
+ "title": "Design immersive environments for visionOS apps and the spatial web",
+ "topics": [
+ "Design",
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/234/",
+ "dataFile": "videos/2026-234.json"
+ },
+ {
+ "id": "292",
+ "year": "2026",
+ "title": "Design intuitive search experiences",
+ "topics": [
+ "Design",
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/292/",
+ "dataFile": "videos/2026-292.json"
+ },
+ {
+ "id": "252",
+ "year": "2026",
+ "title": "Design no-code games with Reality Composer Pro 3",
+ "topics": [
+ "Design",
+ "Developer Tools",
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/252/",
+ "dataFile": "videos/2026-252.json"
+ },
+ {
+ "id": "389",
+ "year": "2026",
+ "title": "Discover container machines",
+ "topics": [
+ "Developer Tools",
+ "Swift",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/389/",
+ "dataFile": "videos/2026-389.json"
+ },
+ {
+ "id": "256",
+ "year": "2026",
+ "title": "Discover generated subtitles and subtitle styles",
+ "topics": [
+ "Accessibility & Inclusion",
+ "Audio & Video"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/256/",
+ "dataFile": "videos/2026-256.json"
+ },
+ {
+ "id": "345",
+ "year": "2026",
+ "title": "Discover new capabilities in the App Intents framework",
+ "topics": [
+ "App Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/345/",
+ "dataFile": "videos/2026-345.json"
+ },
+ {
+ "id": "282",
+ "year": "2026",
+ "title": "Discover the Spatial Preview framework",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/282/",
+ "dataFile": "videos/2026-282.json"
+ },
+ {
+ "id": "285",
+ "year": "2026",
+ "title": "Discover USDKit and what’s new in OpenUSD",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/285/",
+ "dataFile": "videos/2026-285.json"
+ },
+ {
+ "id": "325",
+ "year": "2026",
+ "title": "Dive into Core AI model authoring and optimization",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/325/",
+ "dataFile": "videos/2026-325.json"
+ },
+ {
+ "id": "321",
+ "year": "2026",
+ "title": "Dive into lazy stacks and scrolling with SwiftUI",
+ "topics": [
+ "Design",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/321/",
+ "dataFile": "videos/2026-321.json"
+ },
+ {
+ "id": "397",
+ "year": "2026",
+ "title": "Dub Dub Daily: Day 2",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/397/",
+ "dataFile": "videos/2026-397.json"
+ },
+ {
+ "id": "398",
+ "year": "2026",
+ "title": "Dub Dub Daily: Day 3",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/398/",
+ "dataFile": "videos/2026-398.json"
+ },
+ {
+ "id": "399",
+ "year": "2026",
+ "title": "Dub Dub Daily: Day 4",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/399/",
+ "dataFile": "videos/2026-399.json"
+ },
+ {
+ "id": "370",
+ "year": "2026",
+ "title": "Elevate your app’s text experience with TextKit",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/370/",
+ "dataFile": "videos/2026-370.json"
+ },
+ {
+ "id": "305",
+ "year": "2026",
+ "title": "Enhance RAW image processing with Core Image",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/305/",
+ "dataFile": "videos/2026-305.json"
+ },
+ {
+ "id": "219",
+ "year": "2026",
+ "title": "Enhance the accessibility of your reading app",
+ "topics": [
+ "Accessibility & Inclusion",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/219/",
+ "dataFile": "videos/2026-219.json"
+ },
+ {
+ "id": "205",
+ "year": "2026",
+ "title": "Enhance your presence on the App Store",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/205/",
+ "dataFile": "videos/2026-205.json"
+ },
+ {
+ "id": "224",
+ "year": "2026",
+ "title": "Expand the capabilities of your Virtualization app",
+ "topics": [
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/224/",
+ "dataFile": "videos/2026-224.json"
+ },
+ {
+ "id": "343",
+ "year": "2026",
+ "title": "Explore advanced App Intents features for Siri and Apple Intelligence",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/343/",
+ "dataFile": "videos/2026-343.json"
+ },
+ {
+ "id": "279",
+ "year": "2026",
+ "title": "Explore advances in RealityKit",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/279/",
+ "dataFile": "videos/2026-279.json"
+ },
+ {
+ "id": "233",
+ "year": "2026",
+ "title": "Explore distributed inference and training with MLX",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/233/",
+ "dataFile": "videos/2026-233.json"
+ },
+ {
+ "id": "283",
+ "year": "2026",
+ "title": "Explore enhancements to visionOS object tracking",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/283/",
+ "dataFile": "videos/2026-283.json"
+ },
+ {
+ "id": "320",
+ "year": "2026",
+ "title": "Explore immersive website environments in visionOS",
+ "topics": [
+ "Safari & Web",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/320/",
+ "dataFile": "videos/2026-320.json"
+ },
+ {
+ "id": "328",
+ "year": "2026",
+ "title": "Explore numerical computing in Swift with MLX",
+ "topics": [
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/328/",
+ "dataFile": "videos/2026-328.json"
+ },
+ {
+ "id": "309",
+ "year": "2026",
+ "title": "Explore Retention Messaging in App Store Connect",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/309/",
+ "dataFile": "videos/2026-309.json"
+ },
+ {
+ "id": "281",
+ "year": "2026",
+ "title": "Extend Reality Composer Pro 3 functionality with Xcode",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/281/",
+ "dataFile": "videos/2026-281.json"
+ },
+ {
+ "id": "388",
+ "year": "2026",
+ "title": "Find and fix performance issues in your Metal games",
+ "topics": [
+ "Developer Tools",
+ "Graphics & Games"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/388/",
+ "dataFile": "videos/2026-388.json"
+ },
+ {
+ "id": "369",
+ "year": "2026",
+ "title": "Find your accessory with Bluetooth Channel Sounding",
+ "topics": [
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/369/",
+ "dataFile": "videos/2026-369.json"
+ },
+ {
+ "id": "394",
+ "year": "2026",
+ "title": "Get ready for WWDC26",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/394/",
+ "dataFile": "videos/2026-394.json"
+ },
+ {
+ "id": "215",
+ "year": "2026",
+ "title": "Get started with the HTML Model Element",
+ "topics": [
+ "Safari & Web",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/215/",
+ "dataFile": "videos/2026-215.json"
+ },
+ {
+ "id": "260",
+ "year": "2026",
+ "title": "Get the most out of Device Hub",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/260/",
+ "dataFile": "videos/2026-260.json"
+ },
+ {
+ "id": "8012",
+ "year": "2026",
+ "title": "Icon Composer for Beginners Group Lab",
+ "topics": [
+ "Design"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8012/",
+ "dataFile": "videos/2026-8012.json"
+ },
+ {
+ "id": "304",
+ "year": "2026",
+ "title": "Implement high resolution photo capture",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/304/",
+ "dataFile": "videos/2026-304.json"
+ },
+ {
+ "id": "335",
+ "year": "2026",
+ "title": "Improve your prompts by hill-climbing with Evaluations",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/335/",
+ "dataFile": "videos/2026-335.json"
+ },
+ {
+ "id": "254",
+ "year": "2026",
+ "title": "Integrate MusicKit into your app",
+ "topics": [
+ "Audio & Video",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/254/",
+ "dataFile": "videos/2026-254.json"
+ },
+ {
+ "id": "326",
+ "year": "2026",
+ "title": "Integrate on-device AI models into your app using Core AI",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/326/",
+ "dataFile": "videos/2026-326.json"
+ },
+ {
+ "id": "280",
+ "year": "2026",
+ "title": "Iterate your spatial scenes faster with Reality Composer Pro 3",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/280/",
+ "dataFile": "videos/2026-280.json"
+ },
+ {
+ "id": "101",
+ "year": "2026",
+ "title": "Keynote",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/101/",
+ "dataFile": "videos/2026-101.json"
+ },
+ {
+ "id": "111",
+ "year": "2026",
+ "title": "Keynote (ASL)",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/111/",
+ "dataFile": "videos/2026-111.json"
+ },
+ {
+ "id": "314",
+ "year": "2026",
+ "title": "Learn CSS Grid Lanes",
+ "topics": [
+ "Design",
+ "Safari & Web"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/314/",
+ "dataFile": "videos/2026-314.json"
+ },
+ {
+ "id": "223",
+ "year": "2026",
+ "title": "Live Activities essentials",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/223/",
+ "dataFile": "videos/2026-223.json"
+ },
+ {
+ "id": "246",
+ "year": "2026",
+ "title": "LLM search using Core Spotlight",
+ "topics": [
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/246/",
+ "dataFile": "videos/2026-246.json"
+ },
+ {
+ "id": "8016",
+ "year": "2026",
+ "title": "Machine Learning & AI Group Lab",
+ "topics": [
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8016/",
+ "dataFile": "videos/2026-8016.json"
+ },
+ {
+ "id": "358",
+ "year": "2026",
+ "title": "Make your game great with touch",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/358/",
+ "dataFile": "videos/2026-358.json"
+ },
+ {
+ "id": "324",
+ "year": "2026",
+ "title": "Meet Core AI",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/324/",
+ "dataFile": "videos/2026-324.json"
+ },
+ {
+ "id": "298",
+ "year": "2026",
+ "title": "Meet the Evaluations framework",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/298/",
+ "dataFile": "videos/2026-298.json"
+ },
+ {
+ "id": "253",
+ "year": "2026",
+ "title": "Meet the Music Understanding framework",
+ "topics": [
+ "Audio & Video"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/253/",
+ "dataFile": "videos/2026-253.json"
+ },
+ {
+ "id": "222",
+ "year": "2026",
+ "title": "Meet the new MetricKit",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/222/",
+ "dataFile": "videos/2026-222.json"
+ },
+ {
+ "id": "312",
+ "year": "2026",
+ "title": "Meet the Now Playing framework",
+ "topics": [
+ "Audio & Video"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/312/",
+ "dataFile": "videos/2026-312.json"
+ },
+ {
+ "id": "379",
+ "year": "2026",
+ "title": "Meet Trust Insights",
+ "topics": [
+ "Business & Education",
+ "Privacy & Security"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/379/",
+ "dataFile": "videos/2026-379.json"
+ },
+ {
+ "id": "267",
+ "year": "2026",
+ "title": "Migrate to Swift Testing",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/267/",
+ "dataFile": "videos/2026-267.json"
+ },
+ {
+ "id": "289",
+ "year": "2026",
+ "title": "Modernize your AppKit app",
+ "topics": [
+ "Machine Learning & AI",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/289/",
+ "dataFile": "videos/2026-289.json"
+ },
+ {
+ "id": "278",
+ "year": "2026",
+ "title": "Modernize your UIKit app",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/278/",
+ "dataFile": "videos/2026-278.json"
+ },
+ {
+ "id": "391",
+ "year": "2026",
+ "title": "Offer subscriptions to groups and organizations",
+ "topics": [
+ "App Store, Distribution & Marketing",
+ "Business & Education"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/391/",
+ "dataFile": "videos/2026-391.json"
+ },
+ {
+ "id": "330",
+ "year": "2026",
+ "title": "Optimize custom machine learning operations with Metal tensors",
+ "topics": [
+ "Graphics & Games"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/330/",
+ "dataFile": "videos/2026-330.json"
+ },
+ {
+ "id": "102",
+ "year": "2026",
+ "title": "Platforms State of the Union",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/102/",
+ "dataFile": "videos/2026-102.json"
+ },
+ {
+ "id": "112",
+ "year": "2026",
+ "title": "Platforms State of the Union (ASL)",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/112/",
+ "dataFile": "videos/2026-112.json"
+ },
+ {
+ "id": "8003",
+ "year": "2026",
+ "title": "Power and Performance Group Lab",
+ "topics": [
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8003/",
+ "dataFile": "videos/2026-8003.json"
+ },
+ {
+ "id": "221",
+ "year": "2026",
+ "title": "Prepare your tvOS apps for Dynamic Type",
+ "topics": [
+ "Accessibility & Inclusion",
+ "Audio & Video"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/221/",
+ "dataFile": "videos/2026-221.json"
+ },
+ {
+ "id": "250",
+ "year": "2026",
+ "title": "Principles of great design",
+ "topics": [
+ "Accessibility & Inclusion",
+ "Design",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/250/",
+ "dataFile": "videos/2026-250.json"
+ },
+ {
+ "id": "8009",
+ "year": "2026",
+ "title": "Privacy and Security Group Lab",
+ "topics": [
+ "Privacy & Security"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8009/",
+ "dataFile": "videos/2026-8009.json"
+ },
+ {
+ "id": "268",
+ "year": "2026",
+ "title": "Profile, fix, and verify: Improve app responsiveness with Instruments",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/268/",
+ "dataFile": "videos/2026-268.json"
+ },
+ {
+ "id": "203",
+ "year": "2026",
+ "title": "Read between the strokes with PencilKit",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/203/",
+ "dataFile": "videos/2026-203.json"
+ },
+ {
+ "id": "315",
+ "year": "2026",
+ "title": "Rediscover the HTML select element",
+ "topics": [
+ "Design",
+ "Safari & Web"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/315/",
+ "dataFile": "videos/2026-315.json"
+ },
+ {
+ "id": "220",
+ "year": "2026",
+ "title": "Refine accessibility for custom controls",
+ "topics": [
+ "Accessibility & Inclusion",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/220/",
+ "dataFile": "videos/2026-220.json"
+ },
+ {
+ "id": "212",
+ "year": "2026",
+ "title": "Rev up your CarPlay app",
+ "topics": [
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/212/",
+ "dataFile": "videos/2026-212.json"
+ },
+ {
+ "id": "232",
+ "year": "2026",
+ "title": "Run local agentic AI on the Mac using MLX",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/232/",
+ "dataFile": "videos/2026-232.json"
+ },
+ {
+ "id": "8015",
+ "year": "2026",
+ "title": "Safari and Web Technologies Group Lab",
+ "topics": [
+ "Safari & Web"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8015/",
+ "dataFile": "videos/2026-8015.json"
+ },
+ {
+ "id": "347",
+ "year": "2026",
+ "title": "Secure your app: mitigate risks to agentic features",
+ "topics": [
+ "Privacy & Security"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/347/",
+ "dataFile": "videos/2026-347.json"
+ },
+ {
+ "id": "201",
+ "year": "2026",
+ "title": "Secure your apps with App Attest",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Business & Education",
+ "Privacy & Security"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/201/",
+ "dataFile": "videos/2026-201.json"
+ },
+ {
+ "id": "357",
+ "year": "2026",
+ "title": "Speedrun your game port with agentic coding",
+ "topics": [
+ "Graphics & Games"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/357/",
+ "dataFile": "videos/2026-357.json"
+ },
+ {
+ "id": "393",
+ "year": "2026",
+ "title": "Supercharge your spatial workflows with Reality Composer Pro 3",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/393/",
+ "dataFile": "videos/2026-393.json"
+ },
+ {
+ "id": "341",
+ "year": "2026",
+ "title": "Support the Center Stage front camera in your iOS app",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/341/",
+ "dataFile": "videos/2026-341.json"
+ },
+ {
+ "id": "8001",
+ "year": "2026",
+ "title": "Swift Group Lab",
+ "topics": [
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8001/",
+ "dataFile": "videos/2026-8001.json"
+ },
+ {
+ "id": "8017",
+ "year": "2026",
+ "title": "SwiftData Group Lab",
+ "topics": [
+ "App Services"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8017/",
+ "dataFile": "videos/2026-8017.json"
+ },
+ {
+ "id": "8002",
+ "year": "2026",
+ "title": "SwiftUI for Beginners Group Lab",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8002/",
+ "dataFile": "videos/2026-8002.json"
+ },
+ {
+ "id": "8006",
+ "year": "2026",
+ "title": "SwiftUI Group Lab",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8006/",
+ "dataFile": "videos/2026-8006.json"
+ },
+ {
+ "id": "8120",
+ "year": "2026",
+ "title": "SwiftUI Group Lab",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8120/",
+ "dataFile": "videos/2026-8120.json"
+ },
+ {
+ "id": "213",
+ "year": "2026",
+ "title": "Translate your app using agents in Xcode",
+ "topics": [
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/213/",
+ "dataFile": "videos/2026-213.json"
+ },
+ {
+ "id": "378",
+ "year": "2026",
+ "title": "Unlock in-game content with StoreKit and Background Assets",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/378/",
+ "dataFile": "videos/2026-378.json"
+ },
+ {
+ "id": "372",
+ "year": "2026",
+ "title": "Unwrap PaperKit",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/372/",
+ "dataFile": "videos/2026-372.json"
+ },
+ {
+ "id": "286",
+ "year": "2026",
+ "title": "Use foveated streaming to bring immersive content to visionOS",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/286/",
+ "dataFile": "videos/2026-286.json"
+ },
+ {
+ "id": "272",
+ "year": "2026",
+ "title": "Use SwiftUI with AppKit and UIKit",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/272/",
+ "dataFile": "videos/2026-272.json"
+ },
+ {
+ "id": "295",
+ "year": "2026",
+ "title": "Validate your App Intents adoption with AppIntentsTesting",
+ "topics": [
+ "App Services",
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/295/",
+ "dataFile": "videos/2026-295.json"
+ },
+ {
+ "id": "8004",
+ "year": "2026",
+ "title": "visionOS Group Lab",
+ "topics": [
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8004/",
+ "dataFile": "videos/2026-8004.json"
+ },
+ {
+ "id": "8014",
+ "year": "2026",
+ "title": "watchOS Group Lab",
+ "topics": [
+ "App Services",
+ "Health & Fitness",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8014/",
+ "dataFile": "videos/2026-8014.json"
+ },
+ {
+ "id": "210",
+ "year": "2026",
+ "title": "What’s new in Apple In-App Purchase",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/210/",
+ "dataFile": "videos/2026-210.json"
+ },
+ {
+ "id": "230",
+ "year": "2026",
+ "title": "What’s new in assessment on macOS",
+ "topics": [
+ "App Services",
+ "Business & Education"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/230/",
+ "dataFile": "videos/2026-230.json"
+ },
+ {
+ "id": "237",
+ "year": "2026",
+ "title": "What’s new in image understanding",
+ "topics": [
+ "App Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/237/",
+ "dataFile": "videos/2026-237.json"
+ },
+ {
+ "id": "206",
+ "year": "2026",
+ "title": "What’s new in managing Apple devices",
+ "topics": [
+ "App Store, Distribution & Marketing",
+ "Business & Education"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/206/",
+ "dataFile": "videos/2026-206.json"
+ },
+ {
+ "id": "310",
+ "year": "2026",
+ "title": "What’s new in Shortcuts",
+ "topics": [
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/310/",
+ "dataFile": "videos/2026-310.json"
+ },
+ {
+ "id": "262",
+ "year": "2026",
+ "title": "What’s new in Swift",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/262/",
+ "dataFile": "videos/2026-262.json"
+ },
+ {
+ "id": "274",
+ "year": "2026",
+ "title": "What’s new in SwiftData",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "Swift",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/274/",
+ "dataFile": "videos/2026-274.json"
+ },
+ {
+ "id": "269",
+ "year": "2026",
+ "title": "What’s new in SwiftUI",
+ "topics": [
+ "App Services",
+ "Design",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/269/",
+ "dataFile": "videos/2026-269.json"
+ },
+ {
+ "id": "241",
+ "year": "2026",
+ "title": "What’s new in the Foundation Models framework",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/241/",
+ "dataFile": "videos/2026-241.json"
+ },
+ {
+ "id": "209",
+ "year": "2026",
+ "title": "What’s new in Wallet",
+ "topics": [
+ "App Services",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/209/",
+ "dataFile": "videos/2026-209.json"
+ },
+ {
+ "id": "204",
+ "year": "2026",
+ "title": "What’s new in WebKit for Safari 27",
+ "topics": [
+ "Machine Learning & AI",
+ "Safari & Web",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/204/",
+ "dataFile": "videos/2026-204.json"
+ },
+ {
+ "id": "258",
+ "year": "2026",
+ "title": "What’s new in Xcode 27",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/258/",
+ "dataFile": "videos/2026-258.json"
+ },
+ {
+ "id": "277",
+ "year": "2026",
+ "title": "WidgetKit foundations",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/277/",
+ "dataFile": "videos/2026-277.json"
+ },
+ {
+ "id": "122",
+ "year": "2026",
+ "title": "WWDC26 Platforms State of the Union Recap",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/122/",
+ "dataFile": "videos/2026-122.json"
+ },
+ {
+ "id": "8013",
+ "year": "2026",
+ "title": "Xcode Tips and Tricks Group Lab",
+ "topics": [
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8013/",
+ "dataFile": "videos/2026-8013.json"
+ },
+ {
+ "id": "259",
+ "year": "2026",
+ "title": "Xcode, agents, and you",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/259/",
+ "dataFile": "videos/2026-259.json"
+ },
{
"id": "324",
"year": "2025",
diff --git a/data/wwdc/by-topic/accessibility-inclusion/index.json b/data/wwdc/by-topic/accessibility-inclusion/index.json
index 2b7fcc9..2cb354c 100644
--- a/data/wwdc/by-topic/accessibility-inclusion/index.json
+++ b/data/wwdc/by-topic/accessibility-inclusion/index.json
@@ -1,8 +1,9 @@
{
"id": "accessibility-inclusion",
"name": "Accessibility & Inclusion",
- "videoCount": 57,
+ "videoCount": 63,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -14,6 +15,90 @@
"2017"
],
"videos": [
+ {
+ "id": "8005",
+ "year": "2026",
+ "title": "Accessibility Technologies Group Lab",
+ "topics": [
+ "Accessibility & Inclusion"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8005/",
+ "dataFile": "videos/2026-8005.json"
+ },
+ {
+ "id": "256",
+ "year": "2026",
+ "title": "Discover generated subtitles and subtitle styles",
+ "topics": [
+ "Accessibility & Inclusion",
+ "Audio & Video"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/256/",
+ "dataFile": "videos/2026-256.json"
+ },
+ {
+ "id": "219",
+ "year": "2026",
+ "title": "Enhance the accessibility of your reading app",
+ "topics": [
+ "Accessibility & Inclusion",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/219/",
+ "dataFile": "videos/2026-219.json"
+ },
+ {
+ "id": "221",
+ "year": "2026",
+ "title": "Prepare your tvOS apps for Dynamic Type",
+ "topics": [
+ "Accessibility & Inclusion",
+ "Audio & Video"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/221/",
+ "dataFile": "videos/2026-221.json"
+ },
+ {
+ "id": "250",
+ "year": "2026",
+ "title": "Principles of great design",
+ "topics": [
+ "Accessibility & Inclusion",
+ "Design",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/250/",
+ "dataFile": "videos/2026-250.json"
+ },
+ {
+ "id": "220",
+ "year": "2026",
+ "title": "Refine accessibility for custom controls",
+ "topics": [
+ "Accessibility & Inclusion",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/220/",
+ "dataFile": "videos/2026-220.json"
+ },
{
"id": "238",
"year": "2025",
diff --git a/data/wwdc/by-topic/app-services/index.json b/data/wwdc/by-topic/app-services/index.json
index edcca46..ac37627 100644
--- a/data/wwdc/by-topic/app-services/index.json
+++ b/data/wwdc/by-topic/app-services/index.json
@@ -1,8 +1,9 @@
{
"id": "app-services",
"name": "App Services",
- "videoCount": 172,
+ "videoCount": 198,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -15,6 +16,377 @@
"2016"
],
"videos": [
+ {
+ "id": "297",
+ "year": "2026",
+ "title": "Best practices for integrating visual intelligence in your app",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/297/",
+ "dataFile": "videos/2026-297.json"
+ },
+ {
+ "id": "240",
+ "year": "2026",
+ "title": "Build intelligent Siri experiences with App Schemas",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/240/",
+ "dataFile": "videos/2026-240.json"
+ },
+ {
+ "id": "275",
+ "year": "2026",
+ "title": "Code-along: Add persistence with SwiftData",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "Swift",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/275/",
+ "dataFile": "videos/2026-275.json"
+ },
+ {
+ "id": "271",
+ "year": "2026",
+ "title": "Code-along: Build powerful drag and drop in SwiftUI",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/271/",
+ "dataFile": "videos/2026-271.json"
+ },
+ {
+ "id": "344",
+ "year": "2026",
+ "title": "Code-along: Make your app available to Siri",
+ "topics": [
+ "App Services"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/344/",
+ "dataFile": "videos/2026-344.json"
+ },
+ {
+ "id": "226",
+ "year": "2026",
+ "title": "Create live communication experiences",
+ "topics": [
+ "App Services",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/226/",
+ "dataFile": "videos/2026-226.json"
+ },
+ {
+ "id": "345",
+ "year": "2026",
+ "title": "Discover new capabilities in the App Intents framework",
+ "topics": [
+ "App Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/345/",
+ "dataFile": "videos/2026-345.json"
+ },
+ {
+ "id": "370",
+ "year": "2026",
+ "title": "Elevate your app’s text experience with TextKit",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/370/",
+ "dataFile": "videos/2026-370.json"
+ },
+ {
+ "id": "205",
+ "year": "2026",
+ "title": "Enhance your presence on the App Store",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/205/",
+ "dataFile": "videos/2026-205.json"
+ },
+ {
+ "id": "343",
+ "year": "2026",
+ "title": "Explore advanced App Intents features for Siri and Apple Intelligence",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/343/",
+ "dataFile": "videos/2026-343.json"
+ },
+ {
+ "id": "309",
+ "year": "2026",
+ "title": "Explore Retention Messaging in App Store Connect",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/309/",
+ "dataFile": "videos/2026-309.json"
+ },
+ {
+ "id": "223",
+ "year": "2026",
+ "title": "Live Activities essentials",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/223/",
+ "dataFile": "videos/2026-223.json"
+ },
+ {
+ "id": "203",
+ "year": "2026",
+ "title": "Read between the strokes with PencilKit",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/203/",
+ "dataFile": "videos/2026-203.json"
+ },
+ {
+ "id": "201",
+ "year": "2026",
+ "title": "Secure your apps with App Attest",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Business & Education",
+ "Privacy & Security"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/201/",
+ "dataFile": "videos/2026-201.json"
+ },
+ {
+ "id": "8017",
+ "year": "2026",
+ "title": "SwiftData Group Lab",
+ "topics": [
+ "App Services"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8017/",
+ "dataFile": "videos/2026-8017.json"
+ },
+ {
+ "id": "378",
+ "year": "2026",
+ "title": "Unlock in-game content with StoreKit and Background Assets",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/378/",
+ "dataFile": "videos/2026-378.json"
+ },
+ {
+ "id": "372",
+ "year": "2026",
+ "title": "Unwrap PaperKit",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/372/",
+ "dataFile": "videos/2026-372.json"
+ },
+ {
+ "id": "272",
+ "year": "2026",
+ "title": "Use SwiftUI with AppKit and UIKit",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/272/",
+ "dataFile": "videos/2026-272.json"
+ },
+ {
+ "id": "295",
+ "year": "2026",
+ "title": "Validate your App Intents adoption with AppIntentsTesting",
+ "topics": [
+ "App Services",
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/295/",
+ "dataFile": "videos/2026-295.json"
+ },
+ {
+ "id": "8014",
+ "year": "2026",
+ "title": "watchOS Group Lab",
+ "topics": [
+ "App Services",
+ "Health & Fitness",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8014/",
+ "dataFile": "videos/2026-8014.json"
+ },
+ {
+ "id": "210",
+ "year": "2026",
+ "title": "What’s new in Apple In-App Purchase",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/210/",
+ "dataFile": "videos/2026-210.json"
+ },
+ {
+ "id": "230",
+ "year": "2026",
+ "title": "What’s new in assessment on macOS",
+ "topics": [
+ "App Services",
+ "Business & Education"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/230/",
+ "dataFile": "videos/2026-230.json"
+ },
+ {
+ "id": "237",
+ "year": "2026",
+ "title": "What’s new in image understanding",
+ "topics": [
+ "App Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/237/",
+ "dataFile": "videos/2026-237.json"
+ },
+ {
+ "id": "274",
+ "year": "2026",
+ "title": "What’s new in SwiftData",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "Swift",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/274/",
+ "dataFile": "videos/2026-274.json"
+ },
+ {
+ "id": "269",
+ "year": "2026",
+ "title": "What’s new in SwiftUI",
+ "topics": [
+ "App Services",
+ "Design",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/269/",
+ "dataFile": "videos/2026-269.json"
+ },
+ {
+ "id": "209",
+ "year": "2026",
+ "title": "What’s new in Wallet",
+ "topics": [
+ "App Services",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/209/",
+ "dataFile": "videos/2026-209.json"
+ },
{
"id": "225",
"year": "2025",
diff --git a/data/wwdc/by-topic/app-store-distribution-marketing/index.json b/data/wwdc/by-topic/app-store-distribution-marketing/index.json
index 02faa03..2f23506 100644
--- a/data/wwdc/by-topic/app-store-distribution-marketing/index.json
+++ b/data/wwdc/by-topic/app-store-distribution-marketing/index.json
@@ -1,8 +1,9 @@
{
"id": "app-store-distribution-marketing",
"name": "App Store, Distribution & Marketing",
- "videoCount": 75,
+ "videoCount": 84,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -13,6 +14,135 @@
"2017"
],
"videos": [
+ {
+ "id": "8010",
+ "year": "2026",
+ "title": "App Store Connect Group Lab",
+ "topics": [
+ "App Store, Distribution & Marketing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8010/",
+ "dataFile": "videos/2026-8010.json"
+ },
+ {
+ "id": "216",
+ "year": "2026",
+ "title": "Create web extensions for Safari",
+ "topics": [
+ "App Store, Distribution & Marketing",
+ "Safari & Web"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/216/",
+ "dataFile": "videos/2026-216.json"
+ },
+ {
+ "id": "205",
+ "year": "2026",
+ "title": "Enhance your presence on the App Store",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/205/",
+ "dataFile": "videos/2026-205.json"
+ },
+ {
+ "id": "309",
+ "year": "2026",
+ "title": "Explore Retention Messaging in App Store Connect",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/309/",
+ "dataFile": "videos/2026-309.json"
+ },
+ {
+ "id": "391",
+ "year": "2026",
+ "title": "Offer subscriptions to groups and organizations",
+ "topics": [
+ "App Store, Distribution & Marketing",
+ "Business & Education"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/391/",
+ "dataFile": "videos/2026-391.json"
+ },
+ {
+ "id": "201",
+ "year": "2026",
+ "title": "Secure your apps with App Attest",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Business & Education",
+ "Privacy & Security"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/201/",
+ "dataFile": "videos/2026-201.json"
+ },
+ {
+ "id": "378",
+ "year": "2026",
+ "title": "Unlock in-game content with StoreKit and Background Assets",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/378/",
+ "dataFile": "videos/2026-378.json"
+ },
+ {
+ "id": "210",
+ "year": "2026",
+ "title": "What’s new in Apple In-App Purchase",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/210/",
+ "dataFile": "videos/2026-210.json"
+ },
+ {
+ "id": "206",
+ "year": "2026",
+ "title": "What’s new in managing Apple devices",
+ "topics": [
+ "App Store, Distribution & Marketing",
+ "Business & Education"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/206/",
+ "dataFile": "videos/2026-206.json"
+ },
{
"id": "324",
"year": "2025",
diff --git a/data/wwdc/by-topic/audio-video/index.json b/data/wwdc/by-topic/audio-video/index.json
index b25cd9c..e688bf6 100644
--- a/data/wwdc/by-topic/audio-video/index.json
+++ b/data/wwdc/by-topic/audio-video/index.json
@@ -1,8 +1,9 @@
{
"id": "audio-video",
"name": "Audio & Video",
- "videoCount": 124,
+ "videoCount": 131,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -15,6 +16,102 @@
"2014"
],
"videos": [
+ {
+ "id": "303",
+ "year": "2026",
+ "title": "Build a responsive camera app that launches quickly",
+ "topics": [
+ "Audio & Video",
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/303/",
+ "dataFile": "videos/2026-303.json"
+ },
+ {
+ "id": "338",
+ "year": "2026",
+ "title": "Build live production tools for Apple Immersive Video",
+ "topics": [
+ "Audio & Video",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/338/",
+ "dataFile": "videos/2026-338.json"
+ },
+ {
+ "id": "256",
+ "year": "2026",
+ "title": "Discover generated subtitles and subtitle styles",
+ "topics": [
+ "Accessibility & Inclusion",
+ "Audio & Video"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/256/",
+ "dataFile": "videos/2026-256.json"
+ },
+ {
+ "id": "254",
+ "year": "2026",
+ "title": "Integrate MusicKit into your app",
+ "topics": [
+ "Audio & Video",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/254/",
+ "dataFile": "videos/2026-254.json"
+ },
+ {
+ "id": "253",
+ "year": "2026",
+ "title": "Meet the Music Understanding framework",
+ "topics": [
+ "Audio & Video"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/253/",
+ "dataFile": "videos/2026-253.json"
+ },
+ {
+ "id": "312",
+ "year": "2026",
+ "title": "Meet the Now Playing framework",
+ "topics": [
+ "Audio & Video"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/312/",
+ "dataFile": "videos/2026-312.json"
+ },
+ {
+ "id": "221",
+ "year": "2026",
+ "title": "Prepare your tvOS apps for Dynamic Type",
+ "topics": [
+ "Accessibility & Inclusion",
+ "Audio & Video"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/221/",
+ "dataFile": "videos/2026-221.json"
+ },
{
"id": "319",
"year": "2025",
diff --git a/data/wwdc/by-topic/business-education/index.json b/data/wwdc/by-topic/business-education/index.json
index 2323662..9da3b6f 100644
--- a/data/wwdc/by-topic/business-education/index.json
+++ b/data/wwdc/by-topic/business-education/index.json
@@ -1,8 +1,9 @@
{
"id": "business-education",
"name": "Business & Education",
- "videoCount": 51,
+ "videoCount": 56,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -13,6 +14,78 @@
"2017"
],
"videos": [
+ {
+ "id": "379",
+ "year": "2026",
+ "title": "Meet Trust Insights",
+ "topics": [
+ "Business & Education",
+ "Privacy & Security"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/379/",
+ "dataFile": "videos/2026-379.json"
+ },
+ {
+ "id": "391",
+ "year": "2026",
+ "title": "Offer subscriptions to groups and organizations",
+ "topics": [
+ "App Store, Distribution & Marketing",
+ "Business & Education"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/391/",
+ "dataFile": "videos/2026-391.json"
+ },
+ {
+ "id": "201",
+ "year": "2026",
+ "title": "Secure your apps with App Attest",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Business & Education",
+ "Privacy & Security"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/201/",
+ "dataFile": "videos/2026-201.json"
+ },
+ {
+ "id": "230",
+ "year": "2026",
+ "title": "What’s new in assessment on macOS",
+ "topics": [
+ "App Services",
+ "Business & Education"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/230/",
+ "dataFile": "videos/2026-230.json"
+ },
+ {
+ "id": "206",
+ "year": "2026",
+ "title": "What’s new in managing Apple devices",
+ "topics": [
+ "App Store, Distribution & Marketing",
+ "Business & Education"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/206/",
+ "dataFile": "videos/2026-206.json"
+ },
{
"id": "223",
"year": "2025",
diff --git a/data/wwdc/by-topic/design/index.json b/data/wwdc/by-topic/design/index.json
index 89a8346..6cd6642 100644
--- a/data/wwdc/by-topic/design/index.json
+++ b/data/wwdc/by-topic/design/index.json
@@ -1,8 +1,9 @@
{
"id": "design",
"name": "Design",
- "videoCount": 143,
+ "videoCount": 157,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -17,6 +18,206 @@
"2014"
],
"videos": [
+ {
+ "id": "334",
+ "year": "2026",
+ "title": "Build AI-powered scripts with the fm CLI and Python SDK",
+ "topics": [
+ "Design",
+ "SwiftUI & UI Frameworks",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/334/",
+ "dataFile": "videos/2026-334.json"
+ },
+ {
+ "id": "251",
+ "year": "2026",
+ "title": "Communicate your brand identity on iOS",
+ "topics": [
+ "Design"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/251/",
+ "dataFile": "videos/2026-251.json"
+ },
+ {
+ "id": "322",
+ "year": "2026",
+ "title": "Compose advanced graphics effects with SwiftUI",
+ "topics": [
+ "Design",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/322/",
+ "dataFile": "videos/2026-322.json"
+ },
+ {
+ "id": "290",
+ "year": "2026",
+ "title": "Craft clear names for features and labels in your app",
+ "topics": [
+ "Design"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/290/",
+ "dataFile": "videos/2026-290.json"
+ },
+ {
+ "id": "227",
+ "year": "2026",
+ "title": "Create UI prototypes using agents in Xcode",
+ "topics": [
+ "Design",
+ "Developer Tools",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/227/",
+ "dataFile": "videos/2026-227.json"
+ },
+ {
+ "id": "234",
+ "year": "2026",
+ "title": "Design immersive environments for visionOS apps and the spatial web",
+ "topics": [
+ "Design",
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/234/",
+ "dataFile": "videos/2026-234.json"
+ },
+ {
+ "id": "292",
+ "year": "2026",
+ "title": "Design intuitive search experiences",
+ "topics": [
+ "Design",
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/292/",
+ "dataFile": "videos/2026-292.json"
+ },
+ {
+ "id": "252",
+ "year": "2026",
+ "title": "Design no-code games with Reality Composer Pro 3",
+ "topics": [
+ "Design",
+ "Developer Tools",
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/252/",
+ "dataFile": "videos/2026-252.json"
+ },
+ {
+ "id": "321",
+ "year": "2026",
+ "title": "Dive into lazy stacks and scrolling with SwiftUI",
+ "topics": [
+ "Design",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/321/",
+ "dataFile": "videos/2026-321.json"
+ },
+ {
+ "id": "8012",
+ "year": "2026",
+ "title": "Icon Composer for Beginners Group Lab",
+ "topics": [
+ "Design"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8012/",
+ "dataFile": "videos/2026-8012.json"
+ },
+ {
+ "id": "314",
+ "year": "2026",
+ "title": "Learn CSS Grid Lanes",
+ "topics": [
+ "Design",
+ "Safari & Web"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/314/",
+ "dataFile": "videos/2026-314.json"
+ },
+ {
+ "id": "250",
+ "year": "2026",
+ "title": "Principles of great design",
+ "topics": [
+ "Accessibility & Inclusion",
+ "Design",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/250/",
+ "dataFile": "videos/2026-250.json"
+ },
+ {
+ "id": "315",
+ "year": "2026",
+ "title": "Rediscover the HTML select element",
+ "topics": [
+ "Design",
+ "Safari & Web"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/315/",
+ "dataFile": "videos/2026-315.json"
+ },
+ {
+ "id": "269",
+ "year": "2026",
+ "title": "What’s new in SwiftUI",
+ "topics": [
+ "App Services",
+ "Design",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/269/",
+ "dataFile": "videos/2026-269.json"
+ },
{
"id": "274",
"year": "2025",
diff --git a/data/wwdc/by-topic/developer-tools/index.json b/data/wwdc/by-topic/developer-tools/index.json
index e774c1a..0b45cb0 100644
--- a/data/wwdc/by-topic/developer-tools/index.json
+++ b/data/wwdc/by-topic/developer-tools/index.json
@@ -1,8 +1,9 @@
{
"id": "developer-tools",
"name": "Developer Tools",
- "videoCount": 199,
+ "videoCount": 218,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -17,6 +18,278 @@
"2014"
],
"videos": [
+ {
+ "id": "265",
+ "year": "2026",
+ "title": "Build real-time apps and services with gRPC and Swift",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/265/",
+ "dataFile": "videos/2026-265.json"
+ },
+ {
+ "id": "261",
+ "year": "2026",
+ "title": "Build, deliver, and automate with Xcode Cloud",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/261/",
+ "dataFile": "videos/2026-261.json"
+ },
+ {
+ "id": "8007",
+ "year": "2026",
+ "title": "Coding Intelligence for Beginners Group Lab",
+ "topics": [
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8007/",
+ "dataFile": "videos/2026-8007.json"
+ },
+ {
+ "id": "227",
+ "year": "2026",
+ "title": "Create UI prototypes using agents in Xcode",
+ "topics": [
+ "Design",
+ "Developer Tools",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/227/",
+ "dataFile": "videos/2026-227.json"
+ },
+ {
+ "id": "243",
+ "year": "2026",
+ "title": "Debug and profile agentic app experiences with Instruments",
+ "topics": [
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/243/",
+ "dataFile": "videos/2026-243.json"
+ },
+ {
+ "id": "252",
+ "year": "2026",
+ "title": "Design no-code games with Reality Composer Pro 3",
+ "topics": [
+ "Design",
+ "Developer Tools",
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/252/",
+ "dataFile": "videos/2026-252.json"
+ },
+ {
+ "id": "389",
+ "year": "2026",
+ "title": "Discover container machines",
+ "topics": [
+ "Developer Tools",
+ "Swift",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/389/",
+ "dataFile": "videos/2026-389.json"
+ },
+ {
+ "id": "205",
+ "year": "2026",
+ "title": "Enhance your presence on the App Store",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/205/",
+ "dataFile": "videos/2026-205.json"
+ },
+ {
+ "id": "388",
+ "year": "2026",
+ "title": "Find and fix performance issues in your Metal games",
+ "topics": [
+ "Developer Tools",
+ "Graphics & Games"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/388/",
+ "dataFile": "videos/2026-388.json"
+ },
+ {
+ "id": "260",
+ "year": "2026",
+ "title": "Get the most out of Device Hub",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/260/",
+ "dataFile": "videos/2026-260.json"
+ },
+ {
+ "id": "222",
+ "year": "2026",
+ "title": "Meet the new MetricKit",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/222/",
+ "dataFile": "videos/2026-222.json"
+ },
+ {
+ "id": "267",
+ "year": "2026",
+ "title": "Migrate to Swift Testing",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/267/",
+ "dataFile": "videos/2026-267.json"
+ },
+ {
+ "id": "268",
+ "year": "2026",
+ "title": "Profile, fix, and verify: Improve app responsiveness with Instruments",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/268/",
+ "dataFile": "videos/2026-268.json"
+ },
+ {
+ "id": "213",
+ "year": "2026",
+ "title": "Translate your app using agents in Xcode",
+ "topics": [
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/213/",
+ "dataFile": "videos/2026-213.json"
+ },
+ {
+ "id": "295",
+ "year": "2026",
+ "title": "Validate your App Intents adoption with AppIntentsTesting",
+ "topics": [
+ "App Services",
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/295/",
+ "dataFile": "videos/2026-295.json"
+ },
+ {
+ "id": "262",
+ "year": "2026",
+ "title": "What’s new in Swift",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/262/",
+ "dataFile": "videos/2026-262.json"
+ },
+ {
+ "id": "258",
+ "year": "2026",
+ "title": "What’s new in Xcode 27",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/258/",
+ "dataFile": "videos/2026-258.json"
+ },
+ {
+ "id": "8013",
+ "year": "2026",
+ "title": "Xcode Tips and Tricks Group Lab",
+ "topics": [
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8013/",
+ "dataFile": "videos/2026-8013.json"
+ },
+ {
+ "id": "259",
+ "year": "2026",
+ "title": "Xcode, agents, and you",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/259/",
+ "dataFile": "videos/2026-259.json"
+ },
{
"id": "270",
"year": "2025",
diff --git a/data/wwdc/by-topic/essentials/index.json b/data/wwdc/by-topic/essentials/index.json
index 796be84..8333bcd 100644
--- a/data/wwdc/by-topic/essentials/index.json
+++ b/data/wwdc/by-topic/essentials/index.json
@@ -1,8 +1,9 @@
{
"id": "essentials",
"name": "Essentials",
- "videoCount": 170,
+ "videoCount": 184,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -12,6 +13,195 @@
"2019"
],
"videos": [
+ {
+ "id": "121",
+ "year": "2026",
+ "title": "Announcing Apple’s next big step for Siri and iPhone",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/121/",
+ "dataFile": "videos/2026-121.json"
+ },
+ {
+ "id": "299",
+ "year": "2026",
+ "title": "Create robust evaluations for agentic apps",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/299/",
+ "dataFile": "videos/2026-299.json"
+ },
+ {
+ "id": "292",
+ "year": "2026",
+ "title": "Design intuitive search experiences",
+ "topics": [
+ "Design",
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/292/",
+ "dataFile": "videos/2026-292.json"
+ },
+ {
+ "id": "397",
+ "year": "2026",
+ "title": "Dub Dub Daily: Day 2",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/397/",
+ "dataFile": "videos/2026-397.json"
+ },
+ {
+ "id": "398",
+ "year": "2026",
+ "title": "Dub Dub Daily: Day 3",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/398/",
+ "dataFile": "videos/2026-398.json"
+ },
+ {
+ "id": "399",
+ "year": "2026",
+ "title": "Dub Dub Daily: Day 4",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/399/",
+ "dataFile": "videos/2026-399.json"
+ },
+ {
+ "id": "394",
+ "year": "2026",
+ "title": "Get ready for WWDC26",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/394/",
+ "dataFile": "videos/2026-394.json"
+ },
+ {
+ "id": "335",
+ "year": "2026",
+ "title": "Improve your prompts by hill-climbing with Evaluations",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/335/",
+ "dataFile": "videos/2026-335.json"
+ },
+ {
+ "id": "101",
+ "year": "2026",
+ "title": "Keynote",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/101/",
+ "dataFile": "videos/2026-101.json"
+ },
+ {
+ "id": "111",
+ "year": "2026",
+ "title": "Keynote (ASL)",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/111/",
+ "dataFile": "videos/2026-111.json"
+ },
+ {
+ "id": "298",
+ "year": "2026",
+ "title": "Meet the Evaluations framework",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/298/",
+ "dataFile": "videos/2026-298.json"
+ },
+ {
+ "id": "102",
+ "year": "2026",
+ "title": "Platforms State of the Union",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/102/",
+ "dataFile": "videos/2026-102.json"
+ },
+ {
+ "id": "112",
+ "year": "2026",
+ "title": "Platforms State of the Union (ASL)",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/112/",
+ "dataFile": "videos/2026-112.json"
+ },
+ {
+ "id": "122",
+ "year": "2026",
+ "title": "WWDC26 Platforms State of the Union Recap",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/122/",
+ "dataFile": "videos/2026-122.json"
+ },
{
"id": "359",
"year": "2025",
diff --git a/data/wwdc/by-topic/graphics-games/index.json b/data/wwdc/by-topic/graphics-games/index.json
index 046380a..6f81867 100644
--- a/data/wwdc/by-topic/graphics-games/index.json
+++ b/data/wwdc/by-topic/graphics-games/index.json
@@ -1,8 +1,9 @@
{
"id": "graphics-games",
"name": "Graphics & Games",
- "videoCount": 155,
+ "videoCount": 173,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -17,6 +18,260 @@
"2014"
],
"videos": [
+ {
+ "id": "356",
+ "year": "2026",
+ "title": "Bringing Cyberpunk 2077 to Mac",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/356/",
+ "dataFile": "videos/2026-356.json"
+ },
+ {
+ "id": "287",
+ "year": "2026",
+ "title": "Build next-generation experiences with visionOS 27",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/287/",
+ "dataFile": "videos/2026-287.json"
+ },
+ {
+ "id": "359",
+ "year": "2026",
+ "title": "Build real-time neural rendering pipelines with Metal",
+ "topics": [
+ "Graphics & Games"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/359/",
+ "dataFile": "videos/2026-359.json"
+ },
+ {
+ "id": "284",
+ "year": "2026",
+ "title": "Collaborate on structured 3D models in visionOS",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/284/",
+ "dataFile": "videos/2026-284.json"
+ },
+ {
+ "id": "234",
+ "year": "2026",
+ "title": "Design immersive environments for visionOS apps and the spatial web",
+ "topics": [
+ "Design",
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/234/",
+ "dataFile": "videos/2026-234.json"
+ },
+ {
+ "id": "252",
+ "year": "2026",
+ "title": "Design no-code games with Reality Composer Pro 3",
+ "topics": [
+ "Design",
+ "Developer Tools",
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/252/",
+ "dataFile": "videos/2026-252.json"
+ },
+ {
+ "id": "282",
+ "year": "2026",
+ "title": "Discover the Spatial Preview framework",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/282/",
+ "dataFile": "videos/2026-282.json"
+ },
+ {
+ "id": "285",
+ "year": "2026",
+ "title": "Discover USDKit and what’s new in OpenUSD",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/285/",
+ "dataFile": "videos/2026-285.json"
+ },
+ {
+ "id": "279",
+ "year": "2026",
+ "title": "Explore advances in RealityKit",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/279/",
+ "dataFile": "videos/2026-279.json"
+ },
+ {
+ "id": "283",
+ "year": "2026",
+ "title": "Explore enhancements to visionOS object tracking",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/283/",
+ "dataFile": "videos/2026-283.json"
+ },
+ {
+ "id": "281",
+ "year": "2026",
+ "title": "Extend Reality Composer Pro 3 functionality with Xcode",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/281/",
+ "dataFile": "videos/2026-281.json"
+ },
+ {
+ "id": "388",
+ "year": "2026",
+ "title": "Find and fix performance issues in your Metal games",
+ "topics": [
+ "Developer Tools",
+ "Graphics & Games"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/388/",
+ "dataFile": "videos/2026-388.json"
+ },
+ {
+ "id": "280",
+ "year": "2026",
+ "title": "Iterate your spatial scenes faster with Reality Composer Pro 3",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/280/",
+ "dataFile": "videos/2026-280.json"
+ },
+ {
+ "id": "358",
+ "year": "2026",
+ "title": "Make your game great with touch",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/358/",
+ "dataFile": "videos/2026-358.json"
+ },
+ {
+ "id": "330",
+ "year": "2026",
+ "title": "Optimize custom machine learning operations with Metal tensors",
+ "topics": [
+ "Graphics & Games"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/330/",
+ "dataFile": "videos/2026-330.json"
+ },
+ {
+ "id": "357",
+ "year": "2026",
+ "title": "Speedrun your game port with agentic coding",
+ "topics": [
+ "Graphics & Games"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/357/",
+ "dataFile": "videos/2026-357.json"
+ },
+ {
+ "id": "393",
+ "year": "2026",
+ "title": "Supercharge your spatial workflows with Reality Composer Pro 3",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/393/",
+ "dataFile": "videos/2026-393.json"
+ },
+ {
+ "id": "286",
+ "year": "2026",
+ "title": "Use foveated streaming to bring immersive content to visionOS",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/286/",
+ "dataFile": "videos/2026-286.json"
+ },
{
"id": "288",
"year": "2025",
diff --git a/data/wwdc/by-topic/health-fitness/index.json b/data/wwdc/by-topic/health-fitness/index.json
index b2db4e0..eeafce3 100644
--- a/data/wwdc/by-topic/health-fitness/index.json
+++ b/data/wwdc/by-topic/health-fitness/index.json
@@ -1,8 +1,9 @@
{
"id": "health-fitness",
"name": "Health & Fitness",
- "videoCount": 42,
+ "videoCount": 44,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -13,6 +14,35 @@
"2015"
],
"videos": [
+ {
+ "id": "207",
+ "year": "2026",
+ "title": "Deliver workout insights with HealthKit workout zones",
+ "topics": [
+ "Health & Fitness",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/207/",
+ "dataFile": "videos/2026-207.json"
+ },
+ {
+ "id": "8014",
+ "year": "2026",
+ "title": "watchOS Group Lab",
+ "topics": [
+ "App Services",
+ "Health & Fitness",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8014/",
+ "dataFile": "videos/2026-8014.json"
+ },
{
"id": "321",
"year": "2025",
diff --git a/data/wwdc/by-topic/machine-learning-ai/index.json b/data/wwdc/by-topic/machine-learning-ai/index.json
index f63ccb4..a8e6438 100644
--- a/data/wwdc/by-topic/machine-learning-ai/index.json
+++ b/data/wwdc/by-topic/machine-learning-ai/index.json
@@ -1,8 +1,9 @@
{
"id": "machine-learning-ai",
"name": "Machine Learning & AI",
- "videoCount": 92,
+ "videoCount": 131,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -12,6 +13,560 @@
"2019"
],
"videos": [
+ {
+ "id": "121",
+ "year": "2026",
+ "title": "Announcing Apple’s next big step for Siri and iPhone",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/121/",
+ "dataFile": "videos/2026-121.json"
+ },
+ {
+ "id": "8011",
+ "year": "2026",
+ "title": "Apple Intelligence Group Lab",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8011/",
+ "dataFile": "videos/2026-8011.json"
+ },
+ {
+ "id": "297",
+ "year": "2026",
+ "title": "Best practices for integrating visual intelligence in your app",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/297/",
+ "dataFile": "videos/2026-297.json"
+ },
+ {
+ "id": "339",
+ "year": "2026",
+ "title": "Bring an LLM provider to the Foundation Models framework",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/339/",
+ "dataFile": "videos/2026-339.json"
+ },
+ {
+ "id": "356",
+ "year": "2026",
+ "title": "Bringing Cyberpunk 2077 to Mac",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/356/",
+ "dataFile": "videos/2026-356.json"
+ },
+ {
+ "id": "242",
+ "year": "2026",
+ "title": "Build agentic app experiences with the Foundation Models framework",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/242/",
+ "dataFile": "videos/2026-242.json"
+ },
+ {
+ "id": "334",
+ "year": "2026",
+ "title": "Build AI-powered scripts with the fm CLI and Python SDK",
+ "topics": [
+ "Design",
+ "SwiftUI & UI Frameworks",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/334/",
+ "dataFile": "videos/2026-334.json"
+ },
+ {
+ "id": "240",
+ "year": "2026",
+ "title": "Build intelligent Siri experiences with App Schemas",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/240/",
+ "dataFile": "videos/2026-240.json"
+ },
+ {
+ "id": "287",
+ "year": "2026",
+ "title": "Build next-generation experiences with visionOS 27",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/287/",
+ "dataFile": "videos/2026-287.json"
+ },
+ {
+ "id": "319",
+ "year": "2026",
+ "title": "Build with the new Apple Foundation Model on Private Cloud Compute",
+ "topics": [
+ "Machine Learning & AI",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/319/",
+ "dataFile": "videos/2026-319.json"
+ },
+ {
+ "id": "275",
+ "year": "2026",
+ "title": "Code-along: Add persistence with SwiftData",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "Swift",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/275/",
+ "dataFile": "videos/2026-275.json"
+ },
+ {
+ "id": "8121",
+ "year": "2026",
+ "title": "Coding Intelligence, Machine Learning & AI Group Lab",
+ "topics": [
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8121/",
+ "dataFile": "videos/2026-8121.json"
+ },
+ {
+ "id": "227",
+ "year": "2026",
+ "title": "Create UI prototypes using agents in Xcode",
+ "topics": [
+ "Design",
+ "Developer Tools",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/227/",
+ "dataFile": "videos/2026-227.json"
+ },
+ {
+ "id": "325",
+ "year": "2026",
+ "title": "Dive into Core AI model authoring and optimization",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/325/",
+ "dataFile": "videos/2026-325.json"
+ },
+ {
+ "id": "343",
+ "year": "2026",
+ "title": "Explore advanced App Intents features for Siri and Apple Intelligence",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/343/",
+ "dataFile": "videos/2026-343.json"
+ },
+ {
+ "id": "279",
+ "year": "2026",
+ "title": "Explore advances in RealityKit",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/279/",
+ "dataFile": "videos/2026-279.json"
+ },
+ {
+ "id": "233",
+ "year": "2026",
+ "title": "Explore distributed inference and training with MLX",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/233/",
+ "dataFile": "videos/2026-233.json"
+ },
+ {
+ "id": "326",
+ "year": "2026",
+ "title": "Integrate on-device AI models into your app using Core AI",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/326/",
+ "dataFile": "videos/2026-326.json"
+ },
+ {
+ "id": "101",
+ "year": "2026",
+ "title": "Keynote",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/101/",
+ "dataFile": "videos/2026-101.json"
+ },
+ {
+ "id": "111",
+ "year": "2026",
+ "title": "Keynote (ASL)",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/111/",
+ "dataFile": "videos/2026-111.json"
+ },
+ {
+ "id": "223",
+ "year": "2026",
+ "title": "Live Activities essentials",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/223/",
+ "dataFile": "videos/2026-223.json"
+ },
+ {
+ "id": "8016",
+ "year": "2026",
+ "title": "Machine Learning & AI Group Lab",
+ "topics": [
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8016/",
+ "dataFile": "videos/2026-8016.json"
+ },
+ {
+ "id": "358",
+ "year": "2026",
+ "title": "Make your game great with touch",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/358/",
+ "dataFile": "videos/2026-358.json"
+ },
+ {
+ "id": "324",
+ "year": "2026",
+ "title": "Meet Core AI",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/324/",
+ "dataFile": "videos/2026-324.json"
+ },
+ {
+ "id": "222",
+ "year": "2026",
+ "title": "Meet the new MetricKit",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/222/",
+ "dataFile": "videos/2026-222.json"
+ },
+ {
+ "id": "267",
+ "year": "2026",
+ "title": "Migrate to Swift Testing",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/267/",
+ "dataFile": "videos/2026-267.json"
+ },
+ {
+ "id": "289",
+ "year": "2026",
+ "title": "Modernize your AppKit app",
+ "topics": [
+ "Machine Learning & AI",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/289/",
+ "dataFile": "videos/2026-289.json"
+ },
+ {
+ "id": "102",
+ "year": "2026",
+ "title": "Platforms State of the Union",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/102/",
+ "dataFile": "videos/2026-102.json"
+ },
+ {
+ "id": "112",
+ "year": "2026",
+ "title": "Platforms State of the Union (ASL)",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/112/",
+ "dataFile": "videos/2026-112.json"
+ },
+ {
+ "id": "250",
+ "year": "2026",
+ "title": "Principles of great design",
+ "topics": [
+ "Accessibility & Inclusion",
+ "Design",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/250/",
+ "dataFile": "videos/2026-250.json"
+ },
+ {
+ "id": "268",
+ "year": "2026",
+ "title": "Profile, fix, and verify: Improve app responsiveness with Instruments",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/268/",
+ "dataFile": "videos/2026-268.json"
+ },
+ {
+ "id": "232",
+ "year": "2026",
+ "title": "Run local agentic AI on the Mac using MLX",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/232/",
+ "dataFile": "videos/2026-232.json"
+ },
+ {
+ "id": "210",
+ "year": "2026",
+ "title": "What’s new in Apple In-App Purchase",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/210/",
+ "dataFile": "videos/2026-210.json"
+ },
+ {
+ "id": "262",
+ "year": "2026",
+ "title": "What’s new in Swift",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/262/",
+ "dataFile": "videos/2026-262.json"
+ },
+ {
+ "id": "274",
+ "year": "2026",
+ "title": "What’s new in SwiftData",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "Swift",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/274/",
+ "dataFile": "videos/2026-274.json"
+ },
+ {
+ "id": "241",
+ "year": "2026",
+ "title": "What’s new in the Foundation Models framework",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/241/",
+ "dataFile": "videos/2026-241.json"
+ },
+ {
+ "id": "204",
+ "year": "2026",
+ "title": "What’s new in WebKit for Safari 27",
+ "topics": [
+ "Machine Learning & AI",
+ "Safari & Web",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/204/",
+ "dataFile": "videos/2026-204.json"
+ },
+ {
+ "id": "122",
+ "year": "2026",
+ "title": "WWDC26 Platforms State of the Union Recap",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/122/",
+ "dataFile": "videos/2026-122.json"
+ },
+ {
+ "id": "259",
+ "year": "2026",
+ "title": "Xcode, agents, and you",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/259/",
+ "dataFile": "videos/2026-259.json"
+ },
{
"id": "277",
"year": "2025",
diff --git a/data/wwdc/by-topic/photos-camera/index.json b/data/wwdc/by-topic/photos-camera/index.json
index 673decf..877c1cf 100644
--- a/data/wwdc/by-topic/photos-camera/index.json
+++ b/data/wwdc/by-topic/photos-camera/index.json
@@ -1,8 +1,9 @@
{
"id": "photos-camera",
"name": "Photos & Camera",
- "videoCount": 54,
+ "videoCount": 60,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -14,6 +15,85 @@
"2016"
],
"videos": [
+ {
+ "id": "303",
+ "year": "2026",
+ "title": "Build a responsive camera app that launches quickly",
+ "topics": [
+ "Audio & Video",
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/303/",
+ "dataFile": "videos/2026-303.json"
+ },
+ {
+ "id": "8018",
+ "year": "2026",
+ "title": "Camera and Photo Technologies Group Lab",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8018/",
+ "dataFile": "videos/2026-8018.json"
+ },
+ {
+ "id": "375",
+ "year": "2026",
+ "title": "Create high-quality images using Image Playground",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/375/",
+ "dataFile": "videos/2026-375.json"
+ },
+ {
+ "id": "305",
+ "year": "2026",
+ "title": "Enhance RAW image processing with Core Image",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/305/",
+ "dataFile": "videos/2026-305.json"
+ },
+ {
+ "id": "304",
+ "year": "2026",
+ "title": "Implement high resolution photo capture",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/304/",
+ "dataFile": "videos/2026-304.json"
+ },
+ {
+ "id": "341",
+ "year": "2026",
+ "title": "Support the Center Stage front camera in your iOS app",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/341/",
+ "dataFile": "videos/2026-341.json"
+ },
{
"id": "319",
"year": "2025",
diff --git a/data/wwdc/by-topic/privacy-security/index.json b/data/wwdc/by-topic/privacy-security/index.json
index 6843c33..f726379 100644
--- a/data/wwdc/by-topic/privacy-security/index.json
+++ b/data/wwdc/by-topic/privacy-security/index.json
@@ -1,8 +1,9 @@
{
"id": "privacy-security",
"name": "Privacy & Security",
- "videoCount": 70,
+ "videoCount": 74,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -14,6 +15,62 @@
"2015"
],
"videos": [
+ {
+ "id": "379",
+ "year": "2026",
+ "title": "Meet Trust Insights",
+ "topics": [
+ "Business & Education",
+ "Privacy & Security"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/379/",
+ "dataFile": "videos/2026-379.json"
+ },
+ {
+ "id": "8009",
+ "year": "2026",
+ "title": "Privacy and Security Group Lab",
+ "topics": [
+ "Privacy & Security"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8009/",
+ "dataFile": "videos/2026-8009.json"
+ },
+ {
+ "id": "347",
+ "year": "2026",
+ "title": "Secure your app: mitigate risks to agentic features",
+ "topics": [
+ "Privacy & Security"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/347/",
+ "dataFile": "videos/2026-347.json"
+ },
+ {
+ "id": "201",
+ "year": "2026",
+ "title": "Secure your apps with App Attest",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Business & Education",
+ "Privacy & Security"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/201/",
+ "dataFile": "videos/2026-201.json"
+ },
{
"id": "299",
"year": "2025",
diff --git a/data/wwdc/by-topic/safari-web/index.json b/data/wwdc/by-topic/safari-web/index.json
index 5913aa0..82b062a 100644
--- a/data/wwdc/by-topic/safari-web/index.json
+++ b/data/wwdc/by-topic/safari-web/index.json
@@ -1,8 +1,9 @@
{
"id": "safari-web",
"name": "Safari & Web",
- "videoCount": 67,
+ "videoCount": 74,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -13,6 +14,104 @@
"2015"
],
"videos": [
+ {
+ "id": "216",
+ "year": "2026",
+ "title": "Create web extensions for Safari",
+ "topics": [
+ "App Store, Distribution & Marketing",
+ "Safari & Web"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/216/",
+ "dataFile": "videos/2026-216.json"
+ },
+ {
+ "id": "320",
+ "year": "2026",
+ "title": "Explore immersive website environments in visionOS",
+ "topics": [
+ "Safari & Web",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/320/",
+ "dataFile": "videos/2026-320.json"
+ },
+ {
+ "id": "215",
+ "year": "2026",
+ "title": "Get started with the HTML Model Element",
+ "topics": [
+ "Safari & Web",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/215/",
+ "dataFile": "videos/2026-215.json"
+ },
+ {
+ "id": "314",
+ "year": "2026",
+ "title": "Learn CSS Grid Lanes",
+ "topics": [
+ "Design",
+ "Safari & Web"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/314/",
+ "dataFile": "videos/2026-314.json"
+ },
+ {
+ "id": "315",
+ "year": "2026",
+ "title": "Rediscover the HTML select element",
+ "topics": [
+ "Design",
+ "Safari & Web"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/315/",
+ "dataFile": "videos/2026-315.json"
+ },
+ {
+ "id": "8015",
+ "year": "2026",
+ "title": "Safari and Web Technologies Group Lab",
+ "topics": [
+ "Safari & Web"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8015/",
+ "dataFile": "videos/2026-8015.json"
+ },
+ {
+ "id": "204",
+ "year": "2026",
+ "title": "What’s new in WebKit for Safari 27",
+ "topics": [
+ "Machine Learning & AI",
+ "Safari & Web",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/204/",
+ "dataFile": "videos/2026-204.json"
+ },
{
"id": "235",
"year": "2025",
diff --git a/data/wwdc/by-topic/spatial-computing/index.json b/data/wwdc/by-topic/spatial-computing/index.json
index eb72af4..3d3cacd 100644
--- a/data/wwdc/by-topic/spatial-computing/index.json
+++ b/data/wwdc/by-topic/spatial-computing/index.json
@@ -1,8 +1,9 @@
{
"id": "spatial-computing",
"name": "Spatial Computing",
- "videoCount": 122,
+ "videoCount": 141,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -13,6 +14,275 @@
"2017"
],
"videos": [
+ {
+ "id": "338",
+ "year": "2026",
+ "title": "Build live production tools for Apple Immersive Video",
+ "topics": [
+ "Audio & Video",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/338/",
+ "dataFile": "videos/2026-338.json"
+ },
+ {
+ "id": "287",
+ "year": "2026",
+ "title": "Build next-generation experiences with visionOS 27",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/287/",
+ "dataFile": "videos/2026-287.json"
+ },
+ {
+ "id": "8121",
+ "year": "2026",
+ "title": "Coding Intelligence, Machine Learning & AI Group Lab",
+ "topics": [
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8121/",
+ "dataFile": "videos/2026-8121.json"
+ },
+ {
+ "id": "284",
+ "year": "2026",
+ "title": "Collaborate on structured 3D models in visionOS",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/284/",
+ "dataFile": "videos/2026-284.json"
+ },
+ {
+ "id": "234",
+ "year": "2026",
+ "title": "Design immersive environments for visionOS apps and the spatial web",
+ "topics": [
+ "Design",
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/234/",
+ "dataFile": "videos/2026-234.json"
+ },
+ {
+ "id": "252",
+ "year": "2026",
+ "title": "Design no-code games with Reality Composer Pro 3",
+ "topics": [
+ "Design",
+ "Developer Tools",
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/252/",
+ "dataFile": "videos/2026-252.json"
+ },
+ {
+ "id": "282",
+ "year": "2026",
+ "title": "Discover the Spatial Preview framework",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/282/",
+ "dataFile": "videos/2026-282.json"
+ },
+ {
+ "id": "285",
+ "year": "2026",
+ "title": "Discover USDKit and what’s new in OpenUSD",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/285/",
+ "dataFile": "videos/2026-285.json"
+ },
+ {
+ "id": "279",
+ "year": "2026",
+ "title": "Explore advances in RealityKit",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/279/",
+ "dataFile": "videos/2026-279.json"
+ },
+ {
+ "id": "283",
+ "year": "2026",
+ "title": "Explore enhancements to visionOS object tracking",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/283/",
+ "dataFile": "videos/2026-283.json"
+ },
+ {
+ "id": "320",
+ "year": "2026",
+ "title": "Explore immersive website environments in visionOS",
+ "topics": [
+ "Safari & Web",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/320/",
+ "dataFile": "videos/2026-320.json"
+ },
+ {
+ "id": "281",
+ "year": "2026",
+ "title": "Extend Reality Composer Pro 3 functionality with Xcode",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/281/",
+ "dataFile": "videos/2026-281.json"
+ },
+ {
+ "id": "215",
+ "year": "2026",
+ "title": "Get started with the HTML Model Element",
+ "topics": [
+ "Safari & Web",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/215/",
+ "dataFile": "videos/2026-215.json"
+ },
+ {
+ "id": "280",
+ "year": "2026",
+ "title": "Iterate your spatial scenes faster with Reality Composer Pro 3",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/280/",
+ "dataFile": "videos/2026-280.json"
+ },
+ {
+ "id": "246",
+ "year": "2026",
+ "title": "LLM search using Core Spotlight",
+ "topics": [
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/246/",
+ "dataFile": "videos/2026-246.json"
+ },
+ {
+ "id": "8016",
+ "year": "2026",
+ "title": "Machine Learning & AI Group Lab",
+ "topics": [
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8016/",
+ "dataFile": "videos/2026-8016.json"
+ },
+ {
+ "id": "393",
+ "year": "2026",
+ "title": "Supercharge your spatial workflows with Reality Composer Pro 3",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/393/",
+ "dataFile": "videos/2026-393.json"
+ },
+ {
+ "id": "286",
+ "year": "2026",
+ "title": "Use foveated streaming to bring immersive content to visionOS",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/286/",
+ "dataFile": "videos/2026-286.json"
+ },
+ {
+ "id": "8004",
+ "year": "2026",
+ "title": "visionOS Group Lab",
+ "topics": [
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8004/",
+ "dataFile": "videos/2026-8004.json"
+ },
{
"id": "274",
"year": "2025",
diff --git a/data/wwdc/by-topic/swift/index.json b/data/wwdc/by-topic/swift/index.json
index ee22419..7202b45 100644
--- a/data/wwdc/by-topic/swift/index.json
+++ b/data/wwdc/by-topic/swift/index.json
@@ -1,8 +1,9 @@
{
"id": "swift",
"name": "Swift",
- "videoCount": 118,
+ "videoCount": 132,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -16,6 +17,209 @@
"2015"
],
"videos": [
+ {
+ "id": "265",
+ "year": "2026",
+ "title": "Build real-time apps and services with gRPC and Swift",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/265/",
+ "dataFile": "videos/2026-265.json"
+ },
+ {
+ "id": "261",
+ "year": "2026",
+ "title": "Build, deliver, and automate with Xcode Cloud",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/261/",
+ "dataFile": "videos/2026-261.json"
+ },
+ {
+ "id": "275",
+ "year": "2026",
+ "title": "Code-along: Add persistence with SwiftData",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "Swift",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/275/",
+ "dataFile": "videos/2026-275.json"
+ },
+ {
+ "id": "389",
+ "year": "2026",
+ "title": "Discover container machines",
+ "topics": [
+ "Developer Tools",
+ "Swift",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/389/",
+ "dataFile": "videos/2026-389.json"
+ },
+ {
+ "id": "328",
+ "year": "2026",
+ "title": "Explore numerical computing in Swift with MLX",
+ "topics": [
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/328/",
+ "dataFile": "videos/2026-328.json"
+ },
+ {
+ "id": "260",
+ "year": "2026",
+ "title": "Get the most out of Device Hub",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/260/",
+ "dataFile": "videos/2026-260.json"
+ },
+ {
+ "id": "254",
+ "year": "2026",
+ "title": "Integrate MusicKit into your app",
+ "topics": [
+ "Audio & Video",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/254/",
+ "dataFile": "videos/2026-254.json"
+ },
+ {
+ "id": "267",
+ "year": "2026",
+ "title": "Migrate to Swift Testing",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/267/",
+ "dataFile": "videos/2026-267.json"
+ },
+ {
+ "id": "268",
+ "year": "2026",
+ "title": "Profile, fix, and verify: Improve app responsiveness with Instruments",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/268/",
+ "dataFile": "videos/2026-268.json"
+ },
+ {
+ "id": "8001",
+ "year": "2026",
+ "title": "Swift Group Lab",
+ "topics": [
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8001/",
+ "dataFile": "videos/2026-8001.json"
+ },
+ {
+ "id": "262",
+ "year": "2026",
+ "title": "What’s new in Swift",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/262/",
+ "dataFile": "videos/2026-262.json"
+ },
+ {
+ "id": "274",
+ "year": "2026",
+ "title": "What’s new in SwiftData",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "Swift",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/274/",
+ "dataFile": "videos/2026-274.json"
+ },
+ {
+ "id": "258",
+ "year": "2026",
+ "title": "What’s new in Xcode 27",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/258/",
+ "dataFile": "videos/2026-258.json"
+ },
+ {
+ "id": "259",
+ "year": "2026",
+ "title": "Xcode, agents, and you",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/259/",
+ "dataFile": "videos/2026-259.json"
+ },
{
"id": "280",
"year": "2025",
diff --git a/data/wwdc/by-topic/swiftui-ui-frameworks/index.json b/data/wwdc/by-topic/swiftui-ui-frameworks/index.json
index 46ebbfc..13eca3f 100644
--- a/data/wwdc/by-topic/swiftui-ui-frameworks/index.json
+++ b/data/wwdc/by-topic/swiftui-ui-frameworks/index.json
@@ -1,8 +1,9 @@
{
"id": "swiftui-ui-frameworks",
"name": "SwiftUI & UI Frameworks",
- "videoCount": 280,
+ "videoCount": 303,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -16,6 +17,332 @@
"2015"
],
"videos": [
+ {
+ "id": "334",
+ "year": "2026",
+ "title": "Build AI-powered scripts with the fm CLI and Python SDK",
+ "topics": [
+ "Design",
+ "SwiftUI & UI Frameworks",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/334/",
+ "dataFile": "videos/2026-334.json"
+ },
+ {
+ "id": "275",
+ "year": "2026",
+ "title": "Code-along: Add persistence with SwiftData",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "Swift",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/275/",
+ "dataFile": "videos/2026-275.json"
+ },
+ {
+ "id": "271",
+ "year": "2026",
+ "title": "Code-along: Build powerful drag and drop in SwiftUI",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/271/",
+ "dataFile": "videos/2026-271.json"
+ },
+ {
+ "id": "322",
+ "year": "2026",
+ "title": "Compose advanced graphics effects with SwiftUI",
+ "topics": [
+ "Design",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/322/",
+ "dataFile": "videos/2026-322.json"
+ },
+ {
+ "id": "207",
+ "year": "2026",
+ "title": "Deliver workout insights with HealthKit workout zones",
+ "topics": [
+ "Health & Fitness",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/207/",
+ "dataFile": "videos/2026-207.json"
+ },
+ {
+ "id": "321",
+ "year": "2026",
+ "title": "Dive into lazy stacks and scrolling with SwiftUI",
+ "topics": [
+ "Design",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/321/",
+ "dataFile": "videos/2026-321.json"
+ },
+ {
+ "id": "370",
+ "year": "2026",
+ "title": "Elevate your app’s text experience with TextKit",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/370/",
+ "dataFile": "videos/2026-370.json"
+ },
+ {
+ "id": "219",
+ "year": "2026",
+ "title": "Enhance the accessibility of your reading app",
+ "topics": [
+ "Accessibility & Inclusion",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/219/",
+ "dataFile": "videos/2026-219.json"
+ },
+ {
+ "id": "223",
+ "year": "2026",
+ "title": "Live Activities essentials",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/223/",
+ "dataFile": "videos/2026-223.json"
+ },
+ {
+ "id": "289",
+ "year": "2026",
+ "title": "Modernize your AppKit app",
+ "topics": [
+ "Machine Learning & AI",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/289/",
+ "dataFile": "videos/2026-289.json"
+ },
+ {
+ "id": "278",
+ "year": "2026",
+ "title": "Modernize your UIKit app",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/278/",
+ "dataFile": "videos/2026-278.json"
+ },
+ {
+ "id": "203",
+ "year": "2026",
+ "title": "Read between the strokes with PencilKit",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/203/",
+ "dataFile": "videos/2026-203.json"
+ },
+ {
+ "id": "220",
+ "year": "2026",
+ "title": "Refine accessibility for custom controls",
+ "topics": [
+ "Accessibility & Inclusion",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/220/",
+ "dataFile": "videos/2026-220.json"
+ },
+ {
+ "id": "8002",
+ "year": "2026",
+ "title": "SwiftUI for Beginners Group Lab",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8002/",
+ "dataFile": "videos/2026-8002.json"
+ },
+ {
+ "id": "8006",
+ "year": "2026",
+ "title": "SwiftUI Group Lab",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8006/",
+ "dataFile": "videos/2026-8006.json"
+ },
+ {
+ "id": "8120",
+ "year": "2026",
+ "title": "SwiftUI Group Lab",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8120/",
+ "dataFile": "videos/2026-8120.json"
+ },
+ {
+ "id": "372",
+ "year": "2026",
+ "title": "Unwrap PaperKit",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/372/",
+ "dataFile": "videos/2026-372.json"
+ },
+ {
+ "id": "272",
+ "year": "2026",
+ "title": "Use SwiftUI with AppKit and UIKit",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/272/",
+ "dataFile": "videos/2026-272.json"
+ },
+ {
+ "id": "8014",
+ "year": "2026",
+ "title": "watchOS Group Lab",
+ "topics": [
+ "App Services",
+ "Health & Fitness",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8014/",
+ "dataFile": "videos/2026-8014.json"
+ },
+ {
+ "id": "274",
+ "year": "2026",
+ "title": "What’s new in SwiftData",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "Swift",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/274/",
+ "dataFile": "videos/2026-274.json"
+ },
+ {
+ "id": "269",
+ "year": "2026",
+ "title": "What’s new in SwiftUI",
+ "topics": [
+ "App Services",
+ "Design",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/269/",
+ "dataFile": "videos/2026-269.json"
+ },
+ {
+ "id": "204",
+ "year": "2026",
+ "title": "What’s new in WebKit for Safari 27",
+ "topics": [
+ "Machine Learning & AI",
+ "Safari & Web",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/204/",
+ "dataFile": "videos/2026-204.json"
+ },
+ {
+ "id": "277",
+ "year": "2026",
+ "title": "WidgetKit foundations",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/277/",
+ "dataFile": "videos/2026-277.json"
+ },
{
"id": "274",
"year": "2025",
diff --git a/data/wwdc/by-topic/system-services/index.json b/data/wwdc/by-topic/system-services/index.json
index 82d7abe..f568571 100644
--- a/data/wwdc/by-topic/system-services/index.json
+++ b/data/wwdc/by-topic/system-services/index.json
@@ -1,8 +1,9 @@
{
"id": "system-services",
"name": "System Services",
- "videoCount": 118,
+ "videoCount": 128,
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -16,6 +17,143 @@
"2015"
],
"videos": [
+ {
+ "id": "319",
+ "year": "2026",
+ "title": "Build with the new Apple Foundation Model on Private Cloud Compute",
+ "topics": [
+ "Machine Learning & AI",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/319/",
+ "dataFile": "videos/2026-319.json"
+ },
+ {
+ "id": "226",
+ "year": "2026",
+ "title": "Create live communication experiences",
+ "topics": [
+ "App Services",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/226/",
+ "dataFile": "videos/2026-226.json"
+ },
+ {
+ "id": "389",
+ "year": "2026",
+ "title": "Discover container machines",
+ "topics": [
+ "Developer Tools",
+ "Swift",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/389/",
+ "dataFile": "videos/2026-389.json"
+ },
+ {
+ "id": "224",
+ "year": "2026",
+ "title": "Expand the capabilities of your Virtualization app",
+ "topics": [
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/224/",
+ "dataFile": "videos/2026-224.json"
+ },
+ {
+ "id": "369",
+ "year": "2026",
+ "title": "Find your accessory with Bluetooth Channel Sounding",
+ "topics": [
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/369/",
+ "dataFile": "videos/2026-369.json"
+ },
+ {
+ "id": "222",
+ "year": "2026",
+ "title": "Meet the new MetricKit",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/222/",
+ "dataFile": "videos/2026-222.json"
+ },
+ {
+ "id": "8003",
+ "year": "2026",
+ "title": "Power and Performance Group Lab",
+ "topics": [
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8003/",
+ "dataFile": "videos/2026-8003.json"
+ },
+ {
+ "id": "212",
+ "year": "2026",
+ "title": "Rev up your CarPlay app",
+ "topics": [
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/212/",
+ "dataFile": "videos/2026-212.json"
+ },
+ {
+ "id": "310",
+ "year": "2026",
+ "title": "What’s new in Shortcuts",
+ "topics": [
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/310/",
+ "dataFile": "videos/2026-310.json"
+ },
+ {
+ "id": "209",
+ "year": "2026",
+ "title": "What’s new in Wallet",
+ "topics": [
+ "App Services",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/209/",
+ "dataFile": "videos/2026-209.json"
+ },
{
"id": "299",
"year": "2025",
diff --git a/data/wwdc/by-year/2026/index.json b/data/wwdc/by-year/2026/index.json
new file mode 100644
index 0000000..f68ff31
--- /dev/null
+++ b/data/wwdc/by-year/2026/index.json
@@ -0,0 +1,1900 @@
+{
+ "year": "2026",
+ "videoCount": 136,
+ "topics": [
+ "Accessibility & Inclusion",
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Audio & Video",
+ "Business & Education",
+ "Design",
+ "Developer Tools",
+ "Essentials",
+ "Graphics & Games",
+ "Health & Fitness",
+ "Machine Learning & AI",
+ "Photos & Camera",
+ "Privacy & Security",
+ "Safari & Web",
+ "Spatial Computing",
+ "Swift",
+ "SwiftUI & UI Frameworks",
+ "System Services"
+ ],
+ "videos": [
+ {
+ "id": "8005",
+ "year": "2026",
+ "title": "Accessibility Technologies Group Lab",
+ "topics": [
+ "Accessibility & Inclusion"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8005/",
+ "dataFile": "videos/2026-8005.json"
+ },
+ {
+ "id": "121",
+ "year": "2026",
+ "title": "Announcing Apple’s next big step for Siri and iPhone",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/121/",
+ "dataFile": "videos/2026-121.json"
+ },
+ {
+ "id": "8010",
+ "year": "2026",
+ "title": "App Store Connect Group Lab",
+ "topics": [
+ "App Store, Distribution & Marketing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8010/",
+ "dataFile": "videos/2026-8010.json"
+ },
+ {
+ "id": "8011",
+ "year": "2026",
+ "title": "Apple Intelligence Group Lab",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8011/",
+ "dataFile": "videos/2026-8011.json"
+ },
+ {
+ "id": "297",
+ "year": "2026",
+ "title": "Best practices for integrating visual intelligence in your app",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/297/",
+ "dataFile": "videos/2026-297.json"
+ },
+ {
+ "id": "339",
+ "year": "2026",
+ "title": "Bring an LLM provider to the Foundation Models framework",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/339/",
+ "dataFile": "videos/2026-339.json"
+ },
+ {
+ "id": "356",
+ "year": "2026",
+ "title": "Bringing Cyberpunk 2077 to Mac",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/356/",
+ "dataFile": "videos/2026-356.json"
+ },
+ {
+ "id": "303",
+ "year": "2026",
+ "title": "Build a responsive camera app that launches quickly",
+ "topics": [
+ "Audio & Video",
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/303/",
+ "dataFile": "videos/2026-303.json"
+ },
+ {
+ "id": "242",
+ "year": "2026",
+ "title": "Build agentic app experiences with the Foundation Models framework",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/242/",
+ "dataFile": "videos/2026-242.json"
+ },
+ {
+ "id": "334",
+ "year": "2026",
+ "title": "Build AI-powered scripts with the fm CLI and Python SDK",
+ "topics": [
+ "Design",
+ "SwiftUI & UI Frameworks",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/334/",
+ "dataFile": "videos/2026-334.json"
+ },
+ {
+ "id": "240",
+ "year": "2026",
+ "title": "Build intelligent Siri experiences with App Schemas",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/240/",
+ "dataFile": "videos/2026-240.json"
+ },
+ {
+ "id": "338",
+ "year": "2026",
+ "title": "Build live production tools for Apple Immersive Video",
+ "topics": [
+ "Audio & Video",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/338/",
+ "dataFile": "videos/2026-338.json"
+ },
+ {
+ "id": "287",
+ "year": "2026",
+ "title": "Build next-generation experiences with visionOS 27",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/287/",
+ "dataFile": "videos/2026-287.json"
+ },
+ {
+ "id": "265",
+ "year": "2026",
+ "title": "Build real-time apps and services with gRPC and Swift",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/265/",
+ "dataFile": "videos/2026-265.json"
+ },
+ {
+ "id": "359",
+ "year": "2026",
+ "title": "Build real-time neural rendering pipelines with Metal",
+ "topics": [
+ "Graphics & Games"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/359/",
+ "dataFile": "videos/2026-359.json"
+ },
+ {
+ "id": "319",
+ "year": "2026",
+ "title": "Build with the new Apple Foundation Model on Private Cloud Compute",
+ "topics": [
+ "Machine Learning & AI",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/319/",
+ "dataFile": "videos/2026-319.json"
+ },
+ {
+ "id": "261",
+ "year": "2026",
+ "title": "Build, deliver, and automate with Xcode Cloud",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/261/",
+ "dataFile": "videos/2026-261.json"
+ },
+ {
+ "id": "8018",
+ "year": "2026",
+ "title": "Camera and Photo Technologies Group Lab",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8018/",
+ "dataFile": "videos/2026-8018.json"
+ },
+ {
+ "id": "275",
+ "year": "2026",
+ "title": "Code-along: Add persistence with SwiftData",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "Swift",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/275/",
+ "dataFile": "videos/2026-275.json"
+ },
+ {
+ "id": "271",
+ "year": "2026",
+ "title": "Code-along: Build powerful drag and drop in SwiftUI",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/271/",
+ "dataFile": "videos/2026-271.json"
+ },
+ {
+ "id": "344",
+ "year": "2026",
+ "title": "Code-along: Make your app available to Siri",
+ "topics": [
+ "App Services"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/344/",
+ "dataFile": "videos/2026-344.json"
+ },
+ {
+ "id": "8007",
+ "year": "2026",
+ "title": "Coding Intelligence for Beginners Group Lab",
+ "topics": [
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8007/",
+ "dataFile": "videos/2026-8007.json"
+ },
+ {
+ "id": "8121",
+ "year": "2026",
+ "title": "Coding Intelligence, Machine Learning & AI Group Lab",
+ "topics": [
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8121/",
+ "dataFile": "videos/2026-8121.json"
+ },
+ {
+ "id": "284",
+ "year": "2026",
+ "title": "Collaborate on structured 3D models in visionOS",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/284/",
+ "dataFile": "videos/2026-284.json"
+ },
+ {
+ "id": "251",
+ "year": "2026",
+ "title": "Communicate your brand identity on iOS",
+ "topics": [
+ "Design"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/251/",
+ "dataFile": "videos/2026-251.json"
+ },
+ {
+ "id": "322",
+ "year": "2026",
+ "title": "Compose advanced graphics effects with SwiftUI",
+ "topics": [
+ "Design",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/322/",
+ "dataFile": "videos/2026-322.json"
+ },
+ {
+ "id": "290",
+ "year": "2026",
+ "title": "Craft clear names for features and labels in your app",
+ "topics": [
+ "Design"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/290/",
+ "dataFile": "videos/2026-290.json"
+ },
+ {
+ "id": "375",
+ "year": "2026",
+ "title": "Create high-quality images using Image Playground",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/375/",
+ "dataFile": "videos/2026-375.json"
+ },
+ {
+ "id": "226",
+ "year": "2026",
+ "title": "Create live communication experiences",
+ "topics": [
+ "App Services",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/226/",
+ "dataFile": "videos/2026-226.json"
+ },
+ {
+ "id": "299",
+ "year": "2026",
+ "title": "Create robust evaluations for agentic apps",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/299/",
+ "dataFile": "videos/2026-299.json"
+ },
+ {
+ "id": "227",
+ "year": "2026",
+ "title": "Create UI prototypes using agents in Xcode",
+ "topics": [
+ "Design",
+ "Developer Tools",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/227/",
+ "dataFile": "videos/2026-227.json"
+ },
+ {
+ "id": "216",
+ "year": "2026",
+ "title": "Create web extensions for Safari",
+ "topics": [
+ "App Store, Distribution & Marketing",
+ "Safari & Web"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/216/",
+ "dataFile": "videos/2026-216.json"
+ },
+ {
+ "id": "243",
+ "year": "2026",
+ "title": "Debug and profile agentic app experiences with Instruments",
+ "topics": [
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/243/",
+ "dataFile": "videos/2026-243.json"
+ },
+ {
+ "id": "207",
+ "year": "2026",
+ "title": "Deliver workout insights with HealthKit workout zones",
+ "topics": [
+ "Health & Fitness",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/207/",
+ "dataFile": "videos/2026-207.json"
+ },
+ {
+ "id": "234",
+ "year": "2026",
+ "title": "Design immersive environments for visionOS apps and the spatial web",
+ "topics": [
+ "Design",
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/234/",
+ "dataFile": "videos/2026-234.json"
+ },
+ {
+ "id": "292",
+ "year": "2026",
+ "title": "Design intuitive search experiences",
+ "topics": [
+ "Design",
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/292/",
+ "dataFile": "videos/2026-292.json"
+ },
+ {
+ "id": "252",
+ "year": "2026",
+ "title": "Design no-code games with Reality Composer Pro 3",
+ "topics": [
+ "Design",
+ "Developer Tools",
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/252/",
+ "dataFile": "videos/2026-252.json"
+ },
+ {
+ "id": "389",
+ "year": "2026",
+ "title": "Discover container machines",
+ "topics": [
+ "Developer Tools",
+ "Swift",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/389/",
+ "dataFile": "videos/2026-389.json"
+ },
+ {
+ "id": "256",
+ "year": "2026",
+ "title": "Discover generated subtitles and subtitle styles",
+ "topics": [
+ "Accessibility & Inclusion",
+ "Audio & Video"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/256/",
+ "dataFile": "videos/2026-256.json"
+ },
+ {
+ "id": "345",
+ "year": "2026",
+ "title": "Discover new capabilities in the App Intents framework",
+ "topics": [
+ "App Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/345/",
+ "dataFile": "videos/2026-345.json"
+ },
+ {
+ "id": "282",
+ "year": "2026",
+ "title": "Discover the Spatial Preview framework",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/282/",
+ "dataFile": "videos/2026-282.json"
+ },
+ {
+ "id": "285",
+ "year": "2026",
+ "title": "Discover USDKit and what’s new in OpenUSD",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/285/",
+ "dataFile": "videos/2026-285.json"
+ },
+ {
+ "id": "325",
+ "year": "2026",
+ "title": "Dive into Core AI model authoring and optimization",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/325/",
+ "dataFile": "videos/2026-325.json"
+ },
+ {
+ "id": "321",
+ "year": "2026",
+ "title": "Dive into lazy stacks and scrolling with SwiftUI",
+ "topics": [
+ "Design",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/321/",
+ "dataFile": "videos/2026-321.json"
+ },
+ {
+ "id": "397",
+ "year": "2026",
+ "title": "Dub Dub Daily: Day 2",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/397/",
+ "dataFile": "videos/2026-397.json"
+ },
+ {
+ "id": "398",
+ "year": "2026",
+ "title": "Dub Dub Daily: Day 3",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/398/",
+ "dataFile": "videos/2026-398.json"
+ },
+ {
+ "id": "399",
+ "year": "2026",
+ "title": "Dub Dub Daily: Day 4",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/399/",
+ "dataFile": "videos/2026-399.json"
+ },
+ {
+ "id": "370",
+ "year": "2026",
+ "title": "Elevate your app’s text experience with TextKit",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/370/",
+ "dataFile": "videos/2026-370.json"
+ },
+ {
+ "id": "305",
+ "year": "2026",
+ "title": "Enhance RAW image processing with Core Image",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/305/",
+ "dataFile": "videos/2026-305.json"
+ },
+ {
+ "id": "219",
+ "year": "2026",
+ "title": "Enhance the accessibility of your reading app",
+ "topics": [
+ "Accessibility & Inclusion",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/219/",
+ "dataFile": "videos/2026-219.json"
+ },
+ {
+ "id": "205",
+ "year": "2026",
+ "title": "Enhance your presence on the App Store",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/205/",
+ "dataFile": "videos/2026-205.json"
+ },
+ {
+ "id": "224",
+ "year": "2026",
+ "title": "Expand the capabilities of your Virtualization app",
+ "topics": [
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/224/",
+ "dataFile": "videos/2026-224.json"
+ },
+ {
+ "id": "343",
+ "year": "2026",
+ "title": "Explore advanced App Intents features for Siri and Apple Intelligence",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/343/",
+ "dataFile": "videos/2026-343.json"
+ },
+ {
+ "id": "279",
+ "year": "2026",
+ "title": "Explore advances in RealityKit",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/279/",
+ "dataFile": "videos/2026-279.json"
+ },
+ {
+ "id": "233",
+ "year": "2026",
+ "title": "Explore distributed inference and training with MLX",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/233/",
+ "dataFile": "videos/2026-233.json"
+ },
+ {
+ "id": "283",
+ "year": "2026",
+ "title": "Explore enhancements to visionOS object tracking",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/283/",
+ "dataFile": "videos/2026-283.json"
+ },
+ {
+ "id": "320",
+ "year": "2026",
+ "title": "Explore immersive website environments in visionOS",
+ "topics": [
+ "Safari & Web",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/320/",
+ "dataFile": "videos/2026-320.json"
+ },
+ {
+ "id": "328",
+ "year": "2026",
+ "title": "Explore numerical computing in Swift with MLX",
+ "topics": [
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/328/",
+ "dataFile": "videos/2026-328.json"
+ },
+ {
+ "id": "309",
+ "year": "2026",
+ "title": "Explore Retention Messaging in App Store Connect",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/309/",
+ "dataFile": "videos/2026-309.json"
+ },
+ {
+ "id": "281",
+ "year": "2026",
+ "title": "Extend Reality Composer Pro 3 functionality with Xcode",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/281/",
+ "dataFile": "videos/2026-281.json"
+ },
+ {
+ "id": "388",
+ "year": "2026",
+ "title": "Find and fix performance issues in your Metal games",
+ "topics": [
+ "Developer Tools",
+ "Graphics & Games"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/388/",
+ "dataFile": "videos/2026-388.json"
+ },
+ {
+ "id": "369",
+ "year": "2026",
+ "title": "Find your accessory with Bluetooth Channel Sounding",
+ "topics": [
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/369/",
+ "dataFile": "videos/2026-369.json"
+ },
+ {
+ "id": "394",
+ "year": "2026",
+ "title": "Get ready for WWDC26",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/394/",
+ "dataFile": "videos/2026-394.json"
+ },
+ {
+ "id": "215",
+ "year": "2026",
+ "title": "Get started with the HTML Model Element",
+ "topics": [
+ "Safari & Web",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/215/",
+ "dataFile": "videos/2026-215.json"
+ },
+ {
+ "id": "260",
+ "year": "2026",
+ "title": "Get the most out of Device Hub",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/260/",
+ "dataFile": "videos/2026-260.json"
+ },
+ {
+ "id": "8012",
+ "year": "2026",
+ "title": "Icon Composer for Beginners Group Lab",
+ "topics": [
+ "Design"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8012/",
+ "dataFile": "videos/2026-8012.json"
+ },
+ {
+ "id": "304",
+ "year": "2026",
+ "title": "Implement high resolution photo capture",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/304/",
+ "dataFile": "videos/2026-304.json"
+ },
+ {
+ "id": "335",
+ "year": "2026",
+ "title": "Improve your prompts by hill-climbing with Evaluations",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/335/",
+ "dataFile": "videos/2026-335.json"
+ },
+ {
+ "id": "254",
+ "year": "2026",
+ "title": "Integrate MusicKit into your app",
+ "topics": [
+ "Audio & Video",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/254/",
+ "dataFile": "videos/2026-254.json"
+ },
+ {
+ "id": "326",
+ "year": "2026",
+ "title": "Integrate on-device AI models into your app using Core AI",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/326/",
+ "dataFile": "videos/2026-326.json"
+ },
+ {
+ "id": "280",
+ "year": "2026",
+ "title": "Iterate your spatial scenes faster with Reality Composer Pro 3",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/280/",
+ "dataFile": "videos/2026-280.json"
+ },
+ {
+ "id": "101",
+ "year": "2026",
+ "title": "Keynote",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/101/",
+ "dataFile": "videos/2026-101.json"
+ },
+ {
+ "id": "111",
+ "year": "2026",
+ "title": "Keynote (ASL)",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/111/",
+ "dataFile": "videos/2026-111.json"
+ },
+ {
+ "id": "314",
+ "year": "2026",
+ "title": "Learn CSS Grid Lanes",
+ "topics": [
+ "Design",
+ "Safari & Web"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/314/",
+ "dataFile": "videos/2026-314.json"
+ },
+ {
+ "id": "223",
+ "year": "2026",
+ "title": "Live Activities essentials",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/223/",
+ "dataFile": "videos/2026-223.json"
+ },
+ {
+ "id": "246",
+ "year": "2026",
+ "title": "LLM search using Core Spotlight",
+ "topics": [
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/246/",
+ "dataFile": "videos/2026-246.json"
+ },
+ {
+ "id": "8016",
+ "year": "2026",
+ "title": "Machine Learning & AI Group Lab",
+ "topics": [
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8016/",
+ "dataFile": "videos/2026-8016.json"
+ },
+ {
+ "id": "358",
+ "year": "2026",
+ "title": "Make your game great with touch",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/358/",
+ "dataFile": "videos/2026-358.json"
+ },
+ {
+ "id": "324",
+ "year": "2026",
+ "title": "Meet Core AI",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/324/",
+ "dataFile": "videos/2026-324.json"
+ },
+ {
+ "id": "298",
+ "year": "2026",
+ "title": "Meet the Evaluations framework",
+ "topics": [
+ "Essentials"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/298/",
+ "dataFile": "videos/2026-298.json"
+ },
+ {
+ "id": "253",
+ "year": "2026",
+ "title": "Meet the Music Understanding framework",
+ "topics": [
+ "Audio & Video"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/253/",
+ "dataFile": "videos/2026-253.json"
+ },
+ {
+ "id": "222",
+ "year": "2026",
+ "title": "Meet the new MetricKit",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/222/",
+ "dataFile": "videos/2026-222.json"
+ },
+ {
+ "id": "312",
+ "year": "2026",
+ "title": "Meet the Now Playing framework",
+ "topics": [
+ "Audio & Video"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/312/",
+ "dataFile": "videos/2026-312.json"
+ },
+ {
+ "id": "379",
+ "year": "2026",
+ "title": "Meet Trust Insights",
+ "topics": [
+ "Business & Education",
+ "Privacy & Security"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/379/",
+ "dataFile": "videos/2026-379.json"
+ },
+ {
+ "id": "267",
+ "year": "2026",
+ "title": "Migrate to Swift Testing",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/267/",
+ "dataFile": "videos/2026-267.json"
+ },
+ {
+ "id": "289",
+ "year": "2026",
+ "title": "Modernize your AppKit app",
+ "topics": [
+ "Machine Learning & AI",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/289/",
+ "dataFile": "videos/2026-289.json"
+ },
+ {
+ "id": "278",
+ "year": "2026",
+ "title": "Modernize your UIKit app",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/278/",
+ "dataFile": "videos/2026-278.json"
+ },
+ {
+ "id": "391",
+ "year": "2026",
+ "title": "Offer subscriptions to groups and organizations",
+ "topics": [
+ "App Store, Distribution & Marketing",
+ "Business & Education"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/391/",
+ "dataFile": "videos/2026-391.json"
+ },
+ {
+ "id": "330",
+ "year": "2026",
+ "title": "Optimize custom machine learning operations with Metal tensors",
+ "topics": [
+ "Graphics & Games"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/330/",
+ "dataFile": "videos/2026-330.json"
+ },
+ {
+ "id": "102",
+ "year": "2026",
+ "title": "Platforms State of the Union",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/102/",
+ "dataFile": "videos/2026-102.json"
+ },
+ {
+ "id": "112",
+ "year": "2026",
+ "title": "Platforms State of the Union (ASL)",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/112/",
+ "dataFile": "videos/2026-112.json"
+ },
+ {
+ "id": "8003",
+ "year": "2026",
+ "title": "Power and Performance Group Lab",
+ "topics": [
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8003/",
+ "dataFile": "videos/2026-8003.json"
+ },
+ {
+ "id": "221",
+ "year": "2026",
+ "title": "Prepare your tvOS apps for Dynamic Type",
+ "topics": [
+ "Accessibility & Inclusion",
+ "Audio & Video"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/221/",
+ "dataFile": "videos/2026-221.json"
+ },
+ {
+ "id": "250",
+ "year": "2026",
+ "title": "Principles of great design",
+ "topics": [
+ "Accessibility & Inclusion",
+ "Design",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/250/",
+ "dataFile": "videos/2026-250.json"
+ },
+ {
+ "id": "8009",
+ "year": "2026",
+ "title": "Privacy and Security Group Lab",
+ "topics": [
+ "Privacy & Security"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8009/",
+ "dataFile": "videos/2026-8009.json"
+ },
+ {
+ "id": "268",
+ "year": "2026",
+ "title": "Profile, fix, and verify: Improve app responsiveness with Instruments",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/268/",
+ "dataFile": "videos/2026-268.json"
+ },
+ {
+ "id": "203",
+ "year": "2026",
+ "title": "Read between the strokes with PencilKit",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/203/",
+ "dataFile": "videos/2026-203.json"
+ },
+ {
+ "id": "315",
+ "year": "2026",
+ "title": "Rediscover the HTML select element",
+ "topics": [
+ "Design",
+ "Safari & Web"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/315/",
+ "dataFile": "videos/2026-315.json"
+ },
+ {
+ "id": "220",
+ "year": "2026",
+ "title": "Refine accessibility for custom controls",
+ "topics": [
+ "Accessibility & Inclusion",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/220/",
+ "dataFile": "videos/2026-220.json"
+ },
+ {
+ "id": "212",
+ "year": "2026",
+ "title": "Rev up your CarPlay app",
+ "topics": [
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/212/",
+ "dataFile": "videos/2026-212.json"
+ },
+ {
+ "id": "232",
+ "year": "2026",
+ "title": "Run local agentic AI on the Mac using MLX",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/232/",
+ "dataFile": "videos/2026-232.json"
+ },
+ {
+ "id": "8015",
+ "year": "2026",
+ "title": "Safari and Web Technologies Group Lab",
+ "topics": [
+ "Safari & Web"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8015/",
+ "dataFile": "videos/2026-8015.json"
+ },
+ {
+ "id": "347",
+ "year": "2026",
+ "title": "Secure your app: mitigate risks to agentic features",
+ "topics": [
+ "Privacy & Security"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/347/",
+ "dataFile": "videos/2026-347.json"
+ },
+ {
+ "id": "201",
+ "year": "2026",
+ "title": "Secure your apps with App Attest",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Business & Education",
+ "Privacy & Security"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/201/",
+ "dataFile": "videos/2026-201.json"
+ },
+ {
+ "id": "357",
+ "year": "2026",
+ "title": "Speedrun your game port with agentic coding",
+ "topics": [
+ "Graphics & Games"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/357/",
+ "dataFile": "videos/2026-357.json"
+ },
+ {
+ "id": "393",
+ "year": "2026",
+ "title": "Supercharge your spatial workflows with Reality Composer Pro 3",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/393/",
+ "dataFile": "videos/2026-393.json"
+ },
+ {
+ "id": "341",
+ "year": "2026",
+ "title": "Support the Center Stage front camera in your iOS app",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/341/",
+ "dataFile": "videos/2026-341.json"
+ },
+ {
+ "id": "8001",
+ "year": "2026",
+ "title": "Swift Group Lab",
+ "topics": [
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8001/",
+ "dataFile": "videos/2026-8001.json"
+ },
+ {
+ "id": "8017",
+ "year": "2026",
+ "title": "SwiftData Group Lab",
+ "topics": [
+ "App Services"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8017/",
+ "dataFile": "videos/2026-8017.json"
+ },
+ {
+ "id": "8002",
+ "year": "2026",
+ "title": "SwiftUI for Beginners Group Lab",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8002/",
+ "dataFile": "videos/2026-8002.json"
+ },
+ {
+ "id": "8006",
+ "year": "2026",
+ "title": "SwiftUI Group Lab",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8006/",
+ "dataFile": "videos/2026-8006.json"
+ },
+ {
+ "id": "8120",
+ "year": "2026",
+ "title": "SwiftUI Group Lab",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8120/",
+ "dataFile": "videos/2026-8120.json"
+ },
+ {
+ "id": "213",
+ "year": "2026",
+ "title": "Translate your app using agents in Xcode",
+ "topics": [
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/213/",
+ "dataFile": "videos/2026-213.json"
+ },
+ {
+ "id": "378",
+ "year": "2026",
+ "title": "Unlock in-game content with StoreKit and Background Assets",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/378/",
+ "dataFile": "videos/2026-378.json"
+ },
+ {
+ "id": "372",
+ "year": "2026",
+ "title": "Unwrap PaperKit",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/372/",
+ "dataFile": "videos/2026-372.json"
+ },
+ {
+ "id": "286",
+ "year": "2026",
+ "title": "Use foveated streaming to bring immersive content to visionOS",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/286/",
+ "dataFile": "videos/2026-286.json"
+ },
+ {
+ "id": "272",
+ "year": "2026",
+ "title": "Use SwiftUI with AppKit and UIKit",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/272/",
+ "dataFile": "videos/2026-272.json"
+ },
+ {
+ "id": "295",
+ "year": "2026",
+ "title": "Validate your App Intents adoption with AppIntentsTesting",
+ "topics": [
+ "App Services",
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/295/",
+ "dataFile": "videos/2026-295.json"
+ },
+ {
+ "id": "8004",
+ "year": "2026",
+ "title": "visionOS Group Lab",
+ "topics": [
+ "Spatial Computing"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8004/",
+ "dataFile": "videos/2026-8004.json"
+ },
+ {
+ "id": "8014",
+ "year": "2026",
+ "title": "watchOS Group Lab",
+ "topics": [
+ "App Services",
+ "Health & Fitness",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8014/",
+ "dataFile": "videos/2026-8014.json"
+ },
+ {
+ "id": "210",
+ "year": "2026",
+ "title": "What’s new in Apple In-App Purchase",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/210/",
+ "dataFile": "videos/2026-210.json"
+ },
+ {
+ "id": "230",
+ "year": "2026",
+ "title": "What’s new in assessment on macOS",
+ "topics": [
+ "App Services",
+ "Business & Education"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/230/",
+ "dataFile": "videos/2026-230.json"
+ },
+ {
+ "id": "237",
+ "year": "2026",
+ "title": "What’s new in image understanding",
+ "topics": [
+ "App Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/237/",
+ "dataFile": "videos/2026-237.json"
+ },
+ {
+ "id": "206",
+ "year": "2026",
+ "title": "What’s new in managing Apple devices",
+ "topics": [
+ "App Store, Distribution & Marketing",
+ "Business & Education"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/206/",
+ "dataFile": "videos/2026-206.json"
+ },
+ {
+ "id": "310",
+ "year": "2026",
+ "title": "What’s new in Shortcuts",
+ "topics": [
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/310/",
+ "dataFile": "videos/2026-310.json"
+ },
+ {
+ "id": "262",
+ "year": "2026",
+ "title": "What’s new in Swift",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/262/",
+ "dataFile": "videos/2026-262.json"
+ },
+ {
+ "id": "274",
+ "year": "2026",
+ "title": "What’s new in SwiftData",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "Swift",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/274/",
+ "dataFile": "videos/2026-274.json"
+ },
+ {
+ "id": "269",
+ "year": "2026",
+ "title": "What’s new in SwiftUI",
+ "topics": [
+ "App Services",
+ "Design",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/269/",
+ "dataFile": "videos/2026-269.json"
+ },
+ {
+ "id": "241",
+ "year": "2026",
+ "title": "What’s new in the Foundation Models framework",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/241/",
+ "dataFile": "videos/2026-241.json"
+ },
+ {
+ "id": "209",
+ "year": "2026",
+ "title": "What’s new in Wallet",
+ "topics": [
+ "App Services",
+ "System Services"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/209/",
+ "dataFile": "videos/2026-209.json"
+ },
+ {
+ "id": "204",
+ "year": "2026",
+ "title": "What’s new in WebKit for Safari 27",
+ "topics": [
+ "Machine Learning & AI",
+ "Safari & Web",
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/204/",
+ "dataFile": "videos/2026-204.json"
+ },
+ {
+ "id": "258",
+ "year": "2026",
+ "title": "What’s new in Xcode 27",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/258/",
+ "dataFile": "videos/2026-258.json"
+ },
+ {
+ "id": "277",
+ "year": "2026",
+ "title": "WidgetKit foundations",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "duration": "",
+ "hasCode": true,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/277/",
+ "dataFile": "videos/2026-277.json"
+ },
+ {
+ "id": "122",
+ "year": "2026",
+ "title": "WWDC26 Platforms State of the Union Recap",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/122/",
+ "dataFile": "videos/2026-122.json"
+ },
+ {
+ "id": "8013",
+ "year": "2026",
+ "title": "Xcode Tips and Tricks Group Lab",
+ "topics": [
+ "Developer Tools"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": false,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8013/",
+ "dataFile": "videos/2026-8013.json"
+ },
+ {
+ "id": "259",
+ "year": "2026",
+ "title": "Xcode, agents, and you",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "duration": "",
+ "hasCode": false,
+ "hasTranscript": true,
+ "url": "https://developer.apple.com/videos/play/wwdc2026/259/",
+ "dataFile": "videos/2026-259.json"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/data/wwdc/index.json b/data/wwdc/index.json
index 43625f5..0d6fd01 100644
--- a/data/wwdc/index.json
+++ b/data/wwdc/index.json
@@ -1,7 +1,7 @@
{
"version": "2.0",
- "lastUpdated": "2025-07-18T11:04:52.652Z",
- "totalVideos": 1266,
+ "lastUpdated": "2026-06-12T10:24:26.759Z",
+ "totalVideos": 1402,
"topics": [
{
"id": "accessibility-inclusion",
@@ -100,6 +100,7 @@
}
],
"years": [
+ "2026",
"2025",
"2024",
"2023",
@@ -115,25 +116,25 @@
],
"statistics": {
"byTopic": {
- "accessibility-inclusion": 57,
- "app-services": 172,
- "app-store-distribution-marketing": 75,
- "audio-video": 124,
- "business-education": 51,
- "design": 143,
- "developer-tools": 199,
- "essentials": 170,
- "graphics-games": 155,
- "health-fitness": 42,
- "machine-learning-ai": 92,
+ "accessibility-inclusion": 63,
+ "app-services": 198,
+ "app-store-distribution-marketing": 84,
+ "audio-video": 131,
+ "business-education": 56,
+ "design": 157,
+ "developer-tools": 218,
+ "essentials": 184,
+ "graphics-games": 173,
+ "health-fitness": 44,
"maps-location": 26,
- "photos-camera": 54,
- "privacy-security": 70,
- "safari-web": 67,
- "spatial-computing": 122,
- "swift": 118,
- "swiftui-ui-frameworks": 280,
- "system-services": 118
+ "machine-learning-ai": 131,
+ "photos-camera": 60,
+ "privacy-security": 74,
+ "safari-web": 74,
+ "spatial-computing": 141,
+ "swift": 132,
+ "swiftui-ui-frameworks": 303,
+ "system-services": 128
},
"byYear": {
"2014": 6,
@@ -147,10 +148,11 @@
"2022": 185,
"2023": 181,
"2024": 123,
- "2025": 122
+ "2025": 122,
+ "2026": 136
},
- "videosWithCode": 626,
- "videosWithTranscript": 1266,
+ "videosWithCode": 713,
+ "videosWithTranscript": 1379,
"videosWithResources": 0
}
}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-101.json b/data/wwdc/videos/2026-101.json
new file mode 100644
index 0000000..99799d2
--- /dev/null
+++ b/data/wwdc/videos/2026-101.json
@@ -0,0 +1,18 @@
+{
+ "id": "101",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/101/",
+ "title": "Keynote",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": []
+ },
+ "extractedAt": "2026-06-12T10:24:10.501Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-102.json b/data/wwdc/videos/2026-102.json
new file mode 100644
index 0000000..bc1ad25
--- /dev/null
+++ b/data/wwdc/videos/2026-102.json
@@ -0,0 +1,24 @@
+{
+ "id": "102",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/102/",
+ "title": "Platforms State of the Union",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Welcome to the 2026 Platforms State of the Union.\n\nThis is one of our favorite moments of the year where we get to share what’s new with the technologies, the frameworks, and the tools that you use every day to build incredible apps and games. Apps that inspire us, that raise the bar of what’s possible and that push us to build even better technologies.\n\nWe love connecting with so many of you. Hearing about your passions, your challenges, and how we can better support your work. Your feedback shapes some of the most important technologies that we build. And this past year, nowhere was that more true than with the new design with Liquid Glass and with Apple Intelligence.\n\nThese were both huge themes in the 26 releases, and they're key again this year, with many of our efforts influenced by your feedback.\n\nDesign and intelligence are both so important because they enhance what’s special about your apps. The care and the craft that you put into them.\n\nWith unique interfaces and rich experiences shaped by your deep domain expertise.\n\nCombined with enhanced intelligence capabilities, you can now build features that weren't previously possible. To highlight what's new, we'll dive into three key areas. First, Apple Intelligence, with new ways to bring generative intelligence directly into your apps, and new integrations with system intelligence to bring users back to your apps. Second, platform improvements, with design refinements and more flexible UI layout, updates to Swift and SwiftUI, and enhancements that make your apps faster, more adaptive, and easier to build.\n\nAnd finally, developer productivity, taking agentic coding even further, alongside improvements that make Xcode faster and more personal. We have a lot to cover, so let's get started with Apple Intelligence. At the heart of Apple Intelligence are Apple Foundation models. Working together with Google and leveraging the technologies behind their Gemini family of models, we created the latest Apple Foundation models to power our Apple Intelligence experiences and to provide even better support for the ways you’re using intelligence in your apps.\n\nWe adapted these models to run on device and on Private Cloud Compute. Apple Foundation Models power Apple Intelligence, and your apps can use them too, through the Foundation Models framework. This year, the framework's capabilities are expanding to include image input and support for server models. So if you have a more complex task requiring the most advanced frontier models the API can now integrate with the cloud model provider of your choice.\n\nTo ensure getting started with a large cloud model is as accessible as possible, even if you’re writing your first app, developers with fewer than 2 million first-time App Store downloads will be able to use Apple Foundation Models running in Private Cloud Compute with no cloud API cost.\n\nIt's access to frontier level intelligence with unparalleled privacy protections.\n\nBecause getting started exploring ideas shouldn’t be held back by infrastructure costs.\n\nWith these enhancements, the Foundation Models framework now offers a single API that supports any model you need. In addition to features you build within your apps, Apple Intelligence can also surface your app in more places across the system, giving users more ways to discover and return to it.\n\nThe App Intents framework connects your app to Apple Intelligence, drawing on core operating system technologies, like the Spotlight semantic index. It organizes and surfaces personal context from any supported app. The app toolbox, which identifies features available across apps to serve a user request. And the system orchestrator, which coordinates it all while protecting user privacy. Together, in-app and system-wide intelligence unlock experiences that neither could deliver alone. Your apps made more powerful by intelligence. And intelligence made more meaningful by your apps. Let’s dive into these frameworks and see how they’ll transform what your apps can do. Here's Richard and Mary Beth.\n\nThe Foundation Models framework is a native Swift API that gives you direct access to the same on-device model that powers Apple intelligence.\n\nAnd many of you have already adopted it, creating experiences for shopping apps like Wayfair, educational apps like CellWalk, local sports apps like CricHeroes and more, all running on device with no infrastructure costs or privacy trade-offs.\n\nIt's amazing to see how you've pushed the limits of what an on-device model can do. We've got exciting updates for you. Let’s start with a preview of the intelligence-powered features you will be able to build today. Mary Beth, over to you.\n\nThis year, we're building a sample app all about the Japanese paper craft of origami. It's a place to unwind and get creative with paper. I’ll give you a quick tour. Our app starts with a beautiful gallery of my origami projects. And what’s special about these is that I’ve used foundation models to tailor origami projects to match a person’s interests and materials with step-by-step feedback. Of course, it's more fun to craft with friends. So our app has a built-in chat. It's a fun focused place to plan meetups and talk about crafting.\n\nNow I’m working on a cool feature to combine people’s interests into an origami project. Here, I’ll take this paper Rachel’s bringing and mix in a photo of my dog to generate a fun project for us all to fold together. With Foundation Models framework, my app analyzes the inspiration pictures to get a sense of the materials I have and this dog theme I have in mind. It even translates the Japanese text and uses all of this context to brainstorm a few options. I’ll choose this one.\n\nThe intelligence keeps going in a fully interactive tutorial.\n\nThat's a quick preview. Let's talk about the framework. This year, we’re taking the Foundation Models framework to the next level. First, you get new capabilities like multimodal prompts with text and images.\n\nThis opens up new categories of experiences you can build with image understanding. It's as simple as attaching an image to your prompt. In addition to this, the Vision framework is now integrated, giving you purpose-built tools the model can use, such as OCR for precise text extraction, and barcode readers for quick code scanning all on-device. Next, let's talk about server models.\n\nOn-device models are incredibly useful for many tasks. Yet sometimes you might want a larger model for a more complex workflow.\n\nThat’s why we’re extending the framework so you can easily call server models like Claude, Gemini, and more to use features like tool calling and guided generation.\n\nAnd any model provider can create a Swift package that conforms to the Language Model protocol. So you can pick the one you want for your app. In addition to this, we’re opening up access for those of you getting started with AI to use the Apple Foundation Model running on Private Cloud Compute with no cloud API cost, giving you access to frontier-level intelligence while ensuring your user’s data is not stored or accessible to Apple or anyone else.\n\nYour users will have access to features leveraging the cloud model every day, and iCloud+ subscribers will have expanded access.\n\nNo matter the model you want to use, you can easily swap it in. This makes the Foundation Models framework the best way to run any large language model in your app. With more modalities and more models at your fingertips, the next thing you’ll need is more ways to put them to work. That’s why we’re introducing a new open source Swift package, loaded with pre-built tools to help you get started with concepts like skills and utilities for context management.\n\nFor example, a task management app like Tiimo can use the package to pull in a skill that adapts its tone and recommendations to the user’s data, delivering a personalized brief to help them stay on top of their day. This space evolves so quickly. Tomorrow's abstractions may be very different from today's.\n\nSo these utilities from the open source package are created with new fundamental building blocks called Dynamic Profiles. These are new declarative APIs in the Foundation Models framework for building truly adaptive AI experiences with less code, so you can orchestrate skills and sub-agents, swap tools in and out, and update instructions on the fly.\n\nI'll walk you through how Dynamic Profiles powers intelligence in the Origami app.\n\nFirst, let's open Xcode. I'll start with a LanguageModelSession, which many of you already use. Now, instead of creating a session with a fixed model, tools, and instructions, with Dynamic Profiles, you have the freedom to continuously update your session. So I’ll choose a profile for the LanguageModelSession and start with the familiar Swift result builder syntax.\n\nHere in the body, I’ll define my first Profile as a brainstorming helper that’s going to generate project ideas based on the photos I give it.\n\nI’ll add modifiers to use Private Cloud Compute language model with temperature cranked up for creativity. Now the beauty of a Dynamic Profile is that this body will always resolve to just one Profile driving my session at a time, but I can switch between as many Profiles as my feature needs in the same session.\n\nSo I’ll change Profile based on my app state.\n\nThen I can add in a second Profile to handle tutorial generation. Using Private Cloud Compute again with reasoning level set to deep, since this is my most challenging task. Last, I'll add in a Profile that explains any origami jargon, like “valley fold”, that the user doesn't understand. This is a nice smaller task. I can send the on-device SystemLanguageModel to save on server calls.\n\nLet's see it in action. Here in my tutorial, I can now tap this term I don’t understand, and the on-device model generates a nice explanation.\n\nIn Dynamic Profiles, I’m swapping out models, but everything shares the same continuous transcript. This means more contextual intelligence with less prompting. Now let's use that in my Tutorial Profile. Instructions and tools can be swapped in and out as well. So I’ll use my app’s view model to check if the tutorial’s been generated, and if so, I’ll add in instructions and tools to help the model give high-quality feedback tailored to the user. This body recomputes on every model turn, so my session will stay up to date. Finally, do you see how my three Origami Profiles look a bit like three AI agents? That's because Dynamic Profiles are designed to be adaptable building blocks.\n\nSo if you want to build AI agents or skills or any other high-level abstraction, you can. With the focus of flexibility and composability, these new APIs from the Foundation Models framework are here to grow with you.\n\nTo help you bring all of this to your apps, you’ll have access to a complete set of tools, from building to testing, to shipping with confidence.\n\nThat includes the new Evaluations framework, which gives you the ability to test your prompts and validate that your intelligence-powered features work reliably. The upgraded Foundation Models instrument will help you visualize and debug model behavior in your apps. And the new FM command line tool lets you prompt the model right from the terminal.\n\nAnd there’s so much more, like a Python SDK, tool calling with images, and a new RAG tool powered by Core Spotlight that’s private to your app.\n\nThat’s the Foundation Models framework, providing you seamless access to multiple models with all new capabilities in the native Swift API.\n\nPlus, we're doing something big.\n\nLater this summer, the framework will be open source. So the same Swift APIs you use in your app can now run on your server too, giving you a complete end-to-end AI workflow anywhere you deploy Swift. You've seen how the Foundation Models framework connects to third-party models, Private Cloud Compute, and the on-device model. You have the flexibility you need to get the right model for the job. And when you want to bring a specific model into your app and run it on device, there's Core AI.\n\nCore AI is a brand new framework built right into the platform, along with supporting tools and technologies. It's designed to be the best way to bring and run models on device in your apps.\n\nIt delivers uncompromising performance through a modern memory-safe Swift API, with extensive tuning capabilities from fine-grained interest management and model specialization to custom GPU kernels.\n\nAnd there are Python-based tools alongside the framework, so you can convert and optimize your PyTorch models for the Core AI runtime. The framework is backed by deep integration into a new developer toolchain, with ahead-of-time compilation, dedicated Core AI instruments, and a powerful visual debugger to trace tensor values directly back to your original Python source code.\n\nAnd it’s engineered to scale with your available compute.\n\nSo you can run a compact vision model in your iPhone app for real-time camera queries. Or deploy a multi-billion parameter LLM in a Mac app right at your desk to power an agentic assistant for complex, multi-step workflows.\n\nWhatever the device, whatever the model, it all runs on-device with zero server dependencies and zero token costs. Core AI is optimized for performance on Apple silicon, and it empowers Apple Intelligence experiences across the system, including Siri. And this year, Apple Intelligence offers even more opportunities for developers. Over to Lori. Apple Intelligence draws on personal context from across apps, understands what’s on screen, and can take actions to get things done. And now you can integrate with its capabilities through the App Intents framework. It's how our platforms understand what your apps can do. With it, you can make your app’s content easier to find and its capabilities easier to use through the Action button, Shortcuts, widgets, and in Siri AI.\n\nApp Intents schemas make integration with Siri's capabilities easy. Schemas are recognizable structures that Siri understands deeply, built on years of language model training. We provide entity schemas for describing the content and concepts your app works with, and intent schemas for describing the actions it can perform. Entity schemas enable personal context understanding. By contributing your app’s content to the Spotlight semantic index, you can help your users find information from your app quickly and easily with attribution back to your app.\n\nThe indexing keys for important content properties are built right in, which means more understanding from less code. Siri's understanding of intent schemas means people can make requests naturally. They don’t need to learn specific phrases and you don’t have to define them in your code. Schemas cover common app categories like task management, photo editing, and communication, and include a whole set of system-supported actions. Just adopt the relevant intent schemas for the actions your app can perform to make them available to your users. And because these schemas are system-defined, they’ll benefit from future updates. That means as Siri’s language understanding evolves or as we add new support for languages or regional dialects, your intents will work there too, without any changes to your code. Combining these capabilities with the new View Annotations API will let your users reference and take action on the content in your app when it's on screen. So your users can interact with your app conversationally, saying what feels natural to them, not commands they have to memorize.\n\nNow, I'm going to show you how we've made our Origami app work with Siri. I already have some entities and intents that describe my app’s content and actions, and the entities conform to the IndexedEntity protocol, so they can be indexed into Spotlight. By also conforming to an entity schema, Siri will be able to discover and reason over my app's content. I’ll make sure my Message, Contact, and Conversation entities all conform to the relevant entity schemas by using the @AppEntity macro. I’m indexing these entities into Spotlight when my app finishes launching to make sure everything’s in sync. I’ve rebuilt to get the latest changes, and now let’s see what I can do, even when I’m not in the app.\n\nHey Siri, who's coming to origami night? Siri: Based on your messages in Origami, it looks like Kevin, Mary Beth, Rachel, and Richard are discussing origami night. What's Richard bringing? Siri: Richard mentioned he is thinking of bringing pizza. Awesome. But now I want to follow up, which means I need to act on this information.\n\nI can make the content Siri found actionable by conforming an intent to the sendMessage schema. This time I’m using the @AppIntent macro, since this is an action rather than content.\n\nI’ll build and run again, and now I can say, Siri, text Richard, “Can you make one of the pizzas vegetarian?” Siri: From Origami: Ready to send it? Yes.\n\nSiri: It’s sent. Great. Message sent. I’d also like to let people reference what’s on screen in my app just by saying “the second message” or “this photo”.\n\nThe new View Annotations API lets me associate my views with entities, which can then be passed to my app’s intents, making them actionable.\n\nMy Message List view contains all the individual messages in a conversation. I can use a new view modifier to map each message row to its respective MessageEntity.\n\nLet’s try it out. Hey Siri, send this photo to Kevin and say, “Rachel got us some paper to practice our folds. What color would you like?” Siri: From Origami: Ready to send it? Yes.\n\nSiri: It’s sent.\n\nAnd Siri sends the photo with my message.\n\nBy combining personal context, common app actions, and on-screen awareness, your app can become part of the intelligent fabric of the system.\n\nThrough Siri, users can access it through natural language, discover it through semantic search, and integrate it into their daily workflows. Now, back to Josh.\n\nThis is our vision for an intelligent platform. Rich, native experiences and intelligent natural language interfaces working together.\n\nAs app developers, this represents an incredible opportunity. You can enhance your app’s experiences through natural language with Siri, build powerful AI features with the Foundation Models framework, and even run your own models on-device with Core AI. If you’re using a custom model to power a feature within your app, Core AI is the right technology to use.\n\nYour models will perform efficiently across all devices, and it’s built into the platform, so your apps always benefit from the latest fixes and enhancements. And if you're an enthusiast who is experimenting with, training, researching, or fine-tuning generative models, or if you're running a local inference server, our array framework, MLX, makes it easy to explore cutting-edge innovations and technologies.\n\nIt now supports Metal 4, GPU Neural Accelerators, and it can even scale training across multiple Macs with RDMA over Thunderbolt. It's all open source and it's faster than ever.\n\nThe breadth of capabilities offered by Apple Intelligence and powered by Apple silicon makes Apple’s platforms the best place to build and deliver the next generation of intelligence-enhanced apps and games.\n\nNow let’s take a look a level deeper at what makes all of this possible - the systems your apps depend on, from the frameworks you call to the processes that schedule your work, manage your memory, and render your UI.\n\nWe took an especially close look at the performance and quality of these foundations, and you’ll see a multitude of platform improvements in this year’s releases. When you rebuild with the new SDK, your apps will launch faster and feel more responsive. You'll see refinements and platform improvements across frameworks media, search, and accessibility, enhancements to Swift and SwiftUI, and a lot more, especially around design.\n\nLast year, the new design with Liquid Glass brought a unified design language built for a world where your experience moves across devices.\n\nThe new design is making apps more expressive and delightful while staying instantly familiar to users. It looks great in apps like Tide Guide, where subtle, interactive highlights respond as users scroll through tide data and charts.\n\nAnd SketchPro, where translucent brush panels and controls let artwork show through, even while you switch between tools. Throughout the last year, we’ve been refining the design, and that journey continues with a new set of design updates in the 27 releases.\n\nApps that have already adopted Liquid Glass benefit from many of these improvements automatically. To tell you more, here's Cindy.\n\nIn this year’s releases, you’ll see updates to the foundations of how Liquid Glass is built, refinements to the new design that improve consistency, and new ways for iOS apps to adapt across devices and screen sizes. Let's review the changes you'll see in your apps.\n\nTo maintain exceptional readability, we tuned Liquid Glass so it more effectively diffuses complex content behind it.\n\nAnd to establish more depth and separation, we also introduced a darkened edge along with brighter specular highlights. We also made it more personalizable with a new slider in settings to adjust Liquid Glass anywhere from ultra clear to fully tinted, allowing users to choose the look that works best for them.\n\nApps already using Liquid Glass get these improvements automatically when they run on this year’s releases without even needing to recompile. Liquid Glass seamlessly adapts to a variety of accessibility settings users may choose, such as reducing transparency or increasing contrast.\n\nAnd now macOS 27 also supports the “show borders” environment value, just like iOS. So you can adapt your macOS app's custom controls for this setting as well. Sidebars expand to the edges on Mac and iPad, providing clearer structure while still refracting content from your app and the wallpaper. And icons in the sidebar regain their color using your app’s accent color, giving your app more personality and making it more clear which window is key. List and Label APIs provide these updates automatically and support customizing the tint per item. And every window on macOS now also has the same tighter corner radius, ensuring greater consistency across all apps. When content scrolls under floating bars, a uniform toolbar appears across the top and keeps the text legible while improving contrast.\n\nThis effect is applied automatically for standard toolbars and can be customized using the existing scroll edge effect APIs.\n\nWe also thought about how icons and menus can be used intentionally to call attention to the most important actions, both on macOS and iPadOS.\n\nWhile icons are hidden by default, there’s an API to show icons for key app actions. We’re also updating how Liquid Glass shows up in icons, making them sharper and more defined. This updated rendering applies to all app icons, and we’ve introduced new features such as refraction that can be selectively used for added character. And with Icon Composer, you can now design your icons out of multiple layers of Liquid Glass. It’s been updated with new annotation features to add refraction or dial in Liquid Glass content effects. And it provides an interactive preview of how your icon will look on earlier releases. Together, these updates culminate in a more focused and approachable experience across your apps and across platforms.\n\nNext, let’s talk about app adaptability. iOS apps show up in more places than ever, on iPad as an iPhone app, or on Mac through iPhone Mirroring. When your iOS app shows up in these other contexts with larger displays, users want to be able to take advantage of the extra space to see more information.\n\nSo this year we’re introducing support to resize iOS apps in iPhone Mirroring and on iPad. Let’s see how this works with the Origami app. Once you rebuild with the latest SDK, your app is automatically opted in to resizability. Since Origami is a SwiftUI app, it’s already taking advantage of scene lifecycle and standard framework support for basic resizability.\n\nIf you’re already using SwiftUI, Auto Layout, or responding to size class changes, you are well on your way to supporting full resizability. If you have custom views, you’ll want to update them to using auto layout and trait collections for layout decisions.\n\nUsing the new resizable iOS simulator and Previews, you can test across a variety of screen sizes right in Xcode, so you’ll see exactly how your layout performs.\n\nAnd we’re providing a skill for coding agents that will help you find and fix common resizability issues. Now, instead of designing for specific devices and orientations, you’re designing for a dynamic range of sizes and aspect ratios.\n\nTo provide the best experience when using iPhone Mirroring, update your app to be able to adapt and support any size. Resizable simulator, Previews, and iPhone Mirroring all make it easy to ensure your app is as dynamic and flexible as possible.\n\nNext, let’s talk about SwiftUI. Here's Franck.\n\nSwiftUI is the best way to build apps for any Apple device. We designed SwiftUI to capture everything we know about building great apps on our platforms. It gracefully handles the complexities of layout, animation, and platform integration so you can focus on what makes your app yours. And as new capabilities like Liquid Glass are added, apps get these features easily because they’re designed with SwiftUI in mind.\n\nNew apps like Xogot are built with SwiftUI because they want to feel truly at home on Apple platforms. Xogot is a game development environment that brings the open source Godot engine to Apple devices.\n\nIt started on iPad, expanded to iPhone, and when the time came to bring it to Mac, it felt completely natural. And apps that previously used cross-platform or web technologies like Notion are migrating their user interface to SwiftUI because they want a level of performance and UI consistency that other technologies can’t deliver. With powerful agentic coding tools, porting code to Swift has never been easier. Of course, we reach for SwiftUI ourselves whenever we build apps.\n\nFor example, SwiftUI made it easy to build a new Siri app by enabling us to share code across all our platforms and Creator Studio apps like Logic Pro, build new features with SwiftUI for high performance and cross-platform support. Since we rely on SwiftUI ourselves, every improvement we make for our own apps becomes an improvement for your apps too. And this is a big year for SwiftUI. With richer interactions that help you write less custom code, with speed making your apps much faster, and finally, with new capabilities for your apps. Let’s start with interactions.\n\nThis year, SwiftUI brings more dynamic interactions to your app like reorderable containers, which makes it super easy to add drag to reorder to any container. Building a grid reordering experience outside of lists, like with this grid in the Origami app, used to require a lot of code.\n\nNow, it is just as simple as adding .reorderable() to your ForEach and .reorderContainer() to the parent. And just like that, I can customize the order of my Origami models.\n\nSwiftUI handles the lift and the drop animations and it works with any container like grids and stacks. Now, for Origami models I feel a little less proud of, SwiftUI now supports swipe actions inside any container as well. I can delete a custom row with a swipe by adding the existing️ .swipeActions() modifier to my row and .swipeActionsContainer() to the scrollable container.\n\nThis provides great flexibility for quick actions on my custom row.\n\nFinally, text selection got more flexible too.\n\nOn iOS, it gains the same. full-fidelity selection already found in TextField and TextEditor.\n\nAnd on macOS, it now supports custom text renderers, text vibrancy, and vertical text. Next, let’s take a look at speed.\n\nThis is always a priority for us, but even more so this year, and you will see many improvements without any changes on your end.\n\nTo start, we’ve been gradually unifying the architectures of SwiftUI, AppKit, and UIKit, and this year, they share a common foundation across many controls. So wherever your app is running, they can benefit from the same on-the-line improvements.\n\nFor example, menu pickers on macOS are now better equipped to smoothly handle large lists of items. And in nested stack layouts, whereas SwiftUI used to measure each child multiple times to resolve their flexibility, it now short-circuits computations where they’re not needed, meaning layouts now resize up to twice as fast. And nothing saves performance like avoiding unnecessary work. SwiftUI now only initializes state objects when they're first loaded.\n\nPreviously, a new temporary instance of the state object would get created every time the view is reinitialized.\n\nYou get this improvement for free because state is now lazy under the hood and was converted from a dynamic property to a macro.\n\nAnd when it comes to loading images, AsyncImage avoids redundancies as well. It now caches its content automatically using standard HTTP caching, so images are downloaded once and only re-fetched when needed.\n\nFinally, let's talk about new capabilities starting with toolbars.\n\nWith the new resizability features, optimizing your app for a dynamic range of sizes and aspect ratios is more important than ever.\n\nAnd toolbars are central to that experience.\n\nThis year, SwiftUI gives you finer control over how toolbar items adapt to space.\n\nUse the new visibilityPriority modifier to mark your most important items high. And SwiftUI keeps them visible longer as space shrinks. Less prominent actions, like archive or delete, can be added to the new toolbars overflow menu container, which groups them in an overflow menu. And finally, the new topBarPinnedTrailing placement anchors items to the trailing edge, no matter how the toolbar reflows. Now, when I resize the window, the toolbar stays organized exactly how I want. Important buttons stay visible. Deprioritized ones are in the overflow menu and share is always pinned to the trailing edge. Tabs can also be distinguished with the new prominent tab role pinning the tab to the trailing edge of the screen.\n\nSwiftUI also opens up new ground for document-based apps with a new document infrastructure that provides a ton of functionality out of the box, like first-class URL access for fully customizable reading and writing to disk, the kind that powers apps like Xcode or Pages. For example, with direct access to the file URL, you now have the flexibility to read just the parts of a file you need and write only the pieces that changed, not the entire file. You can also observe and update document attributes using the provided observable configuration. The new document API integrates deeply with modern Swift, with support for observation, Swift concurrency, and so much more.\n\nLastly, here is something pretty awesome. The Spatial Preview framework gives Mac apps new ways to extend in space around users wearing Apple Vision Pro.\n\nWhen you adopt this new API in your app, a 3D model can become spatial when you stream to Apple Vision Pro, allowing your users to preview, edit, and share objects and models in real time. Beyond those we've mentioned already, there are many other new improvements, including better type checking performance with content builders, a new alert binding API, and to support adjusting cross-fade transitions.\n\nTogether, these platform improvements bring more speed, richer interactions, and powerful new capabilities to SwiftUI that you can take advantage of.\n\nNow, let's take a look at Swift itself. Here's Holly.\n\nSwift is designed to be the language you reach for at every layer of the stack.\n\nWhether you’re building full-featured mobile apps, internet-scale services, or embedded firmware, Swift helps you write code that's fast, expressive, and safe. Swift's performance, depth, and unmatched interoperability make it the natural successor to C and C++ for low-level systems and server programming. And its approachability and expressiveness make it ideal for higher-level development like apps and frameworks. We think Swift is the only language with this breadth. That's what makes Swift a language you can keep reaching for as your stack grows.\n\nIncluding outside Apple platforms, where the tools for development on Linux, Windows, Android, and the web are available on Swift.org.\n\nMany of you are extending your use of Swift to additional platforms and the server so you can reuse code and benefit from Swift’s high performance across your entire stack, like Flighty, which uses Swift in their services to share the code that tracks airport visits between the app and backend. Or GoodNotes, which uses Swift for WebAssembly to bring the app to the web, Chrome OS, Android, and Windows, reusing over 100,000 lines of code.\n\nOr Frameo, which uses Swift-Java interoperability to share Swift libraries between the iOS app and the PhotoFrame software written in Java.\n\nInteroperability means you can bring Swift into C, C++, and Java systems you already have. So you can get Swift's benefits at every layer without a rewrite. At Apple, we've been building with Swift at every layer of our own stack. Foundation paved the way for Objective-C frameworks to move to native Swift under the hood. AppKit and UIKit have followed suit by using Swift and SwiftUI extensively in their implementation. WebKit, the open source web engine that powers Safari, is a large and security-critical C++ code base.\n\nUsing Swift’s safe C++ interoperability, WebKit is replacing core components with Swift versions incrementally. In the networking stack, the QUIC transport layer was rewritten in Swift. Later this month, the project will be open sourced and available for cross-platform use through SwiftNIO integration. You can follow along or get involved through the vibrant open source community on Swift.org.\n\nFurther down the stack, more security and performance critical systems moved to Swift this year. The TrueType font rendering engine replaced decades of hand-optimized C with Swift code that’s not only memory safe, but also faster. At the lowest level, we’ve written hundreds of thousands of lines of Swift code across bare metal firmware, coprocessors, and drivers.\n\nFor the 27 releases, we've started writing parts of the core operating system kernel in Swift.\n\nAs Swift becomes more capable in these domains, we’re staying true to one of Swift’s most important design goals: it’s fun.\n\nIt's natural to write and iterate on your ideas in Swift, and the compiler is there to catch mistakes along the way. The latest updates are focused on improving your workflow so you can focus on the fun part: writing great code with confidence. Swift 6.4 is here, built to make everyday tasks feel effortless. I'll show you just a few examples. When your code base is undergoing a migration or incremental adoption of new features, sometimes it’s not realistic to address all compiler warnings across your project at once. You can now suppress warnings in specific parts of your code, and you can promote warnings to errors in places where you want strict enforcement. Availability attributes can get long and repetitive when you’re writing code for multiple Apple platforms. Now, instead of listing out every Apple platform with the same version number, you can simply write ‘anyAppleOS’. The limitation on async calls in a defer block is gone, and awaiting inside a defer just works.\n\nNo matter what you’re building, Swift’s compiler diagnostics are a daily companion, helping you catch mistakes early and guiding you toward correct code. If you’ve spent time writing Swift code, you’ve probably encountered this error message: “The compiler is unable to type check this expression in reasonable time.” This can happen in complex operator expressions, closures, or in deeply nested SwiftUI view bodies.\n\nThis is frustrating, and we’ve made it a lot better.\n\nIn many common cases, code that hit this fallback error will now either compile successfully or give you a more actionable error to work with.\n\nWe know this area is important for a smooth workflow, and we’re continuing to invest in it.\n\nSwift 6.4 makes you more productive in day-to-day code, and it brings that same care to the more specialized corners of your project. There's never been a better time to go full stack with Swift. Now back to Josh.\n\nSo those are the platform improvements in this year’s releases. Now, to move ahead, sometimes we have to leave something behind.\n\nAs we said last year, macOS Tahoe was the final release to support Intel Macs.\n\nThe transition of macOS to Apple silicon is now complete, enabling us to focus on a single architecture across the entire ecosystem. This can benefit your apps as well. You can now ship Apple silicon-only binaries on the Mac App Store, reducing your app’s download size and letting you focus your testing on a single architecture.\n\nAnd with all the refinements to the new design with Liquid Glass, it’s time to complete your migration there too. We'll be removing support for opting to use the old design. So once your app is recompiled with Xcode 27, it will automatically begin to use the new design with Liquid Glass.\n\nWith so many improvements across the system, your apps and games will look and feel better than ever on this year’s releases. Now, let’s turn to your productivity and the tools you use to build with and for our platforms. Intelligence is deeply transforming how you write code, add new features, and build apps. Last year, we brought AI coding assistance to Xcode, and so many of you embraced it immediately. It's helping you write code faster and adopt new APIs more easily. This space moves really fast, so we've picked up the pace of our releases, delivering new Xcode capabilities to you faster than ever before. Earlier this year, we brought coding agents to Xcode, along with tools allowing agents to grab a preview, search documentation, and more, powered by the Model Context Protocol.\n\nWith MCP, Xcode also connects to the tools you already use, from design apps like Figma to services like GitHub.\n\nAnd Xcode includes a built-in integration for agents from Anthropic, OpenAI, and now Google. Today, Xcode adds support for Agent Client Protocol, so you can bring any compatible agent into Xcode. ACP support and Gemini integration are shipping in an update to Xcode 26 available today. And there’s more coming in Xcode 27.\n\nHere's Ken.\n\nFrom the first line of code to the App Store, Xcode is where you build the best apps for Apple platforms.\n\nMillions of you live in it for writing and debugging your code with coding agents right alongside, designing interfaces in SwiftUI and previewing them in real time, testing across devices and simulators to catch issues before your users do, and profiling performance with instruments, keeping your apps fast, responsive, and efficient.\n\nThis year, Xcode has two big stories. The first is intelligence, and we have a lot to talk about in a minute. The second is the daily experience, how Xcode feels to use.\n\nNow just like you, we spend hours in Xcode every day. We build all our operating systems and apps with it. In fact, we build Xcode with Xcode. So it needs to feel like home while being fast, fun, and personal.\n\nAnd we've heard your feedback and improved Xcode across the board. It's faster at loading projects. We fixed top crashes and spins.\n\nDebug sessions are more reliable, with faster expression evaluation, and a console that can handle more intensive logging without hitching.\n\nXcode 27 is 30% smaller. Now, Apple silicon-only, with agents, documentation, and other components downloading in the background, so you're always up to date. Now, let's take a look at the experience. First, your Xcode settings are now automatically saved to iCloud. When I'm setting up a new Mac like this, Xcode offers to import them.\n\nI'll pull in the settings from my iMac. I can sign in with my Apple ID. Xcode fills in my Git config too. And just like that, I'm ready to code with my new Mac. Now, let’s create a new project. Watch this. I'll select new project, then app and boom, I'm in the editor. No file name, no bundle ID, no setup.\n\nOf course I can specify all those things later when I’m ready. This is great for exploring an idea, a new API, or prototyping a view.\n\nAlright, now I'll open the Origami project.\n\nXcode 27 looks beautiful with the design refinements of macOS 27.\n\nCrisp and clean. Let’s customize it. In Xcode 27, you can make the toolbar your own. It's easy to rearrange things, so I can add what I need and remove what I don't. The activity view is now tucked neatly into the document title over here, so there’s even more room for the things I want. The navigation buttons, canvas toggle, editor splits, they’re all right up here on the toolbar.\n\nNow I’ll add a shortcut to quickly create a new coding assistant conversation and that’ll be useful a little bit later.\n\nNext and super fun, themes. Color now flows throughout the entire app, not just the editor. You can personalize everything from the background to syntax colors and dial in that perfect shade of purple for your keywords. And Xcode 27 comes with gorgeous new choices. Let me show you a few of my favorites. Emerald. That feels fresh. You can almost smell it. How about something with a little bit more energy? Neon Noir. Electric. Love it.\n\nLight or dark? Every theme supports both. Here’s Coral Reef. I feel relaxed already. And when I’m working on multiple projects at the same time, I can set a different theme for each. Makes it super easy to tell them apart at a glance.\n\nAll right, let's get back to work. Next, Xcode Cloud, which gives you continuous integration and delivery built right into Xcode.\n\nI’ll set up my Origami project to use it. I'll click get started, grant access to my repository, and that's it. I can kick off my first cloud build. No App Store Connect setup needed.\n\nAnd Xcode Cloud builds are up to twice as fast, now supporting Apple Vision Pro and apps using Metal on Apple silicon.\n\nNext, Previews. They are the best way to iterate on UI and the easiest way to see how your views look across variants, like accessibility sizes, orientations, and localizations. And now you can see variations for any property.\n\nI’ll open this view here that shows a craft note. It renders differently based on the CraftState enum, which has four different values. Now, I can pass that enum to the preview and just like that - I get a grid showing all the states of my UI. All four in one glance.\n\nNext, another one I'm excited about. When testing your app, you use real hardware to evaluate performance, use sensors, and test real-world conditions. And you use simulators to cover older OSes and devices that you don't have.\n\nXcode 27 brings both together in the new Device Hub.\n\nIt replaces Simulator and it does a whole lot more too. Let me show you.\n\nWhen I first run my app, the window looks like the Simulator I know. I can easily rotate, grab a screenshot and jump back to the home screen, just like I’m used to.\n\nWhen I extend the view, I can now change device properties and test how my app responds to different system settings. Like switching to dark mode, increasing the font size, and more.\n\nWe rebuilt the experience from the ground up for the highest fidelity possible.\n\nSo I can pinch to zoom, use two-finger scrolling, and like in Previews, I can dynamically resize the simulator to see how my iOS app handles different sizes. I can also manage and interact with physical devices from the same place, like this iPhone here on my desk. I'll launch the Origami app right here from my Mac. And I can interact with it. All the convenience of the simulator with the fidelity of real hardware in a single place.\n\nThat is a quick look at the experience in Xcode 27.\n\nAnd beyond the experience, the biggest changes this year, the ones that will truly accelerate you, are in intelligence.\n\nKevin, over to you.\n\nWhat a great time to be a developer. Intelligence is transforming how you build apps. Agentic coding, together with Apple platforms, frameworks, and tools, helps you bring ideas to life. Xcode 27 takes the next big step in agentic coding, leveraging the full power of the best models and agents directly into Xcode.\n\nAgents are woven into every layer of the Xcode experience, from the way you interact with them to a set of tools that help you get the best results. Tools like understanding your project, searching documentation, building, and testing. And Xcode 27 helps you even more with new tools like rendering previews with variants, interacting with the simulator, localizing your app, debugging, and more. And this goes beyond tools. When using agents in Xcode, every answer is grounded in Swift, SwiftUI, in Apple frameworks. That’s why, when building for Apple platforms, Xcode is the best place to code with agents. Let me show you what this looks like across every stage of app development.\n\nFrom starting with an idea to implementing and validating it, to improving it, like adding new languages or fixing issues. First, I’m gonna add something fun to the Origami app. My daughter, she loves making origami with me. I want to surprise her with a feature that makes up a little choose-your-own-adventure story about the characters that we make together. I’ll start with a new conversation with the agent. I'll create one here from the toolbar. It opens right in the editor, just like any other file. Here’s what I want to build. In my Origami project, I want a button that generates a choose-your-own-adventure story for my daughter. That alone would get me good results, but I had something a little more specific in mind, so I’ll add some more details. She’ll start by picking some options, like the setting and an item to use in the story. The app will generate the first page, and then let her pick what comes next. I want to use the latest Foundation Models APIs and present it with beautiful typography. When using a coding agent, the best results come from collaborating on the implementation and design first, before any code is written. So I'll add /plan to the prompt. And I’m gonna ask for a diagram while I’m at it. I find it easier to review the plan that way. Let’s get this started. The agent is exploring my project using Xcode tools to help it efficiently understand my code base, its architecture and patterns, to find the best way to build the feature, and it’s asking some clarifying questions. Do I want to persist it? Yes. And how many options for the next part of the story? Two to three is good. And the agent continues to create the plan. Let’s skip forward in time. My plan is now ready, and it shows right next to the conversation in beautifully rendered markdown. It’s very easy to review, and I can also refine it. Here, the current implementation has a fixed set of settings and items to choose. I'd love if she could add her own. I’ll add that as a comment. And now the plan looks good. Let's kick it off. Now Xcode and the agent work together to implement the plan. Xcode shows everything that's changing, like code and previews. As it runs, I can refine the implementation. Like here, I'm gonna add a fun image filter to the story's hero image.\n\nIt looks like Xcode is done building my feature. There's still a lot of code, and the previews look great. Let's just run it. I’ll open an Origami project, tap the new toolbar icon, and just like we asked, page one offers an item and a setting. I’ll pick a forest and a wand, and oh look, there’s the button if I wanted to add my own. And here's the first page of the story with that gorgeous hero image. My daughter's gonna love this. What was just an idea a couple minutes ago is now something I can run in my app. This is amazing! Now, building a feature is more than writing code. It's also making sure it does what it's supposed to do. Xcode 27 can help you with that too, with new tools for agents to check their work.\n\nFor example, agents can validate the logic of your app by running tests, try ideas in isolation using playgrounds, like experimenting with APIs, and check visual changes with Previews in light and dark mode, different orientations, text sizes, or localizations.\n\nAnd now agents can interact with your app in the simulator. Let me show you. I want to test different combinations of settings, items, even customized ones. Xcode launches Origami and Device Hub and starts testing those for me.\n\nThe agent can tap, swipe, and type. When it's done, I get a summary of the tests, and I can see all the screenshots it created along the way. And just like that, the Origami app has a new story feature, designed, built, and tested end-to-end. Next, let's see how we can use agentic coding to improve our app. Agents in Xcode can help with all kinds of engineering tasks, like adopting new APIs, making your app more accessible, and more. Let's localize our Origami app. I’ll start with French. Xcode automatically adds a new language to the strings catalog, then works with the agents to translate strings across the entire project. This is more than a word-for-word translation. Xcode looks at each string in its context, the surrounding code, UI, the action, to find the best translation. And when it's done, I can see all the translations here in the String Catalog. Let's build and run. My app is now localized! Fantastique! Building a great app also means responding to the feedback and data from your users. The Organizer already gives you insights into how your app is doing in the real world - crashes, hangs, performance metrics, anonymized and aggregated. Now, you can use agents to help you find issues that matter most, and fix them. I'll ask Xcode to pull up the top crashes from the latest release. I get a list of crashes ranked by how often they happen. Oh, that first one is from an update I pushed last week. Let's fix it.\n\nXcode looks at the symbolicated crash log, figures out where in my project this happens, identifies the issue, reproduces the crash, makes the fix, and then validates it.\n\nAnd just like that, our issue is fixed. That's agents at work with Xcode across every stage of your development, planning, building, and improving. You'll be amazed when you see Xcode and agents bring your ideas to life.\n\nFinally, let's talk about what makes all of this possible. Xcode 27 ships with the expertise of Apple’s engineers and designers built right in as a corpus of skills, documentation, and MCP tools.\n\nThink of them as specialists. A SwiftUI specialist that knows how to structure your view and data flow. An accessibility specialist that knows what makes an interface work for everyone. Specialists for universal sizing, testing, and performance. In fact, so much of what we’ve seen to this point is powered by one of these specialists. And you can bring your own! Xcode integrates all of them in the same way. Plugins. It's a format used by many agents and widely adopted by the community. It's amazing to see how many of these you have already been building and sharing. A plugin can contain skills, just markdown files that teach the agent new tasks. It can contain tools using the Model Context Protocol. And we’ve added one new capability to plugins. With the Agent Client Protocol, a plugin can bring an agent of your choice.\n\nInstalling one is easy. You can use the command line or paste a git URL right into Xcode.\n\nAnd partners like Figma and GitHub make it even easier to set up with just one click.\n\nAnd then I can put it all together. I can tell Xcode to implement a Figma design in SwiftUI, refine it for different variants, make it resizable using a skill, and post a PR to GitHub. How cool is that? That's Agentic Coding in Xcode 27. From an idea to an app, at every step. Built for how you create, refine, and ship, and reach your users wherever they are. We can't wait to see what great things you make next! Back to you, Josh.\n\nIt's a huge year for Xcode and for many of our other developer tools as well. Like the all-new Reality Composer Pro 3, which has been completely rebuilt for crafting production-ready 3D experiences using RealityKit. It brings support for character animations, more realistic lighting, and live previews that let you see the results of your edits as you make them using Mac Virtual Display. And there’s even more for game developers in this year’s releases, including a major update to Game Porting Toolkit, which dramatically cuts the time it takes to bring games to Apple platforms by adding AI skills for coding agents. And new Metal command line tools give agents direct control during development and debugging, bringing best practices for game development on Apple platforms to every step of your porting journey.\n\nSo that's developer productivity. Across the 2027 releases, there are so many new capabilities to build on, like the App Intents framework, which lets you connect your app to Apple Intelligence, and the Foundation Models framework and Core AI, which enable you to bring powerful generative intelligence features directly into your apps. There are platform improvements across design, Swift, and SwiftUI that make your apps faster, more flexible, and easier to build. And Xcode has even more expansive support for agentic coding. and we’ve only just scratched the surface. There are over 100 sessions to dive deep into everything we've covered today, including Apple Intelligence, Xcode 27, Design, and more.\n\nAll these sessions are available on the Apple Developer app, the website, YouTube, and new this year on Bilibili. And there's so much more happening online throughout the week. Sign up for Group Labs, online panels, and Q & A sessions with Apple engineers and designers. And connect with us on the Apple Developer Forums to ask questions, and follow the conversation about the latest tools and technologies. And the opportunities to connect extend well beyond this week.\n\nYou can Meet with Apple around the world and online in hands-on workshops, labs, and events to learn and connect as a community throughout the year.\n\nWe love when we can meet you in person. And there are many opportunities to do that in our Developer Centers, located in Cupertino, Shanghai, Singapore, and Bengaluru. And we’re excited to announce the opening of our fifth this fall in Berlin, home to one of Europe’s most vibrant developer and designer communities. We can’t wait to see you there or in one of our events online. Whether this is your first WWDC or your 25th, thank you. Your work inspires and drives us. We use the apps and play the games that you all build. So, our greatest hope is that everything we talked about today will enable your next great idea to come to life. We can't wait to see what you do next. Enjoy WWDC.",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/102/2/abb4bd38-dfae-46cf-985f-160769b92d41/downloads/wwdc2026-102_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/102/2/abb4bd38-dfae-46cf-985f-160769b92d41/downloads/wwdc2026-102_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:11.410Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-111.json b/data/wwdc/videos/2026-111.json
new file mode 100644
index 0000000..5298940
--- /dev/null
+++ b/data/wwdc/videos/2026-111.json
@@ -0,0 +1,18 @@
+{
+ "id": "111",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/111/",
+ "title": "Keynote (ASL)",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": []
+ },
+ "extractedAt": "2026-06-12T10:24:11.039Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-112.json b/data/wwdc/videos/2026-112.json
new file mode 100644
index 0000000..81c5f0c
--- /dev/null
+++ b/data/wwdc/videos/2026-112.json
@@ -0,0 +1,24 @@
+{
+ "id": "112",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/112/",
+ "title": "Platforms State of the Union (ASL)",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Welcome to the 2026 Platforms State of the Union.\n\nThis is one of our favorite moments of the year where we get to share what’s new with the technologies, the frameworks, and the tools that you use every day to build incredible apps and games. Apps that inspire us, that raise the bar of what’s possible and that push us to build even better technologies.\n\nWe love connecting with so many of you. Hearing about your passions, your challenges, and how we can better support your work. Your feedback shapes some of the most important technologies that we build. And this past year, nowhere was that more true than with the new design with Liquid Glass and with Apple Intelligence.\n\nThese were both huge themes in the 26 releases, and they're key again this year, with many of our efforts influenced by your feedback.\n\nDesign and intelligence are both so important because they enhance what’s special about your apps. The care and the craft that you put into them.\n\nWith unique interfaces and rich experiences shaped by your deep domain expertise.\n\nCombined with enhanced intelligence capabilities, you can now build features that weren't previously possible. To highlight what's new, we'll dive into three key areas. First, Apple Intelligence, with new ways to bring generative intelligence directly into your apps, and new integrations with system intelligence to bring users back to your apps. Second, platform improvements, with design refinements and more flexible UI layout, updates to Swift and SwiftUI, and enhancements that make your apps faster, more adaptive, and easier to build.\n\nAnd finally, developer productivity, taking agentic coding even further, alongside improvements that make Xcode faster and more personal. We have a lot to cover, so let's get started with Apple Intelligence. At the heart of Apple Intelligence are Apple Foundation models. Working together with Google and leveraging the technologies behind their Gemini family of models, we created the latest Apple Foundation models to power our Apple Intelligence experiences and to provide even better support for the ways you’re using intelligence in your apps.\n\nWe adapted these models to run on device and on Private Cloud Compute. Apple Foundation Models power Apple Intelligence, and your apps can use them too, through the Foundation Models framework. This year, the framework's capabilities are expanding to include image input and support for server models. So if you have a more complex task requiring the most advanced frontier models the API can now integrate with the cloud model provider of your choice.\n\nTo ensure getting started with a large cloud model is as accessible as possible, even if you’re writing your first app, developers with fewer than 2 million first-time App Store downloads will be able to use Apple Foundation Models running in Private Cloud Compute with no cloud API cost.\n\nIt's access to frontier level intelligence with unparalleled privacy protections.\n\nBecause getting started exploring ideas shouldn’t be held back by infrastructure costs.\n\nWith these enhancements, the Foundation Models framework now offers a single API that supports any model you need. In addition to features you build within your apps, Apple Intelligence can also surface your app in more places across the system, giving users more ways to discover and return to it.\n\nThe App Intents framework connects your app to Apple Intelligence, drawing on core operating system technologies, like the Spotlight semantic index. It organizes and surfaces personal context from any supported app. The app toolbox, which identifies features available across apps to serve a user request. And the system orchestrator, which coordinates it all while protecting user privacy. Together, in-app and system-wide intelligence unlock experiences that neither could deliver alone. Your apps made more powerful by intelligence. And intelligence made more meaningful by your apps. Let’s dive into these frameworks and see how they’ll transform what your apps can do. Here's Richard and Mary Beth.\n\nThe Foundation Models framework is a native Swift API that gives you direct access to the same on-device model that powers Apple intelligence.\n\nAnd many of you have already adopted it, creating experiences for shopping apps like Wayfair, educational apps like CellWalk, local sports apps like CricHeroes and more, all running on device with no infrastructure costs or privacy trade-offs.\n\nIt's amazing to see how you've pushed the limits of what an on-device model can do. We've got exciting updates for you. Let’s start with a preview of the intelligence-powered features you will be able to build today. Mary Beth, over to you.\n\nThis year, we're building a sample app all about the Japanese paper craft of origami. It's a place to unwind and get creative with paper. I’ll give you a quick tour. Our app starts with a beautiful gallery of my origami projects. And what’s special about these is that I’ve used foundation models to tailor origami projects to match a person’s interests and materials with step-by-step feedback. Of course, it's more fun to craft with friends. So our app has a built-in chat. It's a fun focused place to plan meetups and talk about crafting.\n\nNow I’m working on a cool feature to combine people’s interests into an origami project. Here, I’ll take this paper Rachel’s bringing and mix in a photo of my dog to generate a fun project for us all to fold together. With Foundation Models framework, my app analyzes the inspiration pictures to get a sense of the materials I have and this dog theme I have in mind. It even translates the Japanese text and uses all of this context to brainstorm a few options. I’ll choose this one.\n\nThe intelligence keeps going in a fully interactive tutorial.\n\nThat's a quick preview. Let's talk about the framework. This year, we’re taking the Foundation Models framework to the next level. First, you get new capabilities like multimodal prompts with text and images.\n\nThis opens up new categories of experiences you can build with image understanding. It's as simple as attaching an image to your prompt. In addition to this, the Vision framework is now integrated, giving you purpose-built tools the model can use, such as OCR for precise text extraction, and barcode readers for quick code scanning all on-device. Next, let's talk about server models.\n\nOn-device models are incredibly useful for many tasks. Yet sometimes you might want a larger model for a more complex workflow.\n\nThat’s why we’re extending the framework so you can easily call server models like Claude, Gemini, and more to use features like tool calling and guided generation.\n\nAnd any model provider can create a Swift package that conforms to the Language Model protocol. So you can pick the one you want for your app. In addition to this, we’re opening up access for those of you getting started with AI to use the Apple Foundation Model running on Private Cloud Compute with no cloud API cost, giving you access to frontier-level intelligence while ensuring your user’s data is not stored or accessible to Apple or anyone else.\n\nYour users will have access to features leveraging the cloud model every day, and iCloud+ subscribers will have expanded access.\n\nNo matter the model you want to use, you can easily swap it in. This makes the Foundation Models framework the best way to run any large language model in your app. With more modalities and more models at your fingertips, the next thing you’ll need is more ways to put them to work. That’s why we’re introducing a new open source Swift package, loaded with pre-built tools to help you get started with concepts like skills and utilities for context management.\n\nFor example, a task management app like Tiimo can use the package to pull in a skill that adapts its tone and recommendations to the user’s data, delivering a personalized brief to help them stay on top of their day. This space evolves so quickly. Tomorrow's abstractions may be very different from today's.\n\nSo these utilities from the open source package are created with new fundamental building blocks called Dynamic Profiles. These are new declarative APIs in the Foundation Models framework for building truly adaptive AI experiences with less code, so you can orchestrate skills and sub-agents, swap tools in and out, and update instructions on the fly.\n\nI'll walk you through how Dynamic Profiles powers intelligence in the Origami app.\n\nFirst, let's open Xcode. I'll start with a LanguageModelSession, which many of you already use. Now, instead of creating a session with a fixed model, tools, and instructions, with Dynamic Profiles, you have the freedom to continuously update your session. So I’ll choose a profile for the LanguageModelSession and start with the familiar Swift result builder syntax.\n\nHere in the body, I’ll define my first Profile as a brainstorming helper that’s going to generate project ideas based on the photos I give it.\n\nI’ll add modifiers to use Private Cloud Compute language model with temperature cranked up for creativity. Now the beauty of a Dynamic Profile is that this body will always resolve to just one Profile driving my session at a time, but I can switch between as many Profiles as my feature needs in the same session.\n\nSo I’ll change Profile based on my app state.\n\nThen I can add in a second Profile to handle tutorial generation. Using Private Cloud Compute again with reasoning level set to deep, since this is my most challenging task. Last, I'll add in a Profile that explains any origami jargon, like “valley fold”, that the user doesn't understand. This is a nice smaller task. I can send the on-device SystemLanguageModel to save on server calls.\n\nLet's see it in action. Here in my tutorial, I can now tap this term I don’t understand, and the on-device model generates a nice explanation.\n\nIn Dynamic Profiles, I’m swapping out models, but everything shares the same continuous transcript. This means more contextual intelligence with less prompting. Now let's use that in my Tutorial Profile. Instructions and tools can be swapped in and out as well. So I’ll use my app’s view model to check if the tutorial’s been generated, and if so, I’ll add in instructions and tools to help the model give high-quality feedback tailored to the user. This body recomputes on every model turn, so my session will stay up to date. Finally, do you see how my three Origami Profiles look a bit like three AI agents? That's because Dynamic Profiles are designed to be adaptable building blocks.\n\nSo if you want to build AI agents or skills or any other high-level abstraction, you can. With the focus of flexibility and composability, these new APIs from the Foundation Models framework are here to grow with you.\n\nTo help you bring all of this to your apps, you’ll have access to a complete set of tools, from building to testing, to shipping with confidence.\n\nThat includes the new Evaluations framework, which gives you the ability to test your prompts and validate that your intelligence-powered features work reliably. The upgraded Foundation Models instrument will help you visualize and debug model behavior in your apps. And the new FM command line tool lets you prompt the model right from the terminal.\n\nAnd there’s so much more, like a Python SDK, tool calling with images, and a new RAG tool powered by Core Spotlight that’s private to your app.\n\nThat’s the Foundation Models framework, providing you seamless access to multiple models with all new capabilities in the native Swift API.\n\nPlus, we're doing something big.\n\nLater this summer, the framework will be open source. So the same Swift APIs you use in your app can now run on your server too, giving you a complete end-to-end AI workflow anywhere you deploy Swift. You've seen how the Foundation Models framework connects to third-party models, Private Cloud Compute, and the on-device model. You have the flexibility you need to get the right model for the job. And when you want to bring a specific model into your app and run it on device, there's Core AI.\n\nCore AI is a brand new framework built right into the platform, along with supporting tools and technologies. It's designed to be the best way to bring and run models on device in your apps.\n\nIt delivers uncompromising performance through a modern memory-safe Swift API, with extensive tuning capabilities from fine-grained interest management and model specialization to custom GPU kernels.\n\nAnd there are Python-based tools alongside the framework, so you can convert and optimize your PyTorch models for the Core AI runtime. The framework is backed by deep integration into a new developer toolchain, with ahead-of-time compilation, dedicated Core AI instruments, and a powerful visual debugger to trace tensor values directly back to your original Python source code.\n\nAnd it’s engineered to scale with your available compute.\n\nSo you can run a compact vision model in your iPhone app for real-time camera queries. Or deploy a multi-billion parameter LLM in a Mac app right at your desk to power an agentic assistant for complex, multi-step workflows.\n\nWhatever the device, whatever the model, it all runs on-device with zero server dependencies and zero token costs. Core AI is optimized for performance on Apple silicon, and it empowers Apple Intelligence experiences across the system, including Siri. And this year, Apple Intelligence offers even more opportunities for developers. Over to Lori. Apple Intelligence draws on personal context from across apps, understands what’s on screen, and can take actions to get things done. And now you can integrate with its capabilities through the App Intents framework. It's how our platforms understand what your apps can do. With it, you can make your app’s content easier to find and its capabilities easier to use through the Action button, Shortcuts, widgets, and in Siri AI.\n\nApp Intents schemas make integration with Siri's capabilities easy. Schemas are recognizable structures that Siri understands deeply, built on years of language model training. We provide entity schemas for describing the content and concepts your app works with, and intent schemas for describing the actions it can perform. Entity schemas enable personal context understanding. By contributing your app’s content to the Spotlight semantic index, you can help your users find information from your app quickly and easily with attribution back to your app.\n\nThe indexing keys for important content properties are built right in, which means more understanding from less code. Siri's understanding of intent schemas means people can make requests naturally. They don’t need to learn specific phrases and you don’t have to define them in your code. Schemas cover common app categories like task management, photo editing, and communication, and include a whole set of system-supported actions. Just adopt the relevant intent schemas for the actions your app can perform to make them available to your users. And because these schemas are system-defined, they’ll benefit from future updates. That means as Siri’s language understanding evolves or as we add new support for languages or regional dialects, your intents will work there too, without any changes to your code. Combining these capabilities with the new View Annotations API will let your users reference and take action on the content in your app when it's on screen. So your users can interact with your app conversationally, saying what feels natural to them, not commands they have to memorize.\n\nNow, I'm going to show you how we've made our Origami app work with Siri. I already have some entities and intents that describe my app’s content and actions, and the entities conform to the IndexedEntity protocol, so they can be indexed into Spotlight. By also conforming to an entity schema, Siri will be able to discover and reason over my app's content. I’ll make sure my Message, Contact, and Conversation entities all conform to the relevant entity schemas by using the @AppEntity macro. I’m indexing these entities into Spotlight when my app finishes launching to make sure everything’s in sync. I’ve rebuilt to get the latest changes, and now let’s see what I can do, even when I’m not in the app.\n\nHey Siri, who's coming to origami night? Siri: Based on your messages in Origami, it looks like Kevin, Mary Beth, Rachel, and Richard are discussing origami night. What's Richard bringing? Siri: Richard mentioned he is thinking of bringing pizza. Awesome. But now I want to follow up, which means I need to act on this information.\n\nI can make the content Siri found actionable by conforming an intent to the sendMessage schema. This time I’m using the @AppIntent macro, since this is an action rather than content.\n\nI’ll build and run again, and now I can say, Siri, text Richard, “Can you make one of the pizzas vegetarian?” Siri: From Origami: Ready to send it? Yes.\n\nSiri: It’s sent. Great. Message sent. I’d also like to let people reference what’s on screen in my app just by saying “the second message” or “this photo”.\n\nThe new View Annotations API lets me associate my views with entities, which can then be passed to my app’s intents, making them actionable.\n\nMy Message List view contains all the individual messages in a conversation. I can use a new view modifier to map each message row to its respective MessageEntity.\n\nLet’s try it out. Hey Siri, send this photo to Kevin and say, “Rachel got us some paper to practice our folds. What color would you like?” Siri: From Origami: Ready to send it? Yes.\n\nSiri: It’s sent.\n\nAnd Siri sends the photo with my message.\n\nBy combining personal context, common app actions, and on-screen awareness, your app can become part of the intelligent fabric of the system.\n\nThrough Siri, users can access it through natural language, discover it through semantic search, and integrate it into their daily workflows. Now, back to Josh.\n\nThis is our vision for an intelligent platform. Rich, native experiences and intelligent natural language interfaces working together.\n\nAs app developers, this represents an incredible opportunity. You can enhance your app’s experiences through natural language with Siri, build powerful AI features with the Foundation Models framework, and even run your own models on-device with Core AI. If you’re using a custom model to power a feature within your app, Core AI is the right technology to use.\n\nYour models will perform efficiently across all devices, and it’s built into the platform, so your apps always benefit from the latest fixes and enhancements. And if you're an enthusiast who is experimenting with, training, researching, or fine-tuning generative models, or if you're running a local inference server, our array framework, MLX, makes it easy to explore cutting-edge innovations and technologies.\n\nIt now supports Metal 4, GPU Neural Accelerators, and it can even scale training across multiple Macs with RDMA over Thunderbolt. It's all open source and it's faster than ever.\n\nThe breadth of capabilities offered by Apple Intelligence and powered by Apple silicon makes Apple’s platforms the best place to build and deliver the next generation of intelligence-enhanced apps and games.\n\nNow let’s take a look a level deeper at what makes all of this possible - the systems your apps depend on, from the frameworks you call to the processes that schedule your work, manage your memory, and render your UI.\n\nWe took an especially close look at the performance and quality of these foundations, and you’ll see a multitude of platform improvements in this year’s releases. When you rebuild with the new SDK, your apps will launch faster and feel more responsive. You'll see refinements and platform improvements across frameworks media, search, and accessibility, enhancements to Swift and SwiftUI, and a lot more, especially around design.\n\nLast year, the new design with Liquid Glass brought a unified design language built for a world where your experience moves across devices.\n\nThe new design is making apps more expressive and delightful while staying instantly familiar to users. It looks great in apps like Tide Guide, where subtle, interactive highlights respond as users scroll through tide data and charts.\n\nAnd SketchPro, where translucent brush panels and controls let artwork show through, even while you switch between tools. Throughout the last year, we’ve been refining the design, and that journey continues with a new set of design updates in the 27 releases.\n\nApps that have already adopted Liquid Glass benefit from many of these improvements automatically. To tell you more, here's Cindy.\n\nIn this year’s releases, you’ll see updates to the foundations of how Liquid Glass is built, refinements to the new design that improve consistency, and new ways for iOS apps to adapt across devices and screen sizes. Let's review the changes you'll see in your apps.\n\nTo maintain exceptional readability, we tuned Liquid Glass so it more effectively diffuses complex content behind it.\n\nAnd to establish more depth and separation, we also introduced a darkened edge along with brighter specular highlights. We also made it more personalizable with a new slider in settings to adjust Liquid Glass anywhere from ultra clear to fully tinted, allowing users to choose the look that works best for them.\n\nApps already using Liquid Glass get these improvements automatically when they run on this year’s releases without even needing to recompile. Liquid Glass seamlessly adapts to a variety of accessibility settings users may choose, such as reducing transparency or increasing contrast.\n\nAnd now macOS 27 also supports the “show borders” environment value, just like iOS. So you can adapt your macOS app's custom controls for this setting as well. Sidebars expand to the edges on Mac and iPad, providing clearer structure while still refracting content from your app and the wallpaper. And icons in the sidebar regain their color using your app’s accent color, giving your app more personality and making it more clear which window is key. List and Label APIs provide these updates automatically and support customizing the tint per item. And every window on macOS now also has the same tighter corner radius, ensuring greater consistency across all apps. When content scrolls under floating bars, a uniform toolbar appears across the top and keeps the text legible while improving contrast.\n\nThis effect is applied automatically for standard toolbars and can be customized using the existing scroll edge effect APIs.\n\nWe also thought about how icons and menus can be used intentionally to call attention to the most important actions, both on macOS and iPadOS.\n\nWhile icons are hidden by default, there’s an API to show icons for key app actions. We’re also updating how Liquid Glass shows up in icons, making them sharper and more defined. This updated rendering applies to all app icons, and we’ve introduced new features such as refraction that can be selectively used for added character. And with Icon Composer, you can now design your icons out of multiple layers of Liquid Glass. It’s been updated with new annotation features to add refraction or dial in Liquid Glass content effects. And it provides an interactive preview of how your icon will look on earlier releases. Together, these updates culminate in a more focused and approachable experience across your apps and across platforms.\n\nNext, let’s talk about app adaptability. iOS apps show up in more places than ever, on iPad as an iPhone app, or on Mac through iPhone Mirroring. When your iOS app shows up in these other contexts with larger displays, users want to be able to take advantage of the extra space to see more information.\n\nSo this year we’re introducing support to resize iOS apps in iPhone Mirroring and on iPad. Let’s see how this works with the Origami app. Once you rebuild with the latest SDK, your app is automatically opted in to resizability. Since Origami is a SwiftUI app, it’s already taking advantage of scene lifecycle and standard framework support for basic resizability.\n\nIf you’re already using SwiftUI, Auto Layout, or responding to size class changes, you are well on your way to supporting full resizability. If you have custom views, you’ll want to update them to using auto layout and trait collections for layout decisions.\n\nUsing the new resizable iOS simulator and Previews, you can test across a variety of screen sizes right in Xcode, so you’ll see exactly how your layout performs.\n\nAnd we’re providing a skill for coding agents that will help you find and fix common resizability issues. Now, instead of designing for specific devices and orientations, you’re designing for a dynamic range of sizes and aspect ratios.\n\nTo provide the best experience when using iPhone Mirroring, update your app to be able to adapt and support any size. Resizable simulator, Previews, and iPhone Mirroring all make it easy to ensure your app is as dynamic and flexible as possible.\n\nNext, let’s talk about SwiftUI. Here's Franck.\n\nSwiftUI is the best way to build apps for any Apple device. We designed SwiftUI to capture everything we know about building great apps on our platforms. It gracefully handles the complexities of layout, animation, and platform integration so you can focus on what makes your app yours. And as new capabilities like Liquid Glass are added, apps get these features easily because they’re designed with SwiftUI in mind.\n\nNew apps like Xogot are built with SwiftUI because they want to feel truly at home on Apple platforms. Xogot is a game development environment that brings the open source Godot engine to Apple devices.\n\nIt started on iPad, expanded to iPhone, and when the time came to bring it to Mac, it felt completely natural. And apps that previously used cross-platform or web technologies like Notion are migrating their user interface to SwiftUI because they want a level of performance and UI consistency that other technologies can’t deliver. With powerful agentic coding tools, porting code to Swift has never been easier. Of course, we reach for SwiftUI ourselves whenever we build apps.\n\nFor example, SwiftUI made it easy to build a new Siri app by enabling us to share code across all our platforms and Creator Studio apps like Logic Pro, build new features with SwiftUI for high performance and cross-platform support. Since we rely on SwiftUI ourselves, every improvement we make for our own apps becomes an improvement for your apps too. And this is a big year for SwiftUI. With richer interactions that help you write less custom code, with speed making your apps much faster, and finally, with new capabilities for your apps. Let’s start with interactions.\n\nThis year, SwiftUI brings more dynamic interactions to your app like reorderable containers, which makes it super easy to add drag to reorder to any container. Building a grid reordering experience outside of lists, like with this grid in the Origami app, used to require a lot of code.\n\nNow, it is just as simple as adding .reorderable() to your ForEach and .reorderContainer() to the parent. And just like that, I can customize the order of my Origami models.\n\nSwiftUI handles the lift and the drop animations and it works with any container like grids and stacks. Now, for Origami models I feel a little less proud of, SwiftUI now supports swipe actions inside any container as well. I can delete a custom row with a swipe by adding the existing️ .swipeActions() modifier to my row and .swipeActionsContainer() to the scrollable container.\n\nThis provides great flexibility for quick actions on my custom row.\n\nFinally, text selection got more flexible too.\n\nOn iOS, it gains the same. full-fidelity selection already found in TextField and TextEditor.\n\nAnd on macOS, it now supports custom text renderers, text vibrancy, and vertical text. Next, let’s take a look at speed.\n\nThis is always a priority for us, but even more so this year, and you will see many improvements without any changes on your end.\n\nTo start, we’ve been gradually unifying the architectures of SwiftUI, AppKit, and UIKit, and this year, they share a common foundation across many controls. So wherever your app is running, they can benefit from the same on-the-line improvements.\n\nFor example, menu pickers on macOS are now better equipped to smoothly handle large lists of items. And in nested stack layouts, whereas SwiftUI used to measure each child multiple times to resolve their flexibility, it now short-circuits computations where they’re not needed, meaning layouts now resize up to twice as fast. And nothing saves performance like avoiding unnecessary work. SwiftUI now only initializes state objects when they're first loaded.\n\nPreviously, a new temporary instance of the state object would get created every time the view is reinitialized.\n\nYou get this improvement for free because state is now lazy under the hood and was converted from a dynamic property to a macro.\n\nAnd when it comes to loading images, AsyncImage avoids redundancies as well. It now caches its content automatically using standard HTTP caching, so images are downloaded once and only re-fetched when needed.\n\nFinally, let's talk about new capabilities starting with toolbars.\n\nWith the new resizability features, optimizing your app for a dynamic range of sizes and aspect ratios is more important than ever.\n\nAnd toolbars are central to that experience.\n\nThis year, SwiftUI gives you finer control over how toolbar items adapt to space.\n\nUse the new visibilityPriority modifier to mark your most important items high. And SwiftUI keeps them visible longer as space shrinks. Less prominent actions, like archive or delete, can be added to the new toolbars overflow menu container, which groups them in an overflow menu. And finally, the new topBarPinnedTrailing placement anchors items to the trailing edge, no matter how the toolbar reflows. Now, when I resize the window, the toolbar stays organized exactly how I want. Important buttons stay visible. Deprioritized ones are in the overflow menu and share is always pinned to the trailing edge. Tabs can also be distinguished with the new prominent tab role pinning the tab to the trailing edge of the screen.\n\nSwiftUI also opens up new ground for document-based apps with a new document infrastructure that provides a ton of functionality out of the box, like first-class URL access for fully customizable reading and writing to disk, the kind that powers apps like Xcode or Pages. For example, with direct access to the file URL, you now have the flexibility to read just the parts of a file you need and write only the pieces that changed, not the entire file. You can also observe and update document attributes using the provided observable configuration. The new document API integrates deeply with modern Swift, with support for observation, Swift concurrency, and so much more.\n\nLastly, here is something pretty awesome. The Spatial Preview framework gives Mac apps new ways to extend in space around users wearing Apple Vision Pro.\n\nWhen you adopt this new API in your app, a 3D model can become spatial when you stream to Apple Vision Pro, allowing your users to preview, edit, and share objects and models in real time. Beyond those we've mentioned already, there are many other new improvements, including better type checking performance with content builders, a new alert binding API, and to support adjusting cross-fade transitions.\n\nTogether, these platform improvements bring more speed, richer interactions, and powerful new capabilities to SwiftUI that you can take advantage of.\n\nNow, let's take a look at Swift itself. Here's Holly.\n\nSwift is designed to be the language you reach for at every layer of the stack.\n\nWhether you’re building full-featured mobile apps, internet-scale services, or embedded firmware, Swift helps you write code that's fast, expressive, and safe. Swift's performance, depth, and unmatched interoperability make it the natural successor to C and C++ for low-level systems and server programming. And its approachability and expressiveness make it ideal for higher-level development like apps and frameworks. We think Swift is the only language with this breadth. That's what makes Swift a language you can keep reaching for as your stack grows.\n\nIncluding outside Apple platforms, where the tools for development on Linux, Windows, Android, and the web are available on Swift.org.\n\nMany of you are extending your use of Swift to additional platforms and the server so you can reuse code and benefit from Swift’s high performance across your entire stack, like Flighty, which uses Swift in their services to share the code that tracks airport visits between the app and backend. Or GoodNotes, which uses Swift for WebAssembly to bring the app to the web, Chrome OS, Android, and Windows, reusing over 100,000 lines of code.\n\nOr Frameo, which uses Swift-Java interoperability to share Swift libraries between the iOS app and the PhotoFrame software written in Java.\n\nInteroperability means you can bring Swift into C, C++, and Java systems you already have. So you can get Swift's benefits at every layer without a rewrite. At Apple, we've been building with Swift at every layer of our own stack. Foundation paved the way for Objective-C frameworks to move to native Swift under the hood. AppKit and UIKit have followed suit by using Swift and SwiftUI extensively in their implementation. WebKit, the open source web engine that powers Safari, is a large and security-critical C++ code base.\n\nUsing Swift’s safe C++ interoperability, WebKit is replacing core components with Swift versions incrementally. In the networking stack, the QUIC transport layer was rewritten in Swift. Later this month, the project will be open sourced and available for cross-platform use hrough SwiftNIO integration. You can follow along or get involved through the vibrant open source community on Swift.org.\n\nFurther down the stack, more security and performance critical systems moved to Swift this year. The TrueType font rendering engine replaced decades of hand-optimized C with Swift code that’s not only memory safe, but also faster. At the lowest level, we’ve written hundreds of thousands of lines of Swift code across bare metal firmware, coprocessors, and drivers.\n\nFor the 27 releases, we've started writing parts of the core operating system kernel in Swift.\n\nAs Swift becomes more capable in these domains, we’re staying true to one of Swift’s most important design goals: it’s fun.\n\nIt's natural to write and iterate on your ideas in Swift, and the compiler is there to catch mistakes along the way. The latest updates are focused on improving your workflow so you can focus on the fun part: writing great code with confidence. Swift 6.4 is here, built to make everyday tasks feel effortless. I'll show you just a few examples. When your code base is undergoing a migration or incremental adoption of new features, sometimes it’s not realistic to address all compiler warnings across your project at once. You can now suppress warnings in specific parts of your code, and you can promote warnings to errors in places where you want strict enforcement. Availability attributes can get long and repetitive when you’re writing code for multiple Apple platforms. Now, instead of listing out every Apple platform with the same version number, you can simply write ‘anyAppleOS’. The limitation on async calls in a defer block is gone, and awaiting inside a defer just works.\n\nNo matter what you’re building, Swift’s compiler diagnostics are a daily companion, helping you catch mistakes early and guiding you toward correct code. If you’ve spent time writing Swift code, you’ve probably encountered this error message: “The compiler is unable to type check this expression in reasonable time.” This can happen in complex operator expressions, closures, or in deeply nested SwiftUI view bodies.\n\nThis is frustrating, and we’ve made it a lot better.\n\nIn many common cases, code that hit this fallback error will now either compile successfully or give you a more actionable error to work with.\n\nWe know this area is important for a smooth workflow, and we’re continuing to invest in it.\n\nSwift 6.4 makes you more productive in day-to-day code, and it brings that same care to the more specialized corners of your project. There's never been a better time to go full stack with Swift. Now back to Josh.\n\nSo those are the platform improvements in this year’s releases. Now, to move ahead, sometimes we have to leave something behind.\n\nAs we said last year, macOS Tahoe was the final release to support Intel Macs.\n\nThe transition of macOS to Apple silicon is now complete, enabling us to focus on a single architecture across the entire ecosystem. This can benefit your apps as well. You can now ship Apple silicon-only binaries on the Mac App Store, reducing your app’s download size and letting you focus your testing on a single architecture.\n\nAnd with all the refinements to the new design with Liquid Glass, it’s time to complete your migration there too. We'll be removing support for opting to use the old design. So once your app is recompiled with Xcode 27, it will automatically begin to use the new design with Liquid Glass.\n\nWith so many improvements across the system, your apps and games will look and feel better than ever on this year’s releases. Now, let’s turn to your productivity and the tools you use to build with and for our platforms. Intelligence is deeply transforming how you write code, add new features, and build apps. Last year, we brought AI coding assistance to Xcode, and so many of you embraced it immediately. It's helping you write code faster and adopt new APIs more easily. This space moves really fast, so we've picked up the pace of our releases, delivering new Xcode capabilities to you faster than ever before. Earlier this year, we brought coding agents to Xcode, along with tools allowing agents to grab a preview, search documentation, and more, powered by the Model Context Protocol.\n\nWith MCP, Xcode also connects to the tools you already use, from design apps like Figma to services like GitHub.\n\nAnd Xcode includes a built-in integration for agents from Anthropic, OpenAI, and now Google. Today, Xcode adds support for Agent Client Protocol, so you can bring any compatible agent into Xcode. ACP support and Gemini integration are shipping in an update to Xcode 26 available today. And there’s more coming in Xcode 27.\n\nHere's Ken.\n\nFrom the first line of code to the App Store, Xcode is where you build the best apps for Apple platforms.\n\nMillions of you live in it for writing and debugging your code with coding agents right alongside, designing interfaces in SwiftUI and previewing them in real time, testing across devices and simulators to catch issues before your users do, and profiling performance with instruments, keeping your apps fast, responsive, and efficient.\n\nThis year, Xcode has two big stories. The first is intelligence, and we have a lot to talk about in a minute. The second is the daily experience, how Xcode feels to use.\n\nNow just like you, we spend hours in Xcode every day. We build all our operating systems and apps with it. In fact, we build Xcode with Xcode. So it needs to feel like home while being fast, fun, and personal.\n\nAnd we've heard your feedback and improved Xcode across the board. It's faster at loading projects. We fixed top crashes and spins.\n\nDebug sessions are more reliable, with faster expression evaluation, and a console that can handle more intensive logging without hitching.\n\nXcode 27 is 30% smaller. Now, Apple silicon-only, with agents, documentation, and other components downloading in the background, so you're always up to date. Now, let's take a look at the experience. First, your Xcode settings are now automatically saved to iCloud. When I'm setting up a new Mac like this, Xcode offers to import them.\n\nI'll pull in the settings from my iMac. I can sign in with my Apple ID. Xcode fills in my Git config too. And just like that, I'm ready to code with my new Mac. Now, let’s create a new project. Watch this. I'll select new project, then app and boom, I'm in the editor. No file name, no bundle ID, no setup.\n\nOf course I can specify all those things later when I’m ready. This is great for exploring an idea, a new API, or prototyping a view.\n\nAlright, now I'll open the Origami project.\n\nXcode 27 looks beautiful with the design refinements of macOS 27.\n\nCrisp and clean. Let’s customize it. In Xcode 27, you can make the toolbar your own. It's easy to rearrange things, so I can add what I need and remove what I don't. The activity view is now tucked neatly into the document title over here, so there’s even more room for the things I want. The navigation buttons, canvas toggle, editor splits, they’re all right up here on the toolbar.\n\nNow I’ll add a shortcut to quickly create a new coding assistant conversation and that’ll be useful a little bit later.\n\nNext and super fun, themes. Color now flows throughout the entire app, not just the editor. You can personalize everything from the background to syntax colors and dial in that perfect shade of purple for your keywords. And Xcode 27 comes with gorgeous new choices. Let me show you a few of my favorites. Emerald. That feels fresh. You can almost smell it. How about something with a little bit more energy? Neon Noir. Electric. Love it.\n\nLight or dark? Every theme supports both. Here’s Coral Reef. I feel relaxed already. And when I’m working on multiple projects at the same time, I can set a different theme for each. Makes it super easy to tell them apart at a glance.\n\nAll right, let's get back to work. Next, Xcode Cloud, which gives you continuous integration and delivery built right into Xcode.\n\nI’ll set up my Origami project to use it. I'll click get started, grant access to my repository, and that's it. I can kick off my first cloud build. No App Store Connect setup needed.\n\nAnd Xcode Cloud builds are up to twice as fast, now supporting Apple Vision Pro and apps using Metal on Apple silicon.\n\nNext, Previews. They are the best way to iterate on UI and the easiest way to see how your views look across variants, like accessibility sizes, orientations, and localizations. And now you can see variations for any property.\n\nI’ll open this view here that shows a craft note. It renders differently based on the CraftState enum, which has four different values. Now, I can pass that enum to the preview and just like that - I get a grid showing all the states of my UI. All four in one glance.\n\nNext, another one I'm excited about. When testing your app, you use real hardware to evaluate performance, use sensors, and test real-world conditions. And you use simulators to cover older OSes and devices that you don't have.\n\nXcode 27 brings both together in the new Device Hub.\n\nIt replaces Simulator and it does a whole lot more too. Let me show you.\n\nWhen I first run my app, the window looks like the Simulator I know. I can easily rotate, grab a screenshot and jump back to the home screen, just like I’m used to.\n\nWhen I extend the view, I can now change device properties and test how my app responds to different system settings. Like switching to dark mode, increasing the font size, and more.\n\nWe rebuilt the experience from the ground up for the highest fidelity possible.\n\nSo I can pinch to zoom, use two-finger scrolling, and like in Previews, I can dynamically resize the simulator to see how my iOS app handles different sizes. I can also manage and interact with physical devices from the same place, like this iPhone here on my desk. I'll launch the Origami app right here from my Mac. And I can interact with it. All the convenience of the simulator with the fidelity of real hardware in a single place.\n\nThat is a quick look at the experience in Xcode 27.\n\nAnd beyond the experience, the biggest changes this year, the ones that will truly accelerate you, are in intelligence.\n\nKevin, over to you.\n\nWhat a great time to be a developer. Intelligence is transforming how you build apps. Agentic coding, together with Apple platforms, frameworks, and tools, helps you bring ideas to life. Xcode 27 takes the next big step in agentic coding, leveraging the full power of the best models and agents directly into Xcode.\n\nAgents are woven into every layer of the Xcode experience, from the way you interact with them to a set of tools that help you get the best results. Tools like understanding your project, searching documentation, building, and testing. And Xcode 27 helps you even more with new tools like rendering previews with variants, interacting with the simulator, localizing your app, debugging, and more. And this goes beyond tools. When using agents in Xcode, every answer is grounded in Swift, SwiftUI, in Apple frameworks. That’s why, when building for Apple platforms, Xcode is the best place to code with agents. Let me show you what this looks like across every stage of app development.\n\nFrom starting with an idea to implementing and validating it, to improving it, like adding new languages or fixing issues. First, I’m gonna add something fun to the Origami app. My daughter, she loves making origami with me. I want to surprise her with a feature that makes up a little choose-your-own-adventure story about the characters that we make together. I’ll start with a new conversation with the agent. I'll create one here from the toolbar. It opens right in the editor, just like any other file. Here’s what I want to build. In my Origami project, I want a button that generates a choose-your-own-adventure story for my daughter. That alone would get me good results, but I had something a little more specific in mind, so I’ll add some more details. She’ll start by picking some options, like the setting and an item to use in the story. The app will generate the first page, and then let her pick what comes next. I want to use the latest Foundation Models APIs and present it with beautiful typography. When using a coding agent, the best results come from collaborating on the implementation and design first, before any code is written. So I'll add /plan to the prompt. And I’m gonna ask for a diagram while I’m at it. I find it easier to review the plan that way. Let’s get this started. The agent is exploring my project using Xcode tools to help it efficiently understand my code base, its architecture and patterns, to find the best way to build the feature, and it’s asking some clarifying questions. Do I want to persist it? Yes. And how many options for the next part of the story? Two to three is good. And the agent continues to create the plan. Let’s skip forward in time. My plan is now ready, and it shows right next to the conversation in beautifully rendered markdown. It’s very easy to review, and I can also refine it. Here, the current implementation has a fixed set of settings and items to choose. I'd love if she could add her own. I’ll add that as a comment. And now the plan looks good. Let's kick it off. Now Xcode and the agent work together to implement the plan. Xcode shows everything that's changing, like code and previews. As it runs, I can refine the implementation. Like here, I'm gonna add a fun image filter to the story's hero image.\n\nIt looks like Xcode is done building my feature. There's still a lot of code, and the previews look great. Let's just run it. I’ll open an Origami project, tap the new toolbar icon, and just like we asked, page one offers an item and a setting. I’ll pick a forest and a wand, and oh look, there’s the button if I wanted to add my own. And here's the first page of the story with that gorgeous hero image. My daughter's gonna love this. What was just an idea a couple minutes ago is now something I can run in my app. This is amazing! Now, building a feature is more than writing code. It's also making sure it does what it's supposed to do. Xcode 27 can help you with that too, with new tools for agents to check their work.\n\nFor example, agents can validate the logic of your app by running tests, try ideas in isolation using playgrounds, like experimenting with APIs, and check visual changes with Previews in light and dark mode, different orientations, text sizes, or localizations.\n\nAnd now agents can interact with your app in the simulator. Let me show you. I want to test different combinations of settings, items, even customized ones. Xcode launches Origami and Device Hub and starts testing those for me.\n\nThe agent can tap, swipe, and type. When it's done, I get a summary of the tests, and I can see all the screenshots it created along the way. And just like that, the Origami app has a new story feature, designed, built, and tested end-to-end. Next, let's see how we can use agentic coding to improve our app. Agents in Xcode can help with all kinds of engineering tasks, like adopting new APIs, making your app more accessible, and more. Let's localize our Origami app. I’ll start with French. Xcode automatically adds a new language to the strings catalog, then works with the agents to translate strings across the entire project. This is more than a word-for-word translation. Xcode looks at each string in its context, the surrounding code, UI, the action, to find the best translation. And when it's done, I can see all the translations here in the String Catalog. Let's build and run. My app is now localized! Fantastique! Building a great app also means responding to the feedback and data from your users. The Organizer already gives you insights into how your app is doing in the real world - crashes, hangs, performance metrics, anonymized and aggregated. Now, you can use agents to help you find issues that matter most, and fix them. I'll ask Xcode to pull up the top crashes from the latest release. I get a list of crashes ranked by how often they happen. Oh, that first one is from an update I pushed last week. Let's fix it.\n\nXcode looks at the symbolicated crash log, figures out where in my project this happens, identifies the issue, reproduces the crash, makes the fix, and then validates it.\n\nAnd just like that, our issue is fixed. That's agents at work with Xcode across every stage of your development, planning, building, and improving. You'll be amazed when you see Xcode and agents bring your ideas to life.\n\nFinally, let's talk about what makes all of this possible. Xcode 27 ships with the expertise of Apple’s engineers and designers built right in as a corpus of skills, documentation, and MCP tools.\n\nThink of them as specialists. A SwiftUI specialist that knows how to structure your view and data flow. An accessibility specialist that knows what makes an interface work for everyone. Specialists for universal sizing, testing, and performance. In fact, so much of what we’ve seen to this point is powered by one of these specialists. And you can bring your own! Xcode integrates all of them in the same way. Plugins. It's a format used by many agents and widely adopted by the community. It's amazing to see how many of these you have already been building and sharing. A plugin can contain skills, just markdown files that teach the agent new tasks. It can contain tools using the Model Context Protocol. And we’ve added one new capability to plugins. With the Agent Client Protocol, a plugin can bring an agent of your choice.\n\nInstalling one is easy. You can use the command line or paste a git URL right into Xcode.\n\nAnd partners like Figma and GitHub make it even easier to set up with just one click.\n\nAnd then I can put it all together. I can tell Xcode to implement a Figma design in SwiftUI, refine it for different variants, make it resizable using a skill, and post a PR to GitHub. How cool is that? That's Agentic Coding in Xcode 27. From an idea to an app, at every step. Built for how you create, refine, and ship, and reach your users wherever they are. We can't wait to see what great things you make next! Back to you, Josh.\n\nIt's a huge year for Xcode and for many of our other developer tools as well. Like the all-new Reality Composer Pro 3, which has been completely rebuilt for crafting production-ready 3D experiences using RealityKit. It brings support for character animations, more realistic lighting, and live previews that let you see the results of your edits as you make them using Mac Virtual Display. And there’s even more for game developers in this year’s releases, including a major update to Game Porting Toolkit, which dramatically cuts the time it takes to bring games to Apple platforms by adding AI skills for coding agents. And new Metal command line tools give agents direct control during development and debugging, bringing best practices for game development on Apple platforms to every step of your porting journey.\n\nSo that's developer productivity. Across the 2027 releases, there are so many new capabilities to build on, like the App Intents framework, which lets you connect your app to Apple Intelligence, and the Foundation Models framework and Core AI, which enable you to bring powerful generative intelligence features directly into your apps. There are platform improvements across design, Swift, and SwiftUI that make your apps faster, more flexible, and easier to build. And Xcode has even more expansive support for agentic coding. and we’ve only just scratched the surface. There are over 100 sessions to dive deep into everything we've covered today, including Apple Intelligence, Xcode 27, Design, and more.\n\nAll these sessions are available on the Apple Developer app, the website, YouTube, and new this year on Bilibili. And there's so much more happening online throughout the week. Sign up for Group Labs, online panels, and Q & A sessions with Apple engineers and designers. And connect with us on the Apple Developer Forums to ask questions, and follow the conversation about the latest tools and technologies. And the opportunities to connect extend well beyond this week.\n\nYou can Meet with Apple around the world and online in hands-on workshops, labs, and events to learn and connect as a community throughout the year.\n\nWe love when we can meet you in person. And there are many opportunities to do that in our Developer Centers, located in Cupertino, Shanghai, Singapore, and Bengaluru. And we’re excited to announce the opening of our fifth this fall in Berlin, home to one of Europe’s most vibrant developer and designer communities. We can’t wait to see you there or in one of our events online. Whether this is your first WWDC or your 25th, thank you. Your work inspires and drives us. We use the apps and play the games that you all build. So, our greatest hope is that everything we talked about today will enable your next great idea to come to life. We can't wait to see what you do next. Enjoy WWDC.",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/112/1/0e1d49e8-277b-49f9-aaff-d937c5956d86/downloads/wwdc2026-112_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/112/1/0e1d49e8-277b-49f9-aaff-d937c5956d86/downloads/wwdc2026-112_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:11.025Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-121.json b/data/wwdc/videos/2026-121.json
new file mode 100644
index 0000000..64461ab
--- /dev/null
+++ b/data/wwdc/videos/2026-121.json
@@ -0,0 +1,24 @@
+{
+ "id": "121",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/121/",
+ "title": "Announcing Apple’s next big step for Siri and iPhone",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Cinematic voiceover: Coming to a pocket near you! ♪ Dramatic classical music ♪ Siri: Hello.\n\nCinematic voiceover: The Siri AI glow-up hits the small screen.\n\nSee Siri AI in the role of a lifetime: your personal assistant.\n\nAsk Siri AI anything and revisit your conversations with ease in a new dedicated app.\n\n♪ Watch Siri AI embark on a quest to provide nutritional details about your meal.\n\nYummy.\n\nCheer as Siri AI handles every detail of your soccer watch party.\n\nGoal! \"Siri is giving main character energy\" and...\n\n...\"puts the personal in personal assistant.\" See next-generation Apple Intelligence extend, reframe, and clean up your photos.\n\nBoo-yah! Create photorealistic images and flex your imagination everywhere from Contact Posters to fun wallpapers in the all-new Image Playground.\n\nSafari has never been more organized with topics or timely with Notify Me.\n\nRest easy knowing your data is protected as compromised passwords are automatically updated with just a tap.\n\nAnd see Apple Intelligence stand guard to protect your privacy.\n\nA faithful sentinel, keeping your private info private.\n\nWith Siri and Apple Intelligence, your data stays protected.\n\nAll this and so much more.\n\nComing to an iPhone near you.\n\n♪ Sweet.\n\n♪",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/121/1/f1e6baa3-3c16-4944-abec-3525818a2702/downloads/wwdc2026-121_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/121/1/f1e6baa3-3c16-4944-abec-3525818a2702/downloads/wwdc2026-121_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:11.273Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-122.json b/data/wwdc/videos/2026-122.json
new file mode 100644
index 0000000..56a0b56
--- /dev/null
+++ b/data/wwdc/videos/2026-122.json
@@ -0,0 +1,24 @@
+{
+ "id": "122",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/122/",
+ "title": "WWDC26 Platforms State of the Union Recap",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Essentials",
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Here's your WWDC26 Platform State of the Union recap.\n\nIntelligence, platform improvements, and tools all in one go.\n\nApple Intelligence has been rebuilt from the ground up. Working together with Google and leveraging the technologies behind their Gemini family of models, we created the latest Apple Foundation Models for our integrated Apple Intelligence experiences.\n\nThe Foundation Models framework expands to include image input and support for cloud models.\n\nSo if you have a more complex task requiring more advanced frontier models, the API can now integrate with the cloud model provider of your choice. And we included dynamic profiles to help you build AI agents and skills with a lot less code by swapping tools in and out and updating instructions on the fly. Core AI is an entirely new framework, designed to be the best way to run on-device models in your apps. It's built right into the OS and takes full advantage of the power of Apple Silicon.\n\nAnd App Intents connect your app to Apple Intelligence, making your users' content discoverable and your app's actions available through natural language in Siri.\n\nAnd the new View Annotations API even lets people act on what's on screen just by asking. Together, in-app and system-wide intelligence unlock experiences neither could deliver alone.\n\nYour apps made more powerful by intelligence and intelligence made more meaningful by your apps.\n\nThe new design with liquid glass is more consistent, more personalizable, and better tuned to maintain exceptional readability.\n\nEvery window on macOS shares the same tighter corner radius. App icons get sharper rendering automatically, and you can add new refraction effects in Icon Composer. iOS apps are now resizable, so users can take advantage of their larger displays when running one on iPads or Macs using iPhone mirroring.\n\nAnd the new resizable iOS simulator and previews make it easy to test across sizes.\n\nSwiftUI brings more speed, richer interactions, and new capabilities.\n\nDrag to reorder and swipe actions now work in any container. Nested layouts resize up to twice as fast, and async image caches automatically.\n\nToolbars get finer control over what stays visible as space shrinks.\n\nAnd on Apple Vision Pro, the new Spatial Preview Framework streams 3D models from your Mac into the space around you.\n\nAnd Xcode has two big stories this year. The Daily Experience and Agentic Coding. Projects load faster. Xcode 27 is 30% smaller because it's Apple Silicon only. Your settings sync via iCloud. The toolbar is fully customizable, and color now flows through the entire app via themes. Emerald. Neon Noir. Coral Reef.\n\nXcode Cloud is easier than ever to set up, with builds up to twice as fast and support for Apple Vision Pro and apps using Metal on Apple Silicon.\n\nAnd the all-new Device Hub replaces Simulator, bringing virtual and physical devices together in a single place. Pinch to Zoom! Live resizing, and full control over real hardware right from your Mac.\n\nLast year, we brought AI coding assistance to Xcode, and so many of you embraced it immediately. We're working with the leading model providers to bring their agents into Xcode, Anthropic, OpenAI, and now Google.\n\nConversations with agents now behave like any other file. Open them, split them, stack them together in the Navigator. When you plan, the agent lays out its approach before a single line of code gets written, so you stay the architect. Agents can now run your tests. Try things in playgrounds and customize previews to validate UI across light and dark mode, orientations, text sizes, and localizations. They can even drive your running app. Tap, scroll, swipe and type to test it end-to-end.\n\nXcode 27 ships with the expertise of Apple's engineers and designers built right in, specialists for Swift UI, accessibility, sizing, testing, and performance that agents can draw on.\n\nYou can extend Xcode too.\n\nPlugins bring skills, MCP tools, and any agent through the agent-client protocol. Figma and GitHub ship their own with just a click to set up. Xcode is more open, more capable, and a whole lot smarter.\n\nIntelligence rebuilt. Platforms refined. Tools transformed.\n\nThat's the highlights from the WWDC26 Platform State of the Union. You can watch the full video on the Apple Developer app, website, YouTube, or BiliBili. We can't wait to see what you build.",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/122/3/acc0a465-b6fa-446b-8f3f-dc122d862f47/downloads/wwdc2026-122_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/122/3/acc0a465-b6fa-446b-8f3f-dc122d862f47/downloads/wwdc2026-122_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:10.686Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-201.json b/data/wwdc/videos/2026-201.json
new file mode 100644
index 0000000..3086aa4
--- /dev/null
+++ b/data/wwdc/videos/2026-201.json
@@ -0,0 +1,59 @@
+{
+ "id": "201",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/201/",
+ "title": "Secure your apps with App Attest",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Business & Education",
+ "Privacy & Security"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hello, my name is Manthan, and I am an engineer on the Trust and Safety team. Today, I will talk about how App Attest can help protect your apps and your users. You built and distributed your app to function in a secure environment on Apple's platforms. Fraudsters are always looking for ways to exploit your apps beyond their intended feature functionalities. These may include attack scenarios where compromised copies of your app, serve valid looking requests to your server, gaining access to sensitive data. Imagine, you've built a quiz proctoring app for students to take quizzes and submit answers. A fraudster may reverse engineer your app to create a modified copy that submits falsified quiz responses to your server. App Attest can help your server reject these types of requests from modified clients.\n\nOr, modify your app to include content that you didn't ship.\n\nBy directly modifying compromised copies of your source code or related resource bundles, re-signing your app, and running a modified copy on the device.\n\nImagine, you have a dragon slayer game, and a fraudster has injected a cheat menu that boosts their abilities in your game. This allows them to submit fraudulent scores to your server and rise up the leaderboard with a compromised copy of your game app.\n\nApp Attest is designed to address these types of threat scenarios. I will start with what App Attest protects against, then walk through integration steps and best practices. I'll flag some common pitfalls, and I'll wrap up with the fraud metric, a powerful tool for detecting suspicious attestation activity. Starting with how App Attest can help protect your app.\n\nApp Attest can help ensure that your app is running on genuine Apple hardware. It does this by issuing an attestation, which provides cryptographic proof about the validity of your app running on the user's device. This can then be verified by your server and it gives you the assurance that your app is running on a secure Apple device.\n\nNext, it helps make you aware of modifications to your App on the user's device. App Attest surfaces information about the relying party, launch category, and bundle version associated with your app, all of which can help you determine if your app has been modified.\n\nYour app is uniquely identified through a relying party identifier. This is a concatenation of your Team Identifier from your Apple developer provisioning profile, and your app's bundle identifier. Imagine a fraudster modifies your app, and re-signs it with a provisioning profile that does not match your Team Identifier.\n\nApp Attest surfaces your app identity on the user's device, allowing you to discover unauthorized modifications.\n\nNew in iOS 27, it highlights the launch validation category of your app on the user's device. You may have distributed your app through the App Store, but observe App Attest indicating a TestFlight launch validation category.\n\nIt also identifies the bundle version for the version of the app that you ship. If a fraudster re-signs your app with an updated bundle version that you are not aware of, this will be transparent through App Attest.\n\nLastly, you can secure payloads from your app to your server using App Attest. App Attest can generate assertions using cryptographic properties from a previously issued attestation. Your server can then verify the assertions on payloads from your app, ensuring that the payload was not tampered in transit.\n\nWith this guidance in mind, here's how App Attest works and how to adopt it.\n\nI will go over each of the core parts of App Attest.\n\nStarting with determining where App Attest is available.\n\nApp Attest is supported on all Apple platforms, including macOS 27 and higher, which was previously not supported. While App Attest is available on all major Apple operating systems, it may not be available for use through all app types on these platforms. Gate your usage of App Attest through the isSupported API from the App Attest framework. For example, App Attest is available on Action and SSO app extensions, but not other types of app extensions.\n\nYou can also use the isSupported response as a fraud signal, incorporating it into your risk assessment. If you've distributed your app on a supported platform but observe a spike in unsupported responses from a particular user, that may indicate tampering. You should decide if the user should be allowed to proceed to use your app functions when App Attest is not supported.\n\nMoving on to the first step in the App Attest workflow, which is generating a key ID.\n\nYour app starts off by calling App Attest to generate a key ID. App Attest creates a Secure Enclave-bound key pair on behalf of the app, where the private key resides in the Secure Enclave.\n\nApp Attest returns a hash of the public key to your app.\n\nYour app then stores the key ID in the Keychain.\n\nYou should keep some best practices in mind when dealing with App Attest keys. Generate 1 key per user for account-based apps or 1 key for your entire app on the user's device. Do not share keys across your user population.\n\nUse Keychain to store the key IDs generated by your app.\n\nKey IDs last as long as the app is installed. They survive app updates, but if the user reinstalls the app or restores their device, including from iCloud backup, the key is invalidated.\n\nAnd keys are per-device, they don't sync across user devices.\n\nThat wraps up App Attest key generation. These form the basis for attestations and assertions.\n\nNow that your App has generated a key ID, it can request App Attest to attest the key. Your app fetches the key ID from Keychain.\n\nYour app requests your server to perform an attestation for the key ID. Your server responds by vending a challenge to your app, to include in the attestation. Your app calls the attestation API from the App Attest framework, providing the key ID and server challenge. App Attest fetches the key pair for the key ID along with some attestation data from the device. This attestation data is derived from the Secure Enclave, which contains a snapshot of the hardware properties of the device from boot. It cannot be modified.\n\nIt initiates a server request to an Apple service which validates the device data, and returns an attestation. App Attest returns the attestation object to your app.\n\nYour app sends the attestation to your server. Your server should validate the attestation, which we will get to next, save it, and associate it with the user of your app.\n\nThere are a few best practices to keep in mind when collecting and handling attestations.\n\nIt's important that your server control the initiation of an attestation. This will help you ensure that your app stays within a safe requests-per-second upper bound.\n\nAttestation failures can occur and your app should try again at some later time. Implement an exponential back-off scheme to avoid hitting global rate limits. You should not hard-code retry logic in your app to minimize uncontrollable spikes against the Apple attestation server.\n\nYour app should collect the attestation outside of user flows. Try to perform the attestation operation on a background task. Finally, the attestation should always be validated by your server, and not the app. If your app becomes compromised, it cannot be trusted for validating an attestation.\n\nI will now go over the attestation itself, which is the object that is returned from the App Attest attestation API.\n\nThe attestation structure has three sections: format, attestation statement, and authenticator data. The format is a fixed string that identifies the Apple anonymized attestation.\n\nNext, the attestation statement embeds a cryptographic certificate chain and receipt.\n\nThe certificate chain proves that the attested key was generated on genuine Apple hardware. Follow the Developer Documentation to validate the certificate chain, which contains the nonce, key ID, and your relying party identifier embedded in the leaf certificate.\n\nOn macOS 27 and later, a key access control property, known as the ACL Blob OID, is also included in the leaf certificate. This represents the security conditions associated with the App Attest key, that were enforced by the Secure Enclave when the attestation was collected on the device. The key access control property is available on all platforms, but is especially important on macOS. On macOS, App Attest configures each generated key with a policy that requires full security mode and System Integrity Protection.\n\nFull security mode ensures the highest level of security and verifies the integrity of the operating system on the user's device. System Integrity Protection prevents the execution of unauthorized code and protects system paths. These are both enabled by default on Mac devices.\n\nBy validating the key access control property, you can be sure of the security conditions that were enforced on the user's device.\n\nThe attestation statement also contains a receipt. This is formatted similar to the App Store receipt, and you should follow the Developer Documentation for parsing this. You should validate the relying party ID, attested key, and your server challenge that is contained in the receipt. Your server should store this receipt for interfacing with the fraud metric.\n\nFinally, the authenticator data identifies information about your app and the attestation.\n\nFollow the Developer Documentation to unpack the authenticator data and validate its contents. On iOS 27 and later, a new structure is appended to the end of the authenticator data, known as extensions It is formatted as per the web authentication standard for the authenticator model. I will take a moment to talk about extensions and how your server should handle this section of the authenticator data.\n\nExtensions describe additional security properties about your app that are collected on the device during the attestation process. Two extension identifiers have been added, the launch validation category and bundle version. The launch validation category helps you understand if your app is being executed in an unexpected environment. The bundle version helps you confirm that a version of your app that you distributed, is running on the user's device.\n\nYou should monitor these properties, check for unexpected values, and factor them into your overall risk assessment for a user. That's the attestation. It is especially useful for detecting signs of tampering.\n\nFor example, you have a macOS app that is integrated with App Attest. Consider a scenario where a fraudster disables System Integrity Protection. They then modify your app, re-sign it with a different provisioning profile, and modify the App Attest framework in the system path.\n\nThe attestation received at your server will highlight the disabled System Integrity Protection state via the key access control property. It may also include a modified Team Identifier, launch validation category, or bundle version.\n\nYour server can reject communication with the modified copy of your app, and you can factor this into your risk assessment for the user.\n\nNow that your server has validated the attestation and stored the public key, your app can use that attested key to secure ongoing communication via assertions.\n\nYour app prepares itself to communicate some data with your server. Your server vends a challenge to include in the payload from your app.\n\nYour app fetches the key ID and calls the assertion API from the App Attest framework, providing the key ID and server challenge. App Attest returns an encoded assertion object to your app.\n\nYour app then embeds the assertion object into its payload and transmits the payload to your server.\n\nYour server validates the assertion and accepts or rejects the payload contents. As your app generates assertions, you should keep some important considerations in mind.\n\nGenerate them on demand as required. Assertions are generated locally on the device and do not round-trip Apple servers. They can be generated at the point in your app's lifecycle where you need them, to embed into your server payload.\n\nAssertions have CPU impact. Generating assertions involves performing cryptographic operations. Be mindful of rapidly generating assertions or generating too many within your app lifecycle.\n\nYour server should validate the counter property embedded within the assertion and ensure it is strictly increasing.\n\nYour server should track the counter from the assertions associated with a user.\n\nThis provides anti-replay attack protections. Each time your app embeds an assertion, the counter value in the assertion object should increase.\n\nIf you observe a steady or decreasing counter value, it may indicate a compromised copy of your app, that is unaware of the recorded counter value at your server.\n\nNow, I will briefly talk about unpacking the assertion object. The assertion is a structure that contains two sections: signature and authenticator data.\n\nFollow the Developer Documentation to validate the signature using the authenticator data, server challenge, and public key from the attestation object.\n\nSimilar to the attestation, the authenticator data in the assertion object identifies information about your app at the time of assertion.\n\nOn iOS 27 and later, a new structure is appended to the end of the authenticator data, known as extensions.\n\nIt should be handled in the same way as the extensions in the attestation object's authenticator data. That's the assertion and that wraps up the core parts of App Attest. The assertion is especially useful for ensuring server requests from valid copies of your app.\n\nNext up, some common pitfalls worth calling out.\n\nYour server should cautiously handle suspicious activity scenarios from your app.\n\nConsider the case of handling new attestations for an existing user. Don't reject new keys outright as legitimate scenarios such as app reinstall or device restore can cause key rotation. This also means do not invalidate keys from previous attestations for a user immediately.\n\nCoupled with the fraud metric, your server's map of attestations for a user can be used as a fraud or abuse signal.\n\nIf your server rejects an attestation or assertion, your app should gracefully handle this.\n\nDegrade functionality tied to App Attest for the user. Allow limited access for the user with heightened monitoring. Avoid blocking the user directly without a comprehensive risk assessment.\n\nI will take a minute to highlight what I mean by a risk assessment for your users. This will vary based on your business, the type of app you distribute, and the implications of potential fraud in your app.\n\nIf you suspect fraudulent activity for a user based on App Attest, follow your business' guidelines for user deactivation or suspension. Remember, blocking users without proper evaluation can erode trust, and may impact legitimate users. Always follow a well-defined risk assessment process.\n\nYou now have a broad and deep understanding of the core flows of App Attest, and how to best interact with them.\n\nThe last part of App Attest to cover is the fraud metric, a tool for detecting suspicious attestation activity.\n\nA compromised device could still pass attestations and act as a broker, by generating valid attestations on behalf of modified app instances running on other devices.\n\nThese modified apps can send compromised requests to your server. The fraud metric provides an approximate count of unique attested keys associated with your app, on a particular device over the past 30 days. You can use this as part of your risk assessment profile for a user, by determining if they are associated with attestations from a potentially-compromised device.\n\nThe fraud metric is accessed between your server and the App Attest data server. Your server retrieves the receipt from an attestation associated with a user. It then sends a POST request to the App Attest data server, using the retrieved receipt.\n\nThe data server then returns a receipt to your server, containing the fraud metric, which you should use for subsequent receipt fetches.\n\nThe receipt is structured similar to an App Store receipt. It has three sections: a signature, certificate chain, and receipt payload.\n\nThe signature signs the receipt payload. The certificate chain roots to the Apple certifying authority. The receipt payload contains information about the attested key associated with the metric and the fraud metric itself.\n\nFollow the Developer Documentation to verify the different parts of this receipt payload.\n\nThe risk metric field defines the fraud metric count. The receipt must also be refreshed. The not before field outlines the earliest point at which you can refresh it. The expiration time field describes when the receipt expires and it can no longer be refreshed.\n\nConsider the following when working with the fraud metric. Any user steps that involve App Attest key rotation will contribute to the fraud metric. For example, reinstalling the app or restoring the device may force key generation and reattestation within your app. These can contribute to the fraud metric.\n\nAvoid using the fraud metric to block users from your app outright. Treat the fraud metric as a fraudulent activity investigation signal. You should monitor its value, analyze it for a baseline, and identify spikes as indicators of suspicious activity.\n\nNow you have everything you need to protect your app's workflows with App Attest keeping your users safe and your app secure.\n\nTo continue your journey, start off by re-building your app against the latest SDKs, so you're accessing the latest features from the App Attest API.\n\nIdentify areas of your app that may benefit from the security of an attestation, such as authentication flows, or sensitive payloads for premium content that can be strengthened with assertions.\n\nSet up your server to validate attestations, store receipts, and track assertion counters.\n\nIncorporate the fraud metric into your risk assessment pipeline App Attest gives you the tools to verify your app's integrity, secure the communication between your app and your server, and detect signs of fraud all backed by the security of Apple hardware. Now go put these protections to work for your users. Thanks for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "5:07",
+ "title": "Generate a Secure Enclave–bound key",
+ "language": "swift",
+ "code": "import DeviceCheck\n\nlet keyID = try await DCAppAttestService.shared.generateKey()"
+ },
+ {
+ "timestamp": "6:32",
+ "title": "Attestation API",
+ "language": "swift",
+ "code": "import DeviceCheck\n\nlet keyId: String = ...\nlet clientDataHash: Data = ...\nlet attestation = try await DCAppAttestService.shared.attestKey(keyId: keyId, clientDataHash: clientDataHash)"
+ },
+ {
+ "timestamp": "12:33",
+ "title": "Assertion API",
+ "language": "swift",
+ "code": "import DeviceCheck\n\nlet keyId: String = ...\nlet clientDataHash: Data = ...\nlet assertion = try await DCAppAttestService.shared.generateAssertion(keyId: String, clientDataHash: Data)"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "W3C Authenticator Data",
+ "url": "https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data"
+ },
+ {
+ "title": "About System Integrity Protection on your Mac",
+ "url": "https://support.apple.com/en-us/102149"
+ },
+ {
+ "title": "DeviceCheck",
+ "url": "https://developer.apple.com/documentation/DeviceCheck"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/201/4/d3eb2e5b-5104-4aee-a754-9985008a5b06/downloads/wwdc2026-201_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/201/4/d3eb2e5b-5104-4aee-a754-9985008a5b06/downloads/wwdc2026-201_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:11.106Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-203.json b/data/wwdc/videos/2026-203.json
new file mode 100644
index 0000000..b48ebad
--- /dev/null
+++ b/data/wwdc/videos/2026-203.json
@@ -0,0 +1,81 @@
+{
+ "id": "203",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/203/",
+ "title": "Read between the strokes with PencilKit",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Welcome! I am Yichen, an engineer who works on drawing features. In this session, I'll show you some exciting new APIs in PencilKit that bring handwriting recognition to your apps, along with improved access to the drawing model that open up interesting use cases. Writing on iPad with Apple Pencil feels as natural as pen on paper. But unlike paper, your handwriting becomes searchable, recognizable, and interactive. PencilKit is the core framework for freehand drawing support on Apple's platforms. It gives you a low-latency canvas with expressive inks, full Apple Pencil support, and access to the underlying data model. iOS 26 introduced PaperKit, which builds on top of PencilKit to power Apple's drawing experience system-wide. All of the new PencilKit APIs covered in this session are also available when working with PaperKit. To learn more about it, check out the \"Meet PaperKit\" video from WWDC25 and the \"Unwrap PaperKit\" video from WWDC26. Apple's platforms have world-class handwriting recognition. You've seen it power features like searching handwriting in Freeform and Notes. One of the most requested capabilities for PencilKit is handwriting recognition. Now it's available to you in iOS, iPadOS, macOS, and visionOS 27. I'll start with PencilKit's handwriting recognition APIs, and show you how to turn handwriting into recognized words.\n\nNext, I'll go over new conversion APIs that let you translate between PencilKit stroke paths and standard Bézier paths. Then, I'll cover improvements to the drawing model, including stroke identity and selection access.\n\nAnd finally, I'll show new stroke slicing APIs that let you split or extract segments from strokes. I've been working with PencilKit in iOS 27 to build my son an app for learning to write in both Chinese and English. In my app, he can practice writing words in both languages and get feedback on his work. Here, the app has a flashcard showing a word in English. To practice, I will write the Chinese translation. Right now, it's showing \"Heart\" in English. So I need to write \"心\" in Chinese.\n\nNow, the app can use handwriting recognition to check my current work before I finish the whole word.\n\nThe Check button shakes to tell me the answer wasn't right.\n\nThis time the app recognized my handwriting as the Chinese word \"心\" which matched the prompt, and the app confirmed it's correct. Great!! That's what handwriting recognition APIs can do in PencilKit.\n\nI'll get started with the core of handwriting recognition.\n\nPKStrokeRecognizer is a Swift actor, so it's thread-safe by design. All methods are asynchronous, because recognition takes time.\n\nThere are three main capabilities that together provide a handwriting recognition experience that is broadly useful for your apps. I'll walk through each one. The first capability is Recognized text. It returns the single most likely result for what was written.\n\nBy default, PKStrokeRecognizer uses the device's languages to determine how to interpret the handwriting. You can also configure preferredLanguages explicitly to match your app's language context.\n\nYou can recognize an entire drawing at once, or pass in a specific subset of strokeIDs. I'll swipe over to the open-ended practice view, to show Recognized text in action.\n\nI can write anything I want here, and PencilKit will recognize it.\n\nAwesome! Hello WWDC.\n\nAs of iOS 27, PKStrokeRecognizer supports 29 languages. The list of languages supported can be accessed using the supportedLanguages property on PKStrokeRecognizer. Note that handwriting recognition in Simulator only supports languages that use Latin characters. Handwriting recognition in PencilKit runs entirely on device. The recognition model is offline and included with the operating system. Handwriting recognition is fast, and works on all devices supported by iOS 27. The next API is Indexable content. It provides a single string representing the contents of the entire drawing.\n\nThis is particularly useful for features like Spotlight search.\n\nWhen multiple languages are active, Indexable content may contain results in more than one of those languages. When handwriting is ambiguous, you want all possible interpretations in your index; so users can find their content regardless of what they search for. Is that a \"1\" or a lowercase \"L\"? Is that \"101\" or \"lol\"? Where recognized text gives you one best answer, Indexable content gives you all the candidates concatenated together.\n\nIf you're persisting Indexable content to disk, keep in mind that recognition results can improve over time as the underlying models are updated.\n\nPKStrokeRecognizer exposes a recognizerVersion property. Store this alongside your indexedContent, and when loading it back, compare against the current version to decide whether to re-index.\n\nAdditionally, consider throttling your calls to PKStrokeRecognizer.\n\nUpdating the results on every stroke may use more power than you need to provide your indexing feature. The third API is for searching text.\n\nGiven a target string, it returns an array of search results indicating where that word likely appears in the drawing, considering all the candidates.\n\nThis is what powers interactive search, where a highlight appears around matching strokes.\n\nsearch() also pairs naturally with UIFindInteraction, the system find and replace experience.\n\nBy implementing UIFindInteractionDelegate and driving it with search() under the hood, you get the system search UI, complete with result navigation and highlighting, directly in your drawing canvas. Back in the flashcards, I've made a change to show how the search function powers flashcard matching.\n\nI've added a visualization of the bounds returned in SearchResults.\n\nNow there's a box showing where the search matched.\n\nThese capabilities also make handwritten content more accessible. You can connect VoiceOver to speak handwriting aloud, making handwriting accessible to people who rely on screen readers. And search lets assistive features locate and navigate to specific words within a drawing. Together, they help bridge the gap in accessibility between handwritten and typed text. Next: Path conversion. Path conversion is a powerful set of APIs that help you to bring PencilKit to your app.\n\nPencilKit represents stroke paths as cubic uniform B-splines. For more information on how PencilKit stores paths, check out the \"Inspect, modify, and construct PencilKit drawings\" video from WWDC20.\n\nB-Splines are a great representation for drawing, but are less common than Bézier paths. In iOS 27, PKStrokePath supports conversion between the two.\n\nWhen converting a path, PencilKit handles the geometry. Bézier paths don't carry PencilKit properties like size, opacity, or force. So you need to provide those for each control point.\n\nWhen starting from a PKStrokePath, converting to and back from a Bézier path will result in the same control point locations, allowing for storing PencilKit strokes in a Bézier-based format, and reconstructing without any loss of fidelity. If your app has its own canvas with strokes stored as Bézier paths, you can now convert those to PKStrokePaths, build a PKDrawing, and feed it into PKStrokeRecognizer. This makes handwriting recognition compatible with any canvas, not just PKCanvasView.\n\nBeyond expanding where PencilKit can be used, iOS 27 introduced key additions that enable deeper access to the model. This flexibility supports more custom use cases in your apps.\n\nPKStroke and PKStrokePath both gain conformance to the Identifiable protocol. It is a stable UUID, so you can track a stroke across transforms, edits, and undo operations.\n\nWith a stable identity in place, you can now control the selection state on PKCanvasView.\n\nThere's also a new delegate method, canvasViewSelectionDidChange, that fires whenever the user's selection changes. When certain inks are drawn together quickly, PencilKit composites those strokes together as if the inks were still wet, by using an equal renderGroupID. In iOS 27, it is now controllable. The last set of new APIs gives you the ability to slice through strokes in two different ways: Programmatic erasing and Substroke extraction. If you have worked with PencilKit's data model, you may already be familiar with the stroke mask. PencilKit represents partial erasure using a mask.\n\nWhen the pixel eraser removes part of a stroke, the remaining visible portions are defined by a mask. Starting in iOS 27, you can apply that same operation programmatically. You provide a PKStrokePath as the eraser.\n\nIt cuts through the drawing, slicing a single stroke into multiple independent strokes with their own mask, just as if the user had used the eraser tool on the canvas.\n\nOne thing to keep in mind: slicing can be expensive on complex drawings.\n\nIf your drawing has a large number of strokes, be mindful of performance and consider erasing on a background thread rather than blocking your UI.\n\nWhen erasing visually cuts strokes, Substroke extraction lets you efficiently obtain a section as a new stroke or path.\n\nStarting in iOS 27, both PKStroke and PKStrokePath support subscript access with parametric ranges anywhere along the path. This gives you precise control over exactly where a slice begins and ends.\n\nPencilKit inks have features like pencil texture particles that are positioned relative to the full stroke.\n\nWhen taking substrokes, PencilKit maintains the consistency of those particles. For Chinese characters, the order that you write the strokes is important. To check that the order is correct, I built a feature using substrokes to replay how I wrote the word.\n\nPencilKit implements rendering in Metal with optimizations that make my animation smooth.\n\nTo take full advantage of these powerful APIs: Adopt PKStrokeRecognizer and bring handwriting recognition to your app.\n\nTry PencilKit in new places to convert your existing Bézier paths into PKStrokePaths and take advantage of handwriting recognition, even without PKCanvasView. Dive deep into PencilKit's model to track strokes with stable identity, respond to selection changes, and build a new level of custom experiences. Finally, take advantage of stroke slicing. Cut through drawings with programmatic erasing, and build smooth animations with substrokes.\n\nEach of these APIs are simple to integrate. Together, they unlock features in your apps that were not possible before.\n\nThe sample code for the demo is available in the resources for this video. Handwriting becomes recognized, searchable, and interactive — everywhere in your app. That's PencilKit in iOS 27. Thanks for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:53",
+ "title": "Recognized text",
+ "language": "swift",
+ "code": "import PencilKit\n\nlet recognizer = PKStrokeRecognizer()\nawait recognizer.updateDrawing(drawing)\nmyLabel.text = await recognizer.recognizedText()"
+ },
+ {
+ "timestamp": "5:22",
+ "title": "Indexable content",
+ "language": "swift",
+ "code": "import PencilKit\n\nlet recognizer = PKStrokeRecognizer()\nawait recognizer.updateDrawing(drawing)\nif let indexedContent = await recognizer.indexableContent {\n index(text: indexedContent)\n}"
+ },
+ {
+ "timestamp": "6:58",
+ "title": "Find text",
+ "language": "swift",
+ "code": "import PencilKit\n\nlet recognizer = PKStrokeRecognizer()\nawait recognizer.updateDrawing(drawing)\nlet results = await recognizer.search(\"apple\")\nfor result in results {\n highlight(bounds: result.bounds)\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Controlling stroke rendering for animation and editing",
+ "url": "https://developer.apple.com/documentation/PencilKit/controlling-stroke-rendering-for-animation-and-editing"
+ },
+ {
+ "title": "Recognizing handwriting and converting it to text",
+ "url": "https://developer.apple.com/documentation/PencilKit/recognizing-handwriting-and-converting-to-text"
+ },
+ {
+ "title": "Building a handwriting recognition experience with PencilKit",
+ "url": "https://developer.apple.com/documentation/PencilKit/building-a-handwriting-recognition-experience-with-pencilkit"
+ },
+ {
+ "title": "PencilKit",
+ "url": "https://developer.apple.com/documentation/PencilKit"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/203/4/eb979cd5-af5b-4091-87ec-4839e8d131b9/downloads/wwdc2026-203_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/203/4/eb979cd5-af5b-4091-87ec-4839e8d131b9/downloads/wwdc2026-203_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "372",
+ "year": "2026",
+ "title": "Unwrap PaperKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/372"
+ },
+ {
+ "id": "285",
+ "year": "2025",
+ "title": "Meet PaperKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/285"
+ },
+ {
+ "id": "10148",
+ "year": "2020",
+ "title": "Inspect, modify, and construct PencilKit drawings",
+ "url": "https://developer.apple.com/videos/play/wwdc2020/10148"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:11.455Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-204.json b/data/wwdc/videos/2026-204.json
new file mode 100644
index 0000000..045f22a
--- /dev/null
+++ b/data/wwdc/videos/2026-204.json
@@ -0,0 +1,82 @@
+{
+ "id": "204",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/204/",
+ "title": "What’s new in WebKit for Safari 27",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Machine Learning & AI",
+ "Safari & Web",
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hi! I'm Jen Simmons! And I'm here to share the latest news about WebKit, the web browser engine that powers Safari.\n\nThe WebKit and Safari teams have been busy this year. I'm excited to share what we've been working on, and how it impacts you as you make websites, web apps, and web content.\n\nWe've already shipped a lot of new web technology, including CSS Grid Lanes, Navigation API, Largest Contentful Paint, and many more.\n\nThe first beta of Safari 27 brings over 60 more new features, including Customizable Select, img sizes=auto, the stretch keyword for layout, and more. Later in this session, I'll walk you through five of the most exciting new features. Stick around to see what CSS Grid Lanes can do, learn how you can customize Select UI, explore what the Model Element brings to the web, and more!\n\nBut first, I think what you might find most exciting as a web developer is the time my team put in this year to improve the quality of WebKit.\n\nI hear from web developers that it's challenging when you to have to work around a problem in our browser engine. You expect more.\n\nWe hear you. And we set out to greatly improve the quality of existing web platform features. We put our focus there this year, rather than on implementing a whole lot of brand new ones.\n\nWhen I look at what we've accomplished so far, a few clear themes emerge. I want to share one quick story from each.\n\nFirst, compatibility.\n\nA big part of this work focuses heavily on the things that directly affect users, making sure real websites work for real people.\n\nYour users are our customers and we want them to have a fantastic experience.\n\nMy team got a report about a particular website. When users typed emoji, the totally wrong character appeared. But why? Well, normally, when a user presses a key, the browser sends a Unicode number to the website. The number for this A uses 7-bits of data.\n\nSome websites want to intercept keyboard input and process it with JavaScript. For decades, websites used the .fromCharCode method to turn those numbers back into characters. .fromCharCode can handle data that's 16-bits or smaller.\n\nAnd for a long time, even as Unicode grew, that was enough. But then a lot of emoji came along. And these new characters got assigned bigger numbers. This one needs 17-bits.\n\nAnd on websites that still use .fromCharCode, that number got truncated to 16-bits. Which maps to a totally different character. So, what should we do? Try to get every website to stop using .fromCharCode? That's not realistic. Users wanna type emoji now! So my colleagues adopted a clever workaround. Now when a user types a character with a code point beyond 16-bits, WebKit doesn't send the number to the website at all.\n\nThe emoji arrives as text instead. No number, no truncation. And now everyone can type a face holding back tears on any website they wish. It's one example of the many changes we've made to directly help users. But, as a web developer, you just want your work to become easier. My team is also working to rebuild the foundations of WebKit in quite a few areas. Sometimes we just gotta to start from scratch to pay off technical debt.\n\nThis deep work isn't necessarily visible on the surface, but it makes everything built on these foundations more reliable. Here's one: block-in-inline layout.\n\nBlock elements end up nested inside of inline elements all the time.\n\nThe code for handling such layout was over two decades old, and had grown tangled and hard to maintain. So we rewrote it from scratch with a new architectural approach.\n\nYou probably won't notice but the work did fix a bunch of issues. My team is also going deep to make significant progress in specific areas. Media and video playback, Scrolling, SVG, Accessibility, WebRTC, even HTML tables! Each of these areas needed something different.\n\nTake SVG for example. To make real progress, we needed the web standard to have more clarity and detail. Only there was no active SVG Working Group. So, that's how my teammates accelerated their journey, by reviving and leading a new standards effort.\n\nFor example, what happens if a web developer makes an SVG with a radial gradient, but doesn't explicitly define its focal point with the fx and fy attributes? In SVG 1, the spec text describing the initial values of fx and fy was confusing, and different browsers interpreted it differently.\n\nNow, SVG 2 removes all ambiguity. The initial value is clearly defined.\n\nAnd we're updating WebKit in Safari 27. We've made over 75 improvements to SVG so far, and there's definitely more work to do.\n\nA lot of the improvements my team is making are to better align with web standards. Both to shore up the interoperability of longstanding web technology, and to keep up with the evolution of new features.\n\nHere's something new: the CSS random function.\n\nYou can use it to generate random values. With this code, width and height get random lengths.\n\nYou can name random values to reuse them. But originally, names were scoped per instance. Each time the box class was called, new random values got calculated. We shipped random in Safari 26.2 but then developers argued that maybe this could be better.\n\nAfter discussion at the CSS Working Group, these names were redefined to be global by default.\n\nNow all these boxes get the same random size. We updated the scoping of names in Safari 26.5. We're still the only browser with support, so there was time to make it better. We're keeping up with recent changes to features like Anchor Positioning and View Transitions. And we're strengthening older features, too. There are hundreds of updates to better align with web standards. We want it to be easier for you to create websites that just work in every browser.\n\nAnd the fifth area of improvements: integration.\n\nA bunch of improvements make sure separate features integrate together correctly.\n\nIn 2014, WebKit shipped support for the sizes attribute in HTML enabling responsive images. In 2018, we shipped support for the CSS min() and max() functions. But when we did, we somehow missed implementing support for min() and max() inside of sizes. Thank you to the developers who filed issues about this.\n\nWhen clamp() came along in 2020, we didn't do it then either. This year we circled back and closed the gap in Safari 26.4. These are the kinds of problems that we want to keep finding and fixing. We care deeply about the experience people have when interacting with the web on our platforms.\n\nBrowser engines are huge and we rely on the feedback and contributions from developers to help identify pain points and inform our priorities.\n\nMillions of apps in the App Store take advantage of web technologies built into WebKit and JavaScriptCore, including apps for iOS, iPadOS, macOS, visionOS, and even watchOS! We care about the web and we sincerely hope our efforts make a real difference in your success.\n\nWith this focus, we were able to tackle over a 1100 feature improvements and fixes since last fall. That's a record for us. We'd love to have you test your projects in the most recent versions of Safari Technology Preview or Safari beta.\n\nAnd if you are having problems, please file an issue. We know there's more to do.\n\nI only covered a few examples of our quality efforts. For all the detail, check out Safari release notes.\n\nQuality improvements are a huge part of what my team has been doing this year but it's not all.\n\nWe've shipped some genuinely exciting new features, too. In the rest of this session, I'll go through several.\n\nStarting with CSS Grid Lanes, which shipped in Safari 26.4.\n\nUse it to create the classic masonry layout in pure CSS. No JavaScript needed.\n\nIt can handle use cases that you've maybe never considered before.\n\nThe code is simple, and it includes all the power of CSS Grid to define tracks.\n\nAnd yes, it works in the other direction! Check out our Field Guide to Grid Lanes at gridlanes.webkit.org. Click through different configurations to experience how it works, and explore the many demos.\n\nUse Safari Web Inspector to better understand and adjust your layout. Enable Order Numbers to reveal the order of Items.\n\nIt's super helpful for adjusting flowtolerance to finesse the experience for users tabbing through content.\n\nAnd watch, \"Learn CSS Grid Lanes,\" where Brandon will walk you through exactly how to create these layouts.\n\nCustomizable Select is another exciting feature coming to the web. Safari 27 transforms the select element. It's easier than ever to implement a fully-custom design to match your website or web app with great accessibility automatically! You start by applying appearance: base-select in CSS to the element.\n\nImmediately the control starts inheriting more CSS, including your font family, text color, and background color.\n\nAlso, apply appearance: base-select to the new ::picker pseudo-element.\n\nThis unlocks the ability to style the menu of options that pops up when a user taps on the control. Other brand new pseudo-elements let you target specific parts of the control, like the ::checkmark, and the ::picker-icon.\n\nNow, you can add additional HTML inside . Like subtext describing the details, or images representing each option. All while leveraging the benefits of using a real HTML form control, with its accessibility and robustness.\n\nUse Grid or Flexbox to layout the options, or anything from CSS and make it look nothing like a traditional drop down menu.\n\nTim will teach you how in his session, \"Rediscover the HTML Select Element.\" Last year, we shipped , a brand new HTML element for Safari in visionOS.\n\nIn Safari 27, is coming to iOS, iPadOS, and macOS.\n\nIt joins , , and in the family of elements that handle media files. This time putting 3D models into HTML.\n\nBy adding a model to your website, you can provide a way for users to check out products, preview an object in their space, or just have a cool experience.\n\nThe markup works like you would expect. You can keep it simple, or just like the other media elements, you can use to link to multiple files in different formats.\n\nYou can optionally include attributes like environmentmap to provide custom lighting for your model. Or stagemode, which sets the default to interaction behavior. Target your model with JavaScript to open up a wide range of possibilities, or wrap the model in to let users on iOS & iPadOS see the product in their space.\n\nYou can find much more about making and using 3D models on developer.apple.com. There's years of documentation, videos, and sample projects diving into what's possible.\n\nWatch \"Get started with HTML Model Element\" to learn more. Aleksei will explain where to get a 3D model, how to optimize it for the web, and what to do in JavaScript.\n\nIn visionOS 27, goes even further with immersive website environments.\n\nA user can go to your website in Safari, tap to open an immersive environment and step into the model you provide. Maybe you've made an immersive video game, and you want to give potential customers a way to preview it.\n\nMaybe your site sells theater tickets. You can show people the view from their seats on any platform and then in visionOS, they can experience the whole theater. There's a new Immersive API for manipulating models. It works just like the Fullscreen API. Learn all about it watching \"Explore immersive website environments in visionOS\" where Jean will show you how he built the theater.\n\nAnd then, there's Web Extensions. Not that long ago, making a browser extension meant creating entirely separate projects for each browser, with different code, using different APIs. That's why so many extensions only worked in one browser.\n\nThings started to change in 2017, when Mozilla deprecated support for Firefox add-ons and committed to a cross-compatible future.\n\nIn 2020, Safari 14 shipped support for Safari Web Extensions.\n\nAnd then in 2021, we led an effort to create the W3C WebExtensions Community Group, and turn these ideas into official web standards. Today, the dream of an interoperable extension future is becoming a reality.\n\nYou can create an extension using one codebase, one set of scripts, one manifest. It's all interoperable HTML, CSS, and JavaScript, just like the rest of the web. You just have to know how to package and distribute your extension to the users of each browser.\n\nBut if you don't use Xcode, or even have a Mac, how can you distribute your extension to Safari users? Now, you can use the Safari web extension packager.\n\nIt enables you to package and distribute your extension using App Store Connect from any web browser, on any operating system. Today, it's easier than ever to reach Safari users.\n\nYou can read the documentation to learn how on developer.apple.com and watch \"Create web extensions for Safari,\" where Kiara will guide you through how to build a web extension from the ground up, as well as how to use App Store Connect.\n\nFinally, I want to quickly mention MapKit JS. It's a tool that you can use to embed interactive maps on your website or web app. All while preserving the privacy of your users.\n\nIt works in all browsers, on any operating system.\n\nLearn all about it at developer.apple.com. My team has done a lot work this year. WebKit is packed with improvements.\n\nI truly hope these efforts make your work more satisfying, more successful, easier, and maybe, more fun! Check out our website, at webkit.org, where we teach about all the new features in every release of Safari. There, you can learn more about what's in Safari 27, watch other WWDC sessions about web technology to dive deeper, and file issues at bugs.webkit.org. We'd love to hear from you, and to know what more we can do to support your work making projects on the web for people to use and enjoy. Thanks for watching!",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "WebKit.org - CSS Grid Lanes Field Guide",
+ "url": "https://gridlanes.webkit.org"
+ },
+ {
+ "title": "Packaging and distributing Safari Web Extensions with App Store Connect",
+ "url": "https://developer.apple.com/documentation/SafariServices/packaging-and-distributing-safari-web-extensions-with-app-store-connect"
+ },
+ {
+ "title": "WebKit.org – Report issues to the WebKit open-source project",
+ "url": "https://bugs.webkit.org"
+ },
+ {
+ "title": "Learn more about MapKitJS",
+ "url": "https://developer.apple.com/maps/web/"
+ },
+ {
+ "title": "Safari Technology Preview",
+ "url": "https://developer.apple.com/safari/technology-preview/"
+ },
+ {
+ "title": "Submit feedback",
+ "url": "http://feedbackassistant.apple.com"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/204/5/0226f57f-eb7b-4c8d-91cb-0ac8f245d88b/downloads/wwdc2026-204_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/204/5/0226f57f-eb7b-4c8d-91cb-0ac8f245d88b/downloads/wwdc2026-204_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "216",
+ "year": "2026",
+ "title": "Create web extensions for Safari",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/216"
+ },
+ {
+ "id": "320",
+ "year": "2026",
+ "title": "Explore immersive website environments in visionOS",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/320"
+ },
+ {
+ "id": "215",
+ "year": "2026",
+ "title": "Get started with the HTML Model Element",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/215"
+ },
+ {
+ "id": "314",
+ "year": "2026",
+ "title": "Learn CSS Grid Lanes",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/314"
+ },
+ {
+ "id": "315",
+ "year": "2026",
+ "title": "Rediscover the HTML select element",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/315"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:12.155Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-205.json b/data/wwdc/videos/2026-205.json
new file mode 100644
index 0000000..ce09b55
--- /dev/null
+++ b/data/wwdc/videos/2026-205.json
@@ -0,0 +1,38 @@
+{
+ "id": "205",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/205/",
+ "title": "Enhance your presence on the App Store",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Developer Tools"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "When someone discovers your app on the App Store, what they see in those first few moments, drives whether they'll want to learn more about your app, or scroll past it. Today, I'll show you new ways to use images and videos to shape that moment. Hi, I'm Ruhi, an engineering manager on the App Store Connect team.\n\nFirst, a quick recap of how your app currently appears on the App Store. The product page, screenshots and previews, give people a glimpse into your app, by highlighting the experience or key features. Similarly when your app is shown in the Search Results, by default, screenshots and previews are shown. Here's what's changing for these two places on the App Store! First, your Product Page can now have a Header, and here you have the freedom to use images and videos, outside of your app screenshots and previews.\n\nProduct Page Header is the first visual element people will see when they land on your app and a compelling asset can spark interest. Use this new real estate, to express your visual identity or brand. These assets can be images, or videos. Videos can be a great addition, allowing you to control the narrative of how someone perceives your app.\n\nYour Product Page Header, app icon and screenshots work together, to give people a clearer picture of what your app is all about.\n\nNow, I'll discuss what's changing for App Store Search Results.\n\nInstead of showing the default app screenshots, you can use an impactful image or video, to make your app stand out in the Search Results. Choose assets that clearly communicate your app's core value and features, in a way that encourages people to tap and learn more about your app.\n\nYou can also use these images and videos to create Ads to boost your app's discovery. Set them up on the Today tab or Search Results using Apple Ads. Additionally, leverage these assets with Custom Product Pages, to better connect with your audience. I'll illustrate how you can do that with a few examples.\n\nExercise App's website is marketing Yoga classes with a banner on top to get the app.\n\nAnd the website is linked to the app's Custom Product Page. Now, you have the ability to customize the header, and use the same marketing visuals that you used on your website. Additionally, when people download the app, you can deep-link them directly into the Yoga offering, for a consistent experience from app discovery to installation.\n\nUsing Custom Product Pages, you can also show a tailored asset for different search keywords, to make your app's search result more relevant and compelling.\n\nSimilar to the website example, consider using the same assets on your header, as your Search Results, for a seamless customer experience.\n\nAdditionally, use Product Page Optimization in App Store Connect, to test different visuals and see exactly which ones your audience respond best to, whether it's your app's logo, core value or a new feature.\n\nApps and games across all the categories on the App Store, can take advantage of these images and videos. For example, outdoor apps could show aspirational imagery. Travel apps could promote popular destinations. Or games could showcase gameplay or interesting characters. You can update your product page header and search result visuals, on both iOS 27 and iPad OS 27. Now, I'll discuss how to setup this rich media using App Store Connect. There are 2 ways to submit to App Review.\n\nFirst, the flow that you are familiar with, through your app's version page. On your version page, upload the assets that you intend to use. Then use the new Preview functionality, to review how your app looks on the App Store with these new assets. You can view them for both iPhone and iPad, across different orientations and languages.\n\nOnce you make sure that everything looks good, submit your version for review like you do today. Once your version is approved and released, the assets on your Product Page Header and Search Results, are also live on the App Store. The second way to submit these assets for review, is through the new Asset Library in App Store Connect! Asset Library, is a centralized place to manage all your app's assets, across platforms, sizes and the different placements. Asset Library includes, your existing screenshots, preview videos, in-app event media and your new marketing images and videos. And these new marketing visuals, are called creative assets, in App Store Connect.\n\nSimilar to the version submission, first, upload your creative assets directly to Asset Library. Once you have uploaded, submit them for review. In this flow, you submit your creative assets standalone, without updating your version, or indicating where you plan to use them in the future.\n\nSo your assets can be approved, either as part of your app version submission, or directly through the Asset Library flow. Once they are approved, all of them are available in your Asset Library. One big advantage of Asset Library, is that approved assets are ready to be used across your app's Product Page Header and Search Results, without going through additional reviews. I'll illustrate with an example! This is the current Product Page Header with a summer hiking asset.\n\nAnd these are some approved assets in the Asset Library. A winter hiking asset is selected, to replace the summer asset, publishing changes directly to the App Store without a new submission.\n\nAnd there it is! Cool! Getting your assets approved ahead of time, gives you the flexibility to update your app's Product Page Header and Search Results in real-time. To automate these flows, you can upload and submit to Asset Library, using the App Store Connect API as well. Additionally, you can use the Apple Ads Platform API to automate ad setup flows, which includes open-source client libraries for Swift and more.\n\nAs a next step, start preparing the images and videos, that you'll use to showcase your app on the App Store. Then, upload your assets to Asset Library, for use on your Product Page, Search Results, and more. Finally preview your app before you submit for review.\n\nI am very excited to see how you use these new visuals, for your app on the App Store and what it unlocks for you! Thank you for watching!",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Design your own ads with creative assets",
+ "url": "https://ads.apple.com/app-store/h/help/design-your-own-ads-with-creative-assets"
+ },
+ {
+ "title": "App Store - What's New",
+ "url": "https://developer.apple.com/app-store/whats-new/"
+ },
+ {
+ "title": "Creating your Product Page",
+ "url": "https://developer.apple.com/app-store/product-page/"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/205/4/47ee16f9-fba0-48a3-9d60-065befef7a95/downloads/wwdc2026-205_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/205/4/47ee16f9-fba0-48a3-9d60-065befef7a95/downloads/wwdc2026-205_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:11.575Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-206.json b/data/wwdc/videos/2026-206.json
new file mode 100644
index 0000000..fa641bd
--- /dev/null
+++ b/data/wwdc/videos/2026-206.json
@@ -0,0 +1,44 @@
+{
+ "id": "206",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/206/",
+ "title": "What’s new in managing Apple devices",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Store, Distribution & Marketing",
+ "Business & Education"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hello and welcome! My name is Cyrus Daboo and I am an engineer on the Device Management team. I'm here to show you what's new in managing Apple devices.\n\nHere's what I have for you. I'll discuss what's new in Apple services.\n\nI'll then cover a number of additions to declarative management.\n\nFollowed by updates to app management.\n\nAnd details of new identity management features.\n\nI'll finish off with a quick update to some education-related technologies.\n\nFirst, some updates to Apple services for business and education. Apple Business is a new all-in-one platform, that combines tools for businesses of all sizes, to effectively run and grow their organization.\n\nThis includes a big expansion, with Apple Business now available in over 200 countries and regions. This gives your organization features like zero-touch deployment, Managed Apple Accounts for users, and the new built-in device management features.\n\nThis makes it easier for businesses to quickly get started with managing Apple devices.\n\nTo support automation of these new features, there are new APIs for Apple Business, including creating Blueprints and Configurations, modifying users and groups, app license information, and getting audit events.\n\nThese join the existing APIs to list servers, devices, and inventory, manage the assignment of inventory to device management servers, and get AppleCare warranty details for devices.\n\nBe sure to check out the documentation on the new APIs to start building them into your products.\n\nThere's also a new volume licensing mechanism for subscriptions in App Store apps. This allows IT administrators to purchase and manage app subscriptions. It'll be available later in Apple Business and Apple School Manager. You can use a device management service, to assign app subscriptions using the same workflows, that already exist for distributing apps at scale. Take a look at the \"Offer subscriptions to groups and organizations\" video for all the details.\n\nThese are exciting new features, and Apple Business and Apple School Manager will continue to evolve to meet the needs of IT teams everywhere.\n\nNow I'll cover changes to device management support on Apple devices. I want to start by revisiting a core pillar of Apple's device management story. \"The future of device management is declarative management\" - which describes the move to declarative management. But the future is now.\n\nDeclarative management isn't something on the roadmap anymore. It's here. It's shipping. It's in production across fleets around the world. If you're managing devices today without using it, you're working harder than you need to. So now \"The standard for device management is declarative management.\" The declarative management enhancements I'll cover here weren't built in isolation.\n\nThey were built alongside the great new hardware that was recently released. There is incredible momentum in business and education, thanks in part to all new Mac computers. MacBook Neo is great for many first-time Mac users, particularly in K-12 and higher education.\n\nAnd the performance improvements in the latest MacBook Air and MacBook Pros, are ideal for demanding AI workflows popular in enterprise.\n\nTogether, this tells one clear story: the Mac and the full Apple device lineup isn't just a choice for business and education. It's the choice.\n\nTo make it easier to switch to a new Mac, a new managed migration feature is available to help migrate data, while preserving device management enrollment and settings.\n\nTo activate this, a new declarative configuration is deployed to the device right after device management enrollment.\n\nThis gives IT administrators control over which accounts, files, and security and privacy settings are migrated. Migration Assistant reports declarative management status, so IT administrators can monitor the progress of migration. These settings are shown to the user, but they're locked. All the user needs to do is click Continue, to begin the migration process. This is a great way to get your users up and running with a new Mac.\n\nThe 26.4 releases also included new declarative configurations for Apple Intelligence, Siri, and keyboard settings.\n\nAnd in the latest releases, these configurations have been updated, to provide IT administrators granular controls for the individual Apple Intelligence and Siri features, that are now available to users.\n\nNow I'll go over how you can leverage the power of the declarative management data model, starting with credential management. Configuration profiles have limits on how they can reference credentials, often forcing large profiles to be used, and making the update process inefficient. Since the declarative model supports a many-to-many relationship, multiple configurations can reference a single credential.\n\nConfiguration profiles that use credentials are being transitioned to declarative configurations, so managing the lifecycle of credentials is much more efficient.\n\nWhen you need to refresh a credential, your server only needs to change the asset, and the device takes care of updating all the configurations that use it.\n\nHere are the new configurations.\n\nWhenever these configurations need a credential be it a certificate, identity, or password, a declarative asset is used for the credential data.\n\nThe status channel is another powerful element of declarative management.\n\nIt removes the need for servers to continually poll devices for state changes.\n\nThe new release adds a number of declarative status items, such as the enrollment type, awaiting device configuration, return to service state, Shared iPad, the device's current push token, and several more. Plus there's a new status item to indicate if Lockdown Mode has been turned on by the user.\n\nA new feature that the status channel exposes is device system health monitoring. iOS and iPadOS devices can report issues with hardware components to users, through the Settings app. Now iOS and iPadOS 27 can provide this same information in a new declarative management status item, for device system health.\n\nThis includes hardware components such as the baseband, camera, Face ID, Touch ID, and more.\n\nThis gives IT administrators a comprehensive view of device health across their entire fleet, so they can take proactive action and keep users productive.\n\nAnother new feature for device management is one that streamlines the process of collecting and submitting logs to AppleCare for analysis.\n\nToday, AppleCare support staff have a way of providing customers with a link that triggers an enhanced log collection process on the device. In iOS, iPadOS, tvOS and macOS 27 releases, IT administrators can now start enhanced log collection on organization-owned devices.\n\nThat is done by using the new TriggerEnhancedLogCollection command.\n\nThus IT teams can facilitate AppleCare collecting these vital logs when needed.\n\nAnd declarative status is available to help IT teams monitor this process.\n\nAnother area with expanded status is Content Caching. Content Caching reduces bandwidth usage and speeds up installation of software updates, applications, Apple Intelligence, and other content on Apple devices, by storing those items on Mac computers hosted on the local network.\n\nContent caching servers can be scaled to support large organizations with wide-spread networks. In macOS 27, there's now a declarative configuration to control the Content Caching service on a Mac, and new declarative status items to report on the state of the service. This gives IT administrators a direct way to monitor the health of their content caching server fleet. Also, content cache servers have a new feature that allows them to directly send their own reports to an arbitrary HTTPs endpoint.\n\nThis allows for more sophisticated monitoring consoles to be built to help IT administrators. All these new status items provide device management services with even more ways to report useful, and critical information, to IT administrators and support staff. Remember, adding support for declarative status items, is a simple matter of subscribing to the items, and the device then sends any changes to the status values to your server as they occur.\n\nAnother important area of device management is managing and configuring apps. This is one of the most important aspects of device management. So now I'll describe the changes in this release for app management. First, the declarative app configuration feature available in iOS, iPadOS, and visionOS is now coming to macOS 27. This allows for secure provisioning of managed apps with credentials and configuration, including the ability to use hardware-bound keys and to enable Managed Device Attestation support, for authenticating apps and extensions with enterprise services.\n\nThis opens the door to more secure enterprise app deployment and configuration on macOS. Please encourage your enterprise app developers to adopt the ManagedApp framework in their products, to help keep your organization operating smoothly and securely.\n\nNext, packages. macOS 27 now gives IT administrators the option to remove all the files and directories that are installed by a declarative management package, when the package configuration itself is removed.\n\nThis ensures unwanted data and files are not left behind on devices when no longer needed.\n\nNow I want to cover privacy settings.\n\nDisclosure and consent is a key element of Apple's approach to privacy. This means users are presented with prompts, when apps or websites in Safari, try to access features like the camera, microphone, or location. For some workers this means having to tap though multiple prompts for the apps they use everyday. These prompts may be quickly dismissed by users, resulting in improperly configured apps. To streamline this process, in iOS, iPadOS and macOS 27, there is a new consolidated privacy consent prompt, which is shown when an app is first launched, or a website first appears in Safari. Let's examine how this works for apps. The prompt shows the name of the organization and app. A justification string provided by the IT administrator, and details of each component whose privacy default is being recommended, along with the app's own justification for each one. This gives the user a clear picture of what is being asked for, and why, and by whom.\n\nThere are two buttons in the prompt. If the user chooses Allow, then the defaults are applied to the privacy settings, and no additional prompts appear as they use the app. If the user chooses Not Now, then consent prompts appear when they use the app and it accesses the components, just like the unmanaged state.\n\nImportantly, the Allow button is the default button, and is clearly highlighted to steer the user towards making the right choice. The same prompt for apps also applies to websites asking for privacy permissions in Safari. The same elements are present, and again the default button is clearly shown. Here are the privacy components that can be managed for both apps and websites. IT administrators determine which of these a user needs access to in their apps, or on websites, and then they create a declarative configuration that lists the app or website, together with the selected components.\n\nThese new controls preserve user privacy, while eliminating multiple prompts. It also gives IT administrators the comfort in knowing that users are now more likely to make the right choice.\n\nNow let's turn our attention to a critical part of managing apps on macOS: the ability to control which apps, and other binaries are running.\n\nMac computers contain a collection of apps from the App Store, and other binaries and executables likely installed from outside the App Store. Many of these are managed, but there are many more that are installed intentionally or unintentionally by the user, but those don't always comply with the organization's requirements.\n\nOrganizations need control over allowed binaries to meet compliance regulations. So, in macOS 27, new declarative management settings are available to control binary execution. This uses the Endpoint Security framework to allow or deny binary execution, and to shut down any processes associated with a binary that has been denied.\n\nThere are flexible rules to match binaries, which utilize code signing properties, to ensure the matched binaries are indeed the ones the IT administrator wants to control.\n\nThere is also an option to automatically allow any managed app, without having to add specific rules for each one.\n\nThe new app privacy controls and binary blocking restrictions are part of a new declarative app.settings configuration. And the Safari website permissions are part of the existing declarative safari.settings configuration.\n\nThese are great new capabilities for controlling apps, binaries, privacy prompts and more.\n\nNow I'll cover another important enterprise feature for Mac computers: the use of platform single sign-on to integrate with identity providers.\n\nThe last few releases have significantly improved and enhanced Platform SSO on macOS, to simplify setup and better support shared workflows. All of it grounded with the goals of making login more intuitive, highly secure, and providing more phishing resistant ways for organizations to keep their users and their data on Mac computers safe.\n\nAnd we are taking Platform SSO even further with macOS 27, starting with a new login and unlock experience. Right from the start it's clear to the user they are using their organization's credentials. Users can enter their password or use Touch ID, as they do today.\n\nTouch ID is the most secure and convenient way for users to unlock their Mac, but until now it's been optional. New in macOS 27 is the ability for IT administrators to require users to use Touch ID, in addition to entering their password on organization devices, offering a built-in second factor. This is enforced when logging in, at screen unlock, and even for the FileVault unlock process.\n\nModern authentication standards allow for many different ways to authenticate users, offering security features to prevent phishing attacks, as well as adapt the authentication interface to match organization branding, security policies, and user demographics. This includes one-time codes for multi-factor authentication.\n\nPush notifications for conditional access workflows.\n\nAnd QR codes for a password-free sign-in, designed for young learners and shared-device environments like healthcare, retail, and logistics. And there are lots more. To support these, macOS 27 introduces a new web-based authentication option for Platform SSO. Identity providers and organizations now have the ability to use a secure web view that renders in the login window and screen unlock. This can run any modern authentication flow including custom challenge-response sequences. The web view operates within a tightly controlled execution context managed by the operating system, to ensure organizations and users are protected. Also, the web view can scan a QR code. When a scan is initiated, the camera operates entirely within a secure system process, completely isolated from the web view itself. The web page only receives the decoded data from the QR code, never the raw camera feed or any image data.\n\nThis ensures that websites can't capture images of the user or their surroundings, even inadvertently. Web authentication works across the login window, screen unlock, and the FileVault unlock process. And provides secure and enforceable authentication. Offline authentication is also supported, to ensure continuity of access without weakening the security posture of unconnected devices. For enterprises, this opens up deep customization such as: localized sign-in pages, accessibility-optimized flows, conditional prompting based on device state, and seamless integration with existing identity infrastructure. Developers like Authentik, ClassLink, and Identity Automation, are working to enable the new web-login and QR code support for Platform SSO in their products.\n\nNow let's cover Authenticated Guest Mode with Platform SSO. It allows users, such as as a nurse or doctor going from room to room, to quickly and securely login to a shared Mac in a temporary session. macOS 27 now extends this capability to allow an authenticated guest user, to also sign in on FileVault protected Mac computers, to unlock FileVault itself. So full disk encryption is now available to protect the data on the device, as the authenticated guest user uses the device, ensuring compliance with data protection regulations. This functionality is automatically available on devices configured for Authenticated Guest Mode, and doesn't require additional configuration.\n\nAuthentication shouldn't be a barrier, it should be a bridge. And the best sign-in is the one users don't even have to think about.\n\nThese new identity and login capabilities give organizations the flexibility to design experiences that feel effortless for users, and the confidence that security is assured.\n\nFinally, a quick look at some updates to our education offerings.\n\nI just covered Authenticated Guest Mode for macOS, and now I can share that it is also coming to Shared iPad later in this release. When enabled, iPad boots into a temporary session and presents a login screen, where users sign in with their Managed Apple Account. The sign-in can use native authentication or federated authentication with an identity provider, with full support for Single Sign-On. In the temporary session, users see their user name in the top-left corner of the screen. Also, the temporary session shares device capacity with the system, with no hard quotas, making use of storage much more flexible. When they sign out from the lock screen, all local data and the Managed Apple Account are automatically removed from the device. This is a great addition to Shared iPad that gives you even more ways to deploy it.\n\nDevices such as iPads and Mac computers have become the norm for classroom learning, but it's often hard for teachers to keep students focused on the task at hand during class. I am pleased to announce a new guided browsing feature in the Classroom app. Teachers can lock the websites that students can interact with, to one or more tabs using Classroom app. And they can lock students to a single tab for an immediate focal point. Teachers can configure what websites to use in this mode, by directly entering them, or by using bookmarks they prepared while planning the class work. They can limit students ability to navigate inside or outside of websites. And they can grant access to camera and microphone and students have agency over whether they remain enabled. They can navigate one student or many to the set of chosen web sites. The student devices open the guided browser and show the appropriate websites. Taken together, these capabilities help address a key problem in classrooms today.\n\nSo, that's a quick overview of some of the exciting new features, available in Apple devices and platforms in this release. All the device management object schema and documentation are available for you right now on the open source GitHub site, and developer.apple.com.\n\nAlso check out the \"App Attest\" video, which provides details on new ways to securely identify enterprise apps.\n\nAnd finally there's the \"Assessment mode\" video for all the new features available to assessment mode app vendors.\n\nThe standard for device management is declarative management, and with great new Mac hardware and powerful mobile devices, combined with the great new device management features in this release, you can make it your standard. You can provide your users with the best in class experience everyone expects from Apple devices. Now's the time for device management vendors, and identity providers, to build support for these features, so IT administrators can deliver them to users without delay. Thank you and enjoy the rest of your WWDC.",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/206/4/e49f983e-700d-4d52-ae6b-a0fa1ea89fd0/downloads/wwdc2026-206_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/206/4/e49f983e-700d-4d52-ae6b-a0fa1ea89fd0/downloads/wwdc2026-206_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "391",
+ "year": "2026",
+ "title": "Offer subscriptions to groups and organizations",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/391"
+ },
+ {
+ "id": "201",
+ "year": "2026",
+ "title": "Secure your apps with App Attest",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/201"
+ },
+ {
+ "id": "230",
+ "year": "2026",
+ "title": "What’s new in assessment on macOS",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/230"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:11.520Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-207.json b/data/wwdc/videos/2026-207.json
new file mode 100644
index 0000000..ac9aaf9
--- /dev/null
+++ b/data/wwdc/videos/2026-207.json
@@ -0,0 +1,77 @@
+{
+ "id": "207",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/207/",
+ "title": "Deliver workout insights with HealthKit workout zones",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Health & Fitness",
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hello! My name is Seth and I am an engineer on the HealthKit team.\n\nThere are many health and fitness apps in the App Store that help people plan, track, and visualize their fitness goals. When people grant your app permission, your app can leverage HealthKit's centralized, secure database and powerful APIs to create workouts and access the underlying health data.\n\nWorkout zones can leverage health data, to help people train smarter by tracking time spent at specific intensity levels. Heart rate zones are a training resource that's personalized to an individual. They are calculated by considering one's age and resting heart rate.\n\nTypically, people use 5 heart rate zones to track their effort level in a workout.\n\nEach heart rate sample, like 135 beats per minute, falls into the boundaries of a particular zone, like Zone 3, which indicates the intensity level.\n\nThese ranges can be a guide to plan or track exertion levels in activities like running, cycling, high intensity interval training and rowing.\n\nSimilarly, cycling power zones, help cycling enthusiasts measure their power output in watts, based on a personalized functional threshold power.\n\nIn iOS 27 and watchOS 27, heart rate and cycling power zones support has been integrated into HealthKit.\n\nI have a sample app that can create, track, and provide a summary for workout sessions. Throughout this session, I'll show you how you can incorporate workout zones into your app, using this sample app as a guide. In this session, I will demonstrate adding heart rate zone support to the sample app. When adopting cycling power zones in your app, you will find they follow a similar structure.\n\nIn this session, I'll go over - accessing zone data from completed workouts, registering for live zone change updates during a workout session, accessing people's preferred zone configurations, and how to provide custom zone configurations for workouts in your app. Workout zones turn raw data, like heart rate samples, into actionable training guidance. Many workout plans have intensity goals for each workout. For endurance training, a person might want to stay below a certain threshold. However, an interval training exercise might require a person to stay at, or above a specific level, for a period of time.\n\nZones also help with recovery and load balance. For instance, a workout that is largely spent in higher zones, like zone 4, or zone 5, suggests a hard effort. You can use this information, to offer guidance, or classify the intensity of a workout.\n\nWith workout zones now integrated into HealthKit, people can share heart rate and cycling power zone information, directly with your app. The time in each zone is automatically calculated by HealthKit, based on incoming samples during the workout.\n\nWorkout zones, follow a similar authorization flow as other HealthKit data types. Before accessing workout zone data, request HealthKit authorization for the relevant quantity types. In this case workouts, heart rate, and cycling power.\n\nLet's look at how you can retrieve heart rate zones from a completed workout in HealthKit.\n\nMy app currently tracks live workouts and displays a summary, once the workout completes. To help people visualize their intensity and effort, I now want to display the total time spent, in each heart rate zone for the completed workout. I can use the HealthKit APIs, to help graph out the time in each heart rate zone in my app's summary view.\n\nTo retrieve zone data after a workout, access the zoneGroupsByType dictionary, on either the HKWorkout or an individual HKWorkoutActivity, passing the appropriate HKQuantityType. Here I want heart rate zones, so I will supply the heart rate quantity type. In iOS 27 and watchOS 27, HealthKit supports workout zones for heart rate and cycling power, and they share a similar structure. To receive cycling power zones, just update the associated quantity type.\n\nIf heart rate zones are available, this will return an HKWorkoutZoneGroup structure. Let's take a look, at what all this contains.\n\nHKWorkoutZoneGroup contains two properties, a Configuration and an array of zone durations.\n\nThe HKWorkoutZoneConfiguration, describes the set of zones and how they were created. It contains the HKQuantityType for the zones. In this case, it's heart rate.\n\nThe source is an enum that tells us how the workout zone thresholds were configured. Whether they were created automatically by the System. If they were set manually, by the User in settings. Or if they were custom supplied by an App at the time of the workout. I will go over the Source, in more detail, later in the session.\n\nThe Configuration also contains an array of zones ordered by the zones' boundaries. Each zone contains an index and a minimum and maximum HKQuantity. The first zone has no lower bound and the last has no upper bound, ensuring the full range of values is always covered. Zones are guaranteed to be contiguous and non-overlapping.\n\nHKWorkoutZoneGroup, also contains an array of zone durations.\n\nThis is an array, where each element contains the zone and the time spent in each zone, ordered by the zone threshold values.\n\nI can use these zone durations to populate a graph in my app.\n\nWhen the person ends the workout in my app, I can chart the time spent in each heart rate zone.\n\nWorkout zones are available on HKWorkout and HKWorkoutActivity. This means your app can display zones for the entire duration of the workout all at once, or, in the case of multi-sport workouts, break them up by individual activities.\n\nSome workout plans require a person to stay in or below specific zones. In these cases it would be great if my app could display the current heart rate zone, and notify the person if it changes, allowing them to adjust their intensity level, in order to stay in their target zone. I can use the live workout zone updates to handle zone changes in my app.\n\nDuring a live workout, HealthKit receives incoming heart rate samples. HealthKit processes each heart rate sample to identify the heart rate zone. When there's a change, like from Zone 2 to Zone 3, HealthKit sends a notification to your app, as the samples are processed.\n\nHKLiveWorkoutBuilderDelegate is the protocol apps use to receive updates about a live workout. The workout is tracked by HealthKit. When something important happens, like a new activity begins, or a data type that I'm tracking updates, HealthKit passes that update to my delegate, and I can make changes in my app, like update the UI. To process changes in heart rate zone, I'll use the didUpdateWorkoutZone method. Each update will include the Current zone and Previous zone. Updates are only sent when the Current zone changes, like from Zone 2 to Zone 3, as well as the zone group, containing the entire zone configuration and the current total time in each zone.\n\nFinally, it includes a timestamp for the last sample processed. This is helpful to display a running timer of the time in the current zone.\n\nOnce my delegate processes the zone update, I make changes in my app, to highlight the new current zone.\n\nI'll adopt this in my app, so I can handle zone changes within a workout.\n\nIn my app, I can now highlight the person's current zone and notify them, if their current zone changes.\n\nBy default, HealthKit uses the preferred workout zone thresholds, configured in Health Settings. With preferred zones, people receive a consistent experience across apps and devices, since these zones sync across devices via HealthKit.\n\nPreferred zones include those calculated by the system. These zones are periodically calculated, based on user metrics, if available. For instance, heart rate zones are automatically calculated, based on factors such as the person's age and resting heart rate.\n\nPreferred zones can also be manually configured in Health Settings.\n\nBefore starting a zone workout, make sure the person has a preferred zone configuration set. You can query for the preferred zone configurations on either the HKHealthStore, or the HKWorkoutBuilder. Custom zones are the right choice, when your app has specific zone definitions that differ from zone preferences in Health Settings, such as a training platform with a proprietary zone model. I can use these two concepts in my app, to provide a custom set of heart rate zones, if they have not been configured directly.\n\nFirst, I can check if a preferred heartRate zoneConfiguration has been set. If not, I can use an array of zone thresholds, and the associated HKUnit to create an array of HKQuantity zone boundaries.\n\nI can use the boundaries and the heartRate quantityType, to create the default HKWorkoutZoneConfiguration. The zone boundary units must match and be compatible with the zone configurations quantity type. HealthKit creates zones based on the provided boundaries. The first zone starts at 0 and the last is unbounded. Between 3 and 9 zones are required.\n\nNext, I will provide the custom configuration to the HKWorkoutBuilder. Custom zones must be added to the builder before calling beginCollection in your app.\n\nThere are a few important things to keep in mind, when using custom zone configurations. Custom zone configurations are only saved within the context of the workout. Your app is responsible for saving, and syncing custom zone configurations, if needed.\n\nWorkout zone configurations can contain varying thresholds and numbers of zones. This is common for cycling power zones. For instance, the system defaults to 6 zones, but some training apps use 5 and others use 7 or 8. This is important to keep in mind if your app compares efforts with different numbers of zones.\n\nIf your app compares time-in-zone across multiple workouts, with different amounts of zones, this zone information, can immediately be compared. For instance, zone 3 in a 5-zone workout, may reflect different values than zone 3 in a 7-zone workout. Each zone represents a different range of values.\n\nInstead, make sure to normalize zones, based on each workout's number of zones and their boundaries. Do this by taking the original samples on the workout, then sorting them into the appropriate number of buckets for your app. In this case, 7! Workout zones enable richer, more actionable fitness apps. Whether your building a postworkout summary screen, a live coaching experience, or a long-term training dashboard, HealthKit offers a simplified interface for your app to access this data.\n\nTo get started. Adopt the workout zones API in your app. You can use the provided sample app as a guide. Chart or graph zone data in your app, so people can visualize their workout effort. And handle live zone changes, to keep people informed, as their intensity levels adjusts throughout a workout.\n\nI love using your apps to help me pursue my fitness goals. Thank you for being a part of the developer community and empowering people to take charge of their health. Thank you for watching.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:54",
+ "title": "Reading Heart Rate Zones from a completed workout",
+ "language": "swift",
+ "code": "// Read heart rate zones from the completed workout\n\nif let heartRateZoneGroup = workout.zoneGroupsByType?[HKQuantityType(.heartRate)] {\nlet zones = ZoneDisplayData(\n zoneCount: heartRateZoneGroup.configuration.zones.count,\n currentZoneIndex: nil,\n durations: heartRateZoneGroup.zoneDurations.map(\\.duration)\n)"
+ },
+ {
+ "timestamp": "7:57",
+ "title": "Handling Live Zone Updates",
+ "language": "swift",
+ "code": "func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder,\n didUpdateWorkoutZone zoneUpdate: HKLiveWorkoutZoneUpdate) {\n guard let zoneGroup = zoneUpdate.zoneGroup else {\n return\n }\n if let currentIndex = zoneUpdate.currentZoneDuration?.zone.index {\n let data = ZoneDisplayData(\n zoneCount: zoneGroup.configuration.zones.count,\n currentZoneIndex: currentIndex,\n durations: zoneGroup.zoneDurations.map(\\.duration)\n )\n Task { @MainActor in\n self.heartRateZones = data\n }\n }\n}"
+ },
+ {
+ "timestamp": "9:19",
+ "title": "Check if Preferred Zone has been set",
+ "language": "swift",
+ "code": "if try await builder.zoneConfiguration(for: HKQuantityType(.heartRate)) == nil {"
+ },
+ {
+ "timestamp": "9:24",
+ "title": "Create Zone Boundaries",
+ "language": "swift",
+ "code": "let defaultHeartRateZoneThresholds = [91.0, 114.0, 136.0, 158.0]\n let bpmUnit = HKUnit.count().unitDivided(by: HKUnit.minute())\n let boundaries = defaultHeartRateZoneThresholds.map(\n {HKQuantity(unit: bpmUnit, doubleValue:$0)}\n )"
+ },
+ {
+ "timestamp": "9:33",
+ "title": "Create Default Workout Zone Configuration",
+ "language": "swift",
+ "code": "let heartRate = HKQuantityType(.heartRate)\n let defaultConfiguration = try HKWorkoutZoneConfiguration(quantityType: heartRate,\n zoneBoundaries: boundaries)"
+ },
+ {
+ "timestamp": "9:58",
+ "title": "Set Custom Zone Configuration",
+ "language": "swift",
+ "code": "try await builder.setCustomZoneConfiguration(defaultConfiguration,\n for: heartRate)\n}"
+ },
+ {
+ "timestamp": "10:03",
+ "title": "Begin Data Collection",
+ "language": "swift",
+ "code": "// Begin data collection\nlet startDate = Date()\ntry await builder.beginCollection(at: startDate)"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Tracking heart rate zones for workouts",
+ "url": "https://developer.apple.com/documentation/HealthKit/tracking-heart-rate-zones-for-workouts"
+ },
+ {
+ "title": "Accessing workout zone data",
+ "url": "https://developer.apple.com/documentation/HealthKit/accessing-workout-zone-data"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/207/5/8627c1d4-7a34-46f2-8491-f0d1c138edd1/downloads/wwdc2026-207_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/207/5/8627c1d4-7a34-46f2-8491-f0d1c138edd1/downloads/wwdc2026-207_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:11.832Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-209.json b/data/wwdc/videos/2026-209.json
new file mode 100644
index 0000000..52b18a9
--- /dev/null
+++ b/data/wwdc/videos/2026-209.json
@@ -0,0 +1,115 @@
+{
+ "id": "209",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/209/",
+ "title": "What’s new in Wallet",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "System Services"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Shaun. And welcome to \"What's New in Wallet\". Since their introduction, passes have become an essential part of how people move through their day. From picking up a morning coffee, to tapping through transit gates, to checking in and boarding a flight, all without ever handing over a physical card. Passes help people move through the world faster, more privately, and more securely. In iOS 27, we're taking passes further than ever. iOS 27 introduces a stunning new pass style called Poster Generic. Four new barcode types, giving you more flexibility at the point of presentation. And a powerful new way for passes to surface relevant actions, right below the pass face.\n\nAnd we're also introducing a brand new suite of developer tools for Mac and server platforms, making it easier than ever to design, personalize, and build great passes.\n\nWe have a lot to cover, so let's dive straight in with the brand new pass style, Poster Generic.\n\nPoster Generic is a great fit for membership cards, loyalty programs, store cards, and anywhere you want bold, colorful artwork to take center stage.\n\nThe pass face consists of a background image, a primary logo, header fields, primary fields, a footer field, and lastly, a barcode if supplied. To adopt this pass style, in the pass.json of your pass bundle, specify the posterGeneric top-level style key. Then add your content with the usual pass fields structure, and the pass will take care of laying everything out across the pass face.\n\nOne thing to keep in mind, is that the pass face supports a single footer field, so if you include more than one, only the first will be displayed.\n\nPoster Generic requires iOS 27 or later.\n\nTo support customers on iOS 26 and earlier, we recommend including the existing generic top-level style key, alongside the posterGeneric style key in your pass.json, with relevant fields under each.\n\nThat way, customers who have not yet made the update to iOS 27, will still be able to add your pass to Wallet. Next, in iOS 27, passes support four new barcode types. EAN-13, Code 39, Codabar, and ITF.\n\nThese are specified using the existing barcode object and barcodes array in your pass.json.\n\nFor example, to present your barcode in Codabar format, set the format of your barcode to PKBarcodeFormatCodabar.\n\nYou can find the full list of format definitions in the Wallet Passes documentation. Because previous versions of iOS do not support these new formats, we strongly recommend providing an array of barcodes in priority order. By providing multiple barcodes, you give the system the best chance of rendering a barcode your hardware can scan, regardless of which operating system version your customer is on.\n\nIf you only provide one of the new barcode types and nothing else, the pass will not render a barcode on iOS 26 and earlier.\n\nSo we recommend leading with your preferred format, and falling back gracefully.\n\nNow we do recognize that there are some circumstances, where supporting multiple barcode types simply isn't an option. If that's your situation, there are two things you should do. First, surface the credential ID in a pass field so it can be entered manually, and make it easy to find. Consider using a primaryField, or headerField, so it's prominent on the pass face.\n\nSecond, make sure your front-line staff are trained for manual entry workflows. A pass that can't be scanned, shouldn't result in a blocked customer. Getting this right, can mean the difference between a seamless experience and a frustrating one. Next, lets talk about featured actions.\n\nWith the second generation event ticket introduced in iOS 18 you can provide semantic URLs to expose additional user actions drawn below the pass, such as viewing the event schedule. In iOS 27, there's a new flexible API, that allows you to provide actions for all pass styles. In the top level of your pass.json, define the featuredActions key, which takes an array of action objects. Each action is defined as a unique ID, the action type, and a value, such as a URL. For example, to provide an action to view offers, I'd set the identifier to a unique ID, the action type to membershipBenefits, and a URL to navigate the user. Wallet draws this below the pass, with an appropriate colorful icon and a localized call-to-action.\n\nEach pass can contain up to two featured actions. We recommend providing only the most meaningful and relevant actions to your customers, and provide your actions in priority order.\n\nYou can find the full list of supported actions types and their expected values in the Wallet Passes documentation. Now, passes have gained a lot of features and functionality over the last decade. And it can be hard to connect what is described in the pass bundle, to the visual representation presented on device.\n\nSo we're excited to announce a brand new app, for Mac, that makes designing passes easier than ever. And it's called Pass Designer.\n\nPass Designer is a what-you-see-is-what-you-get editor, giving you a true-to-iOS rendering of your pass as you build it.\n\nPass Designer creates template files. To convert a template into a personalized, signed pass ready for distribution, alongside Pass Designer we're also announcing a new Swift on Server package called Pass Builder. Pass Builder runs on Mac and Linux. It provides a Swift API, and a command-line executable, called buildpass.\n\nMy friend Stacey runs a doggy day care, and I want to help her create membership cards for each of her fluffy clients. I'm going to use Pass Designer to design a template, and then use Pass Builder on Server to personalize the template, and distribute passes at scale.\n\nLet's jump into Pass Designer.\n\nAnd here we go. A live preview of the pass on the right, and a sidebar to edit the pass on the left.\n\nThe sidebar allows you to configure the pass Identity & Signing configuration, the pass style, images, barcodes, fields, and semantics, if the pass style supports it. So this is a bit of a blank canvas. But I know Stacey takes some gorgeous portrait photos of each dog, so lets experiment with the new Poster Generic style and see how those photos look. So I'll go into Style, and change the pass style to Poster Generic.\n\nAnd then, I'm gonna navigate to Images in the sidebar and drag in a sample photo, Stacey has given me from my Desktop.\n\nThat looks like it fits quite nicely. Now lets add some fields.\n\nI'll navigate to Header Fields in the sidebar, and add a new header field.\n\nI'm going to set the key to DOG_ID, the label to Member ID, and add a placeholder ID to see how it looks. Great. Now lets add some primary fields for dog name, and their favorite toy. In the sidebar, I'm going to navigate to Primary Fields, and add a primary field for name.\n\nI'll set the key to DOG_NAME.\n\nNow here's a neat trick with the Poster Generic pass face, if I omit the label on the first primary field, we'll get the cool bold title for the field value.\n\nSo I'm going to type in a placeholder name of Finley. And add a primary field for favorite toy. So again, I'll navigate to Primary Fields, and add a new field.\n\nI'll set the key to LOVES, the label to Loves, and add a placeholder value of Flying Disc. And now lets make it super easy for Stacey to check each dog in and out of daycare, by encoding their member ID, as a barcode.\n\nI'm going to head over to Barcode & NFC.\n\nAnd write in a placeholder barcode message. This works, but I don't think I need to use that much space for a QR code. Lets try a narrow barcode type instead, such as PDF417.\n\nI'm going to click on the Format picker, and select PDF417. That looks much better. Now lets add some finishing touches, by adding a Primary Logo.\n\nI'll navigate to Images, and drag in the Primary Logo from my Desktop.\n\nAnd adjusting the colors by navigating to Style, and setting the Label Color. And the final finishing touch, adding \"Stacey's Doggy Day Care\" to the footer.\n\nI'll navigate to Footer Fields, and add a new field.\n\nAnd set the text value to Stacey's Doggy Day Care.\n\nNow that's a cute pass, for a very cute dog.\n\nThe very last thing to do, is to save our template.\n\nPass Designer saves templates as pkpasstemplate files.\n\nI'm going to go to File, then Save, and then save the template.\n\nNow, I don't want to create an individual pass for every member of Stacey's Doggy Day Care in Pass Designer. Instead, I want to personalize this template at scale, on a server, using Pass Builder. I'm going to switch over to the Swift server, for Stacey's Doggy Day Care. First, I'm going to add PassBuilder, as a dependency, to the Swift package manifest, and to the server target.\n\nNow I'm going to implement this function createPass in the server source, to generate a personalized pass. The function createPass receives a model already loaded from the database, with the dog's name, membership ID, their favorite toy, and a URL to a gorgeous photo of each dog. Let's get started by first loading the template I made earlier, using the PassPackage type. PassPackage provides a type-safe way to access and configure the pass bundle, including a pass property to interact with the contents of the pass.json. Let's personalize the fields.\n\nI'll call setValue on the pass fields property to set the dog's name, their membership ID and their favorite toy.\n\nNow let's set the background image to the photo of the dog.\n\nI'm going create an instance of PassImage, passing in a URL to the photo of the dog, and assigning it to the background image property on the pass package type. And lets configure the barcode, so dogs can be quickly checked in and out of day care.\n\nI'll create a Pass.Barcode, with the encoded message being the dog's membership ID, and using the PDF417 format.\n\nAnd lastly, I think it'd be great to have a featured action for clients to view their membership, so lets add that by setting a Pass.Action on featuredActions. And just like that the server is generating a personalized pass. Now, we need to build our pass and sign it for distribution.\n\nAs a reminder, to build and sign a pass for Wallet, you need to generate and write a manifest of the contents of your pass, into your pass bundle. Then create a detached signature of the manifest, and write the signature into your pass bundle. Then compress the resulting directory and add the .pkpass file extension. Pass Builder takes care of all of that for you. You just need to provide the certificates. Going back to the server source. Using the PassCertificate type, I'm going to load the pass signing certificate, and the WWDR intermediate certificate. Next, I'm going to create an instance of PassSigner, passing in the certificates. And finally, I'm going to call the signPass function on PassSigner, passing in the personalized package I made earlier, and the URL on disk, to write the signed pass for distribution. And then return the URL.\n\nAnd now Stacey's doggy day care has gorgeous Wallet Passes for every client.\n\nPass Builder can also be used from other programming languages. The swift-java project can generate native Java bindings for the Swift API, allowing you to invoke Pass Builder from the Java runtime. We're also making protobuf definitions of the pass package format available, allowing you to generate type-safe models in your preferred programming language. You can then generate a customization message, and invoke the buildpass command line executable, to personalize and sign your pass. For more information on using swift-java, check out the \"Explore Swift and Java interoperability\" developer session. And for documentation on the buildpass command line, see the Pass Builder developer documentation. Both of which are linked in the session description.\n\nAnd that's a brief end-to-end demo of Pass Designer and Pass Builder. From designing a template, to personalizing it, to signing it for distribution.\n\nYou can find links to download Pass Designer, the Pass Builder source, and comprehensive documentation in the session description.\n\nI've covered a lot today, so for next steps. Check out Pass Designer. Use Pass Designer to experiment with the new Poster Generic style, and check if it's the right fit for your pass.\n\nIf you plan on adopting any of the new barcode types, make a plan for providing graceful fallbacks.\n\nAnd take a moment to identify the most meaningful and relevant actions for your customers, then bring them to life, with featured actions. This is a new chapter for building Wallet Passes, and we're so excited to see all the amazing passes you're going to create. Thanks for watching, and have a wonderful WWDC.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "1:41",
+ "title": "Adopting Poster Generic",
+ "language": "swift",
+ "code": "// Adopting Poster Generic\n\"posterGeneric\": {\n \"headerFields\": [\n {\n \"key\": \"memberID\",\n \"label\": \"Guest No.\",\n \"value\": \"102035\"\n }\n ],\n \"footerFields\": [\n {\n \"key\": \"membershipType\",\n \"value\": \"Family Pass\"\n }\n ]\n}"
+ },
+ {
+ "timestamp": "2:11",
+ "title": "Adopting Poster Generic with Generic fallback",
+ "language": "swift",
+ "code": "// Adopting Poster Generic and supporting Generic on iOS 26 and earlier\n\"posterGeneric\": {\n \"headerFields\": [\n {\n \"key\": \"memberID\",\n \"label\": \"Guest No.\",\n \"value\": \"102035\"\n }\n ],\n \"footerFields\": [\n {\n \"key\": \"membershipType\",\n \"value\": \"Family Pass\"\n }\n ]\n},\n\"generic\": {\n \"headerFields\": [\n {\n \"key\": \"memberID\",\n \"label\": \"Guest No.\",\n \"value\": \"102035\"\n }\n ],\n \"footerFields\": [\n {\n \"key\": \"membershipType\",\n \"value\": \"Family Pass\"\n }\n ]\n}"
+ },
+ {
+ "timestamp": "2:52",
+ "title": "Barcodes: Add new types for iOS 27",
+ "language": "swift",
+ "code": "// Adopting new barcode types\n\"barcodes\": [\n {\n \"format\": \"PKBarcodeFormatCodabar\"\n \"message\": \"…\"\n \"messageEncoding\": \"…\"\n }\n]"
+ },
+ {
+ "timestamp": "3:37",
+ "title": "Barcodes: Supporting iOS 26 and earlier",
+ "language": "swift",
+ "code": "// Adopting new barcode types and supporting iOS 26 and earlier.\n\"barcodes\": [\n {\n \"format\": \"PKBarcodeFormatCodabar\"\n \"message\": \"123456789\"\n \"messageEncoding\": \"iso-8859-1\"\n },\n {\n \"format\": \"PKBarcodeFormatQR\"\n \"message\": \"123456789\"\n \"messageEncoding\": \"iso-8859-1\"\n }\n]"
+ },
+ {
+ "timestamp": "4:48",
+ "title": "Featured actions",
+ "language": "swift",
+ "code": "// Featured actions\n\"featuredActions\": [\n {\n \"identifier\": \"my-offer-id\",\n \"type\": \"membershipBenefits\",\n \"url\": \"www.example.com/offers\"\n }\n]"
+ },
+ {
+ "timestamp": "10:56",
+ "title": "Package.swift",
+ "language": "swift",
+ "code": "// Package.swift\n\nimport PackageDescription\n\nlet package = Package(\n name: \"MyServer\",\n products: [\n .library(\n name: \"MyServer\",\n targets: [\"MyServer\"]\n ),\n ],\n dependencies: [\n .package(path: \"./path/to/PassBuilder\")\n ],\n targets: [\n .target(\n name: \"MyServer\",\n dependencies: [\n .product(name: \"PassBuilder\", package: \"PassBuilder\")\n ]\n ),\n …\n ]"
+ },
+ {
+ "timestamp": "11:05",
+ "title": "CreatePass.swift",
+ "language": "swift",
+ "code": "// CreatePass.swift\n\nimport PassBuilder\n\nfunc createPass(for doggo: MemeberModel) async throws -> URL {\n var package = PassPackage(url: \"template.pkpasstemplate\")\n \n package.pass.fields.setValue(doggo.name, forKey: \"DOG_NAME\")\n package.pass.fields.setValue(doggo.favoriteToy, forKey: \"LOVES\")\n package.pass.fields.setValue(doggo.id, forKey: \"MEMBER_ID\")\n \n package.background = PassImage(url: doggo.photoURL)\n \n package.pass.barcodes = [\n Pass.Barcode(message: doggo.id, format: .pdf417)\n ]\n \n package.featuredActions = [\n Pass.Action(id: \"action-1\", type: \"viewMembership\", url: doggo.membershipURL) \n ]\n …\n}"
+ },
+ {
+ "timestamp": "13:11",
+ "title": "CreatePass.swift",
+ "language": "swift",
+ "code": "// CreatePass.swift\n\nimport PassBuilder\n\nfunc createPass(for doggo: MemeberModel) async throws -> URL {\n var package = PassPackage(url: \"template.pkpasstemplate\")\n \n package.pass.fields.setValue(doggo.name, forKey: \"DOG_NAME\")\n package.pass.fields.setValue(doggo.favoriteToy, forKey: \"LOVES\")\n package.pass.fields.setValue(doggo.id, forKey: \"MEMBER_ID\")\n \n package.background = PassImage(url: doggo.photoURL)\n \n package.pass.barcodes = [\n Pass.Barcode(message: doggo.id, format: .pdf417)\n ]\n \n package.featuredActions = [\n Pass.Action(id: \"action-1\", type: \"viewMembership\", url: doggo.membershipURL) \n ]\n\n let passCertificate = try PassCertificate(url: \"pass.p12\", password: \"s3cr3t\")\n let wwdrCertificate = try PassCertificate(url: \"wwdr.cer\")\n \n let signer = PassSigner(\n passCertificate: passCertificate,\n wwdrCertifiate: wwdrCertificate\n )\n \n let destinationURL = URL(string: \"/www/passes/\" + doggo.id)\n try signer.signPass(package, writingTo: destinationURL)\n \n return destinationURL\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Pass Designer",
+ "url": "https://developer.apple.com/pass-designer"
+ },
+ {
+ "title": "Creating a pass with Pass Designer",
+ "url": "https://developer.apple.comdeveloper.apple.com/documentation/walletpasses/creating-a-pass-with-pass-designer"
+ },
+ {
+ "title": "Pass Builder",
+ "url": "https://github.com/apple/pass-builder"
+ },
+ {
+ "title": "Pass.Barcodes",
+ "url": "https://developer.apple.com/documentation/WalletPasses/Pass/Barcodes-data.dictionary"
+ },
+ {
+ "title": "Learn more about Pass Designer",
+ "url": "https://developer.apple.com/pass-designer/"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/209/5/25eb40d5-b64d-4677-bc99-f5c3a30d386a/downloads/wwdc2026-209_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/209/5/25eb40d5-b64d-4677-bc99-f5c3a30d386a/downloads/wwdc2026-209_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "307",
+ "year": "2025",
+ "title": "Explore Swift and Java interoperability",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/307"
+ },
+ {
+ "id": "202",
+ "year": "2025",
+ "title": "What’s new in Wallet",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/202"
+ },
+ {
+ "id": "10108",
+ "year": "2024",
+ "title": "What’s new in Wallet and Apple Pay",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10108"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:12.706Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-210.json b/data/wwdc/videos/2026-210.json
new file mode 100644
index 0000000..9ea029c
--- /dev/null
+++ b/data/wwdc/videos/2026-210.json
@@ -0,0 +1,120 @@
+{
+ "id": "210",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/210/",
+ "title": "What’s new in Apple In-App Purchase",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing",
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hello, I'm Sam, and I'm an engineer on the StoreKit team.\n\nThe App Store provides a safe and trusted marketplace for people around the world to discover your apps. By leveraging the App Store APIs, you can offer digital goods and services to customers, while letting the App Store commerce platform facilitate transactions and end-to-end payment processing.\n\nI'll cover new features in this session to help you merchandise products and grow your apps business. First, I'll share how to expand pricing options for your subscription products, Then, I'll discuss updates coming to the offer code redemption API, And finally, I'll review the enhanced App Store Connect submission experience coming to In-App Purchases. I'll start with updates to subscription pricing. iOS 26.5 introduced monthly subscriptions with a 12-month commitment. With this new pricing capability, you can offer customers an option to pay monthly for annual subscriptions.\n\nBilling plans can be added to both new and existing, one-year, auto-renewable subscriptions for your app in App Store Connect. By providing customers with the option to pay upfront or monthly, you can reach a larger customer base and allow the customer to choose which billing plan best suits their needs. Once you compile your app against the 26.5 SDK, customers in available markets can subscribe to the new billing plan inside your app on devices running iOS, iPadOS, macOS, tvOS, or visionOS 26.4. I'll show you how it works by setting up a new billing plan for the SKDemo app. This app uses StoreKit to merchandise and sell In-App Purchase products. You can download the sample code project in the Resources for this session to follow along. Here, I have the SKDemo app set up in App Store Connect, and I navigated to the Subscriptions section. I want to offer customers more affordable options for the SKDemo+ subscription, so I'll select a product with a one-year duration.\n\nThen, under monthly with a 12-month commitment availability, I'll choose Set Up Availability. And then follow the steps to configure the billing plan. Once created, subscription offers can be configured for each billing plan type. For example, I added a free trial only for the customers who subscribe to a 12-month commitment.\n\nNow that I have new subscription pricing configured for SKDemo in App Store Connect, I can merchandise the new payment option to customers with StoreKit.\n\nPricingTerms is a new property available on SubscriptionInfo for merchandising billing plan information. The PricingTerms array lists all available billing plans for a given product.\n\nEvery auto-renewable subscription carries at least one billing plan in this array with a default billingPlanType of .upFront. Because I configured a monthly subscription with a 12-month commitment for SKDemo+, a second object is returned in the PricingTerms array. This billing plan uses a billingPlanType of .monthly, which only applies to monthly subscriptions with a 12-month commitment.\n\nTo merchandise pricing terms in my app, I'll use StoreKit views. The StoreKit views APIs handle the work of loading product metadata from the App Store and adjusting layout, so your store automatically adapts to feel at home across all platforms. I'll show you how it works in code. I'll start by adding the new SwiftUI view modifier, .preferredSubscriptionPricingTerms and attach it to my existing SubscriptionStoreView. I'll then filter for the .pricingTerms with a .monthly .billingPlanType.\n\nAnd now, my subscription store is updated and ready to merchandise the monthly subscription with a 12-month commitment. If you want to customize the SubscriptionStoreView further, check out \"Meet StoreKit for SwiftUI\" from WWDC23, and learn how StoreKit views can match your app with custom icons, backgrounds, and any other changes.\n\nTo merchandise the commitment plan in custom store UI in your app, fetch products with the Product API. And filter for .pricingTerms with a .monthly .billingPlanType.\n\nThen, obtain the monthly and totalCommitmentPrice and display in the UI. Note, that billing plan metadata is only returned when available in the customer's storefront.\n\nTo make the purchase, pass the new .billingPlanType purchase option and handle the result.\n\nBefore a customer subscribes to a commitment plan for the first time, new messaging is included with a disclosure sheet. The App Store automatically presents this sheet, and it's displayed once per Apple Account. The sheet includes information about the number of payments required to fulfill the commitment, along with cancellation guidance. When a subscription is active, the App Store also automatically provides new UI for customers to manage their monthly subscription with a 12-month commitment. Here, customers can see all available plans and check the number of remaining payments along with when their billing commitment renews.\n\nYou can present the .manageSubscriptionsSheet directly inside your app using the .manageSubscriptionsSheet API in SwiftUI. Or in UIKit call the showManageSubscriptions API.\n\nTo retrieve the active subscription information in your app, you can use new fields on Transaction that provide billing plan-specific metadata.\n\nFor Transactions with an .upfront billingPlanType, commitmentInfo is nil. For transactions with a .monthly billingPlanType, commitmentInfo returns the progress, price and expiration for the 12-month commitment.\n\nYou can use these fields from the latest transaction to check customer entitlements or build your own commitment progress indicator. Always use the latest transaction to get an accurate expirationDate.\n\nSimilarly, you can retrieve the renewalBillingPlanType and commitmentInfo on RenewalInfo, for information on the renewal of the overall commitment. These new fields are available starting with OS 26.4.\n\nOnce you've added logic to merchandise and unlock content for the new billing plan, you can start testing with StoreKit Testing in Xcode. Starting with Xcode 26.5, open your StoreKit configuration file and select a one-year, auto-renewable subscription. Then, using the new Billing Plan picker, choose Monthly with a 12-month commitment to create a billing plan.\n\nThis creates new fields to configure pricing for the monthly subscription with a 12-month commitment. Similar to App Store Connect, offers can be uniquely created for each billing plan type, here in Xcode.\n\nOnce configured, test your purchase flow with the transaction manager and verify commitmentInfo in the Transaction inspector.\n\nTo learn more about best practices and setting up StoreKit Testing in Xcode, check out \"What's new in Storekit 2 and Storekit Testing in Xcode\" from WWDC23. If your app uses the App Store Server APIs or App Store Server Notifications V2, you can leverage new capabilities for customers who subscribe to a monthly subscription with a 12-month commitment. There are new fields available in the signed transaction and renewal info objects that provide additional information about the commitment plan. Subscription notifications continue to provide updates on the subscription lifecycle, such as monthly renewals, throughout the 12-month commitment.\n\nThe Retention Messaging API also supports this new payment option for auto-renewable subscriptions. Check out our WWDC26 session \"Explore Retention Messaging in App Store Connect\" to learn how this server-to-server API can help with your customer retention strategy.\n\nI'll now show you a transaction payload example from the server for one billing period purchase of a 12-month commitment.\n\nThis decoded JWSTransaction includes new fields that correspond to the ones I covered earlier in StoreKit.\n\nUse these fields to detect billing period purchases and understand where they live in the context of the overall commitment.\n\nThe signed renewal info object contains similar fields which reflect the renewal preferences of the customer after the current commitment ends. Accordingly, they are only present in the renewal info when the subscription is currently in a commitment. In this example, the customer changed their subscription to renew into a different BILLED_UPFRONT plan once their commitment completes.\n\nTo learn more about handling renewals and other events throughout the 12-month commitment, check out the developer documentation for \"Managing the life cycle of monthly subscriptions with a 12-month commitment.\" In addition to new pricing capabilities available on annual subscriptions, I'm excited to share that there is another new feature to expand your subscription offerings. Bundles and Suites. Offering subscription Bundles and Suites is another way you can provide customers with more value in their subscriptions across apps.\n\nLet's review the difference between Bundles and Suites. A Bundle is a group of subscriptions which can be purchased individually but are sold together in a single purchase and offered at a better price than purchasing all of the subscriptions individually.\n\nA Suite is a group of subscriptions which only exist in the context of the Suite. These subscriptions cannot be purchased individually, but together typically provide service to a related set of apps. You can start testing the API for Bundles and Suites in Xcode 27, and more details on this program are coming later in 2026.\n\nShifting gears to offer codes. Offer codes can be added to a consumable, non-consumable, auto-renewable subscription, or non-renewing subscription product and allow you to provide free or discounted In-App Purchases to customers for a specific duration.\n\nTo learn how to configure offer codes in App Store Connect, check out \"Implement App Store Offers\" from WWDC24.\n\nCustomers can redeem offer codes directly within your app, when you present this sheet using the OfferCodeRedemption API. Similar to creating a purchase, the API now returns a verificationResult when the redemption completes. The API also accepts a set of RedeemOption values that configure the code redemption.\n\nIf the redemption succeeds, you receive a transaction object in the verificationResult. If the redemption fails, you receive an error that describes why the redemption failed. A UIKit variation of the updated redemption API is available as well using presentOfferCodeRedeemSheet.\n\nYou can now test redeeming offer codes in Xcode 27 with the new redemption API on all applicable product types.\n\nWhen you're ready to submit an app to the App Store, you can utilize the enhanced submission experience for In-App Purchases in App Store Connect.\n\nThis includes the ability to group multiple products as review items into a single App Review submission.\n\nIn-App Purchase products can now be combined with other review items including in-app events, custom product pages, and product page optimizations.\n\nAfter submission, you can check the status from App Review in a centralized view for all review items. These enhancements help keep your submission workflow organized and consistent across all review item types by unifying the submission process.\n\nI'll go through an example of the enhanced submission flow using the SKDemo app in App Store Connect. When I have an In-App Purchase product ready to submit to App Review, I can add to an in-draft submission with the Add for Review drop-down. Then, I can view all of the review items in a submission by clicking on the in-progress draft.\n\nTo add a large group of products to a submission, I'll go to my list of In-App Purchase products and select each one that I would like reviewed together.\n\nThen, I'll click Add for Review and select the in-progress submission.\n\nIn addition to the App Store Connect website, the enhanced submission experience is coming to the App Store Connect API. reviewSubmissions is expanding to support In-App Purchase, subscription, and subscription group resources.\n\nThe reviewSubmissions API collection will enable you to automate your tasks for all review items through a single interface.\n\nThe existing resources for In-App Purchase, subscription, and subscription group will be deprecated in favor of the reviewSubmission and reviewSubmissionItems resources, so start migrating today.\n\nCheck out \"What's New in App Store Connect\" from WWDC22 for a deep dive on the mechanics of review items and submitting to App Review.\n\nThese new features for In-App Purchases equip you with more ways to grow your customer base and provide great value to customers. Before we conclude, here are some next steps to get you on your way. Start by adding new billing plans to offer customers the flexibility of a monthly subscription while still securing a long-term commitment for your annual subscription products. Update offer code redemption call sites across your app to take advantage of the extended redemption API.\n\nBegin testing these new features today in Xcode 27 and sandbox. When you're ready, submit your updated In-App Purchase products with the enhanced App Review experience.\n\nThank you for joining me and being part of the Apple Developer community. I hope these features will drive your In-App Purchases to reach even more people, so you can focus on creating great apps and games that people will love.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:29",
+ "title": "Merchandise pricing terms with StoreKit views",
+ "language": "swift",
+ "code": "// Merchandise pricing terms with StoreKit views\n\nimport StoreKit\nimport SwiftUI\n\nstruct SubscriptionStore: View {\n var body: some View {\n SubscriptionStoreView(groupID: \"3F19ED53\") {\n // Custom marketing content\n }\n .preferredSubscriptionPricingTerms {_, subscriptionInfo in\n subscriptionInfo.pricingTerms.first {\n $0.billingPlanType == .monthly\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "4:02",
+ "title": "Get subscription pricing terms and make a purchase",
+ "language": "swift",
+ "code": "// Get subscription pricing terms and make a purchase\n\nimport StoreKit\n\nvar product: Product?\n// Fetch and assign product\n\n// Get the monthly billing plan's pricing terms for merchandising\nlet pricingTerms = product?.subscription?.pricingTerms\n .first(where: {$0.billingPlanType == .monthly })\nif let pricingTerms {\n let monthlyPrice = pricingTerms.billingDisplayPrice\n let totalCommitmentPrice = pricingTerms.commitmentInfo.price\n // Display both monthly and total commitment price to the customer\n}\n\nlet result = try? await product?.purchase(options: [.billingPlanType(.monthly)])\nswitch result {\n // Verify the transaction, give the customer access to\n // the purchased content, and then finish the transaction\n}"
+ },
+ {
+ "timestamp": "5:05",
+ "title": "Sheet to manage subscriptions by subscriptionGroupID",
+ "language": "swift",
+ "code": "// Sheet to manage subscriptions by subscriptionGroupID\n\nimport SwiftUI\nimport StoreKit\n\nstruct ManageSubscriptionsButton: View {\n let subscriptionGroupID: String\n @State var presentingManageSubscriptionsSheet: Bool = false\n\n var body: some View {\n Button(\"Manage Subscriptions\") {\n presentingManageSubscriptionsSheet = true\n }\n .manageSubscriptionsSheet(\n isPresented: $presentingManageSubscriptionsSheet,\n subscriptionGroupID: subscriptionGroupID\n )\n }\n}"
+ },
+ {
+ "timestamp": "7:45",
+ "title": "JWSTransaction (decoded) for a monthly subscription with a 12-month commitment",
+ "language": "swift",
+ "code": "// JWSTransaction (decoded) for a monthly subscription with a 12-month commitment\n\n{\n // …\n \"expiresDate\": 1783503660000, // for this billing period\n \"price\": 10990, // for this billing period\n \"productId\": \"plus.pro.annual\",\n \"purchaseDate\": 1780911660000,\n \"type\": \"Auto-Renewable Subscription\",\n \"billingPlanType\": \"MONTHLY\",\n \"commitmentInfo\": {\n \"billingPeriodNumber\": 1,\n \"totalBillingPeriods\": 12,\n \"commitmentExpiresDate\": 1812447660000,\n \"commitmentPrice\": 131880,\n }\n}"
+ },
+ {
+ "timestamp": "7:59",
+ "title": "JWSRenewalInfo (decoded) for a monthly subscription with a 12-month commitment",
+ "language": "swift",
+ "code": "// JWSRenewalInfo (decoded) for a monthly subscription with a 12-month commitment\n\n{\n // … \n \"renewalBillingPlanType\": \"MONTHLY\",\n \"commitmentInfo\": {\n \"commitmentAutoRenewProductId\": “plus.standard.annual”,\n \"commitmentAutoRenewStatus\": 0,\n \"commitmentRenewalDate\": 1812447660000,\n \"commitmentRenewalPrice\": 10990,\n \"commitmentRenewalBillingPlanType\": \"BILLED_UPFRONT\"\n }\n}"
+ },
+ {
+ "timestamp": "9:58",
+ "title": "Sheet to redeem an offer code",
+ "language": "swift",
+ "code": "// Sheet to redeem an offer code\n\nstruct OfferCodeRedemption: View {\n @State var presentingOfferCodeSheet: Bool = false\n\n var body: some View {\n Button(\"Redeem Offer Code\") {\n presentingOfferCodeSheet = true\n }\n .offerCodeRedemption(options: [], isPresented: $presentingOfferCodeSheet) {result in\n switch result {\n case .success(let verificationResult):\n switch verificationResult {\n // Verify the transaction, give the customer access to\n // the purchased content, and then finish the transaction\n }\n case .failure(let error):\n // Handle error\n }\n }\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "In-App Purchase types",
+ "url": "https://developer.apple.com/help/app-store-connect/reference/in-app-purchases-and-subscriptions/in-app-purchase-types"
+ },
+ {
+ "title": "Managing the life cycle of monthly subscriptions with a 12-month commitment",
+ "url": "https://developer.apple.com/documentation/StoreKit/managing-lifecycle-of-monthly-subscriptions-with-a-12-month-commitment-"
+ },
+ {
+ "title": "Supporting monthly subscriptions with a 12-month commitment",
+ "url": "https://developer.apple.com/documentation/StoreKit/supporting-monthly-subscriptions-with-a-12-month-commitment"
+ },
+ {
+ "title": "App Store Server Notifications V2",
+ "url": "https://developer.apple.com/documentation/AppStoreServerNotifications/App-Store-Server-Notifications-V2"
+ },
+ {
+ "title": "Supporting offer codes in your app",
+ "url": "https://developer.apple.com/documentation/StoreKit/supporting-offer-codes-in-your-app"
+ },
+ {
+ "title": "Implementing a store in your app using the StoreKit API",
+ "url": "https://developer.apple.com/documentation/StoreKit/implementing-a-store-in-your-app-using-the-storekit-api"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/210/4/f029ab19-6670-48c6-b9b1-88ac6692cdda/downloads/wwdc2026-210_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/210/4/f029ab19-6670-48c6-b9b1-88ac6692cdda/downloads/wwdc2026-210_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "309",
+ "year": "2026",
+ "title": "Explore Retention Messaging in App Store Connect",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/309"
+ },
+ {
+ "id": "10110",
+ "year": "2024",
+ "title": "Implement App Store Offers",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10110"
+ },
+ {
+ "id": "10013",
+ "year": "2023",
+ "title": "Meet StoreKit for SwiftUI",
+ "url": "https://developer.apple.com/videos/play/wwdc2023/10013"
+ },
+ {
+ "id": "10140",
+ "year": "2023",
+ "title": "What’s new in StoreKit 2 and StoreKit Testing in Xcode",
+ "url": "https://developer.apple.com/videos/play/wwdc2023/10140"
+ },
+ {
+ "id": "10043",
+ "year": "2022",
+ "title": "What's new in App Store Connect",
+ "url": "https://developer.apple.com/videos/play/wwdc2022/10043"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:12.080Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-212.json b/data/wwdc/videos/2026-212.json
new file mode 100644
index 0000000..3e4354f
--- /dev/null
+++ b/data/wwdc/videos/2026-212.json
@@ -0,0 +1,56 @@
+{
+ "id": "212",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/212/",
+ "title": "Rev up your CarPlay app",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "System Services"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Chris, an engineer on the car experience team. I'm delighted to go over what's new in CarPlay for iOS 27. CarPlay is the smarter, safer way to use your iPhone in the car. iOS 27 brings new capabilities to all categories of CarPlay apps so you can rev up your app in CarPlay.\n\nWe'll introduce new app categories supported in CarPlay and cover CarPlay framework updates that apply to all apps. We'll also talk about new features specific to CarPlay navigation apps … and see what's new in the CarPlay Simulator.\n\nCarPlay supports a wide variety of app categories.\n\nAudio apps can play music, podcasts, and more. Communication apps allow people to send and receive messages, and make phone calls. Navigation apps provide turn-by-turn directions. What all CarPlay apps have in common is that they help people accomplish important tasks without taking out their iPhone.\n\nIn addition, CarPlay supports Live Activities and widgets from any app. You can display timely, relevant information even if your app is not specifically designed for CarPlay. This year we added support for voice-based conversational apps … … and now with iOS 27 you can create apps to browse and play videos in new cars that support the video in car feature. If your app supports AirPlay video streaming, no changes are needed. People can already watch videos from your app on their CarPlay display when they aren't driving. To do so, they simply select the car's display when playing the video on iPhone. Now you can take it to the next level and let people browse their favorite videos from iPhone right on their CarPlay display. It's great for those situations where you're sitting in your car waiting for a friend at the airport, parked at a charging station, or just taking a moment in the comfort of your car. CarPlay video apps work in cars that support the video in car feature. CarPlay video apps need to support AirPlay video streaming … … and use the CarPlay framework to provide a browsing experience on the CarPlay display. In the next section we'll review additions to the CarPlay framework that help you create a video browsing UI. But first a few additional notes about video apps.\n\nAt any given time, the car may indicate that video playback is not available. If this happens your video will be played as audio-only. This is great for continuing to listen to a podcast or sports broadcast while driving. Apps with the CarPlay video entitlement only appear on the CarPlay home screen if the car supports the video in car feature. If your app's content is suitable for both video and audio listening, include both CarPlay audio app and CarPlay video app entitlements. With both entitlements your app can always appear in CarPlay. Whether a car supports CarPlay or CarPlay Ultra, you can enable your apps on the driver's screens by supporting the CarPlay framework. From getting directions, sending a message, to finding a parking garage, apps across all categories use the CarPlay framework to present their user interface.\n\nWe have plenty of UI updates for all CarPlay apps, including many enhancements for lists, a new MiniPlayer for now playing, and voice control has a new presentation style.\n\nWhen your app is using the CarPlay framework, iOS manages the display of UI elements and handles the interface with the car.\n\nBy using a rich collection of templates your app does not need to manage the layout of UI elements for different screen resolutions, or support different input hardware such as touchscreens, knobs, or touch pads.\n\nWe have a sample app called Landmarks, which lets people explore interesting sites around the world. For CarPlay, I wanted to listen to narrated audio stories that tell the history and key facts of the landmarks. Using the CarPlay framework, I built a list of the stories that worked great for browsing and then playing the audio stories in my car. But with this new car that supports the video in car feature now I can also watch videos of these amazing landmarks. Landmarks is checking if the CPSessionConfiguration supports video and if so, adds a videos tab.\n\nThis list of videos showcases many of the other new APIs available in CarPlay framework. Let's take a closer look. Lists can now show images with portrait or landscape aspect ratios. Card elements take that even further with thumbnails, which can have overlays, playback progress, and sports information right on the image. Landmarks uses an overlay with a title that describes the video as newly added or live streaming. For custom badges, overlays can also be an image. The CPPlaybackConfiguration API is how your app provides metadata about playable items to CarPlay framework. For content you'd prefer to play as a video, set preferred presentation to video. Otherwise set the preferred presentation to audio. The playback configuration's elapsed time and duration are shown a progress bar for your playable item… … and the playback action indicates if selecting this item will play, pause, or replay this item. Make sure to update the thumbnail's playback configuration on any playback state changes …to keep the thumbnail accurately representing the state of playback for that item.\n\nAn additional overlay is available for showing sports teams and scores. The sports overlay has a left team,... … a right team, … … and event status.\n\nUse a details header when you want to present one item prominently at the top of a list of additional items. This works great for showing the current episode at the top of a list of episodes, or for summarizing a movie with bonus content shown below. The details header combines together a single thumbnail … … with a title … … body text, … … playback configuration, … … and action buttons.\n\nHere the Landmarks app added a thumbnail overlay, configured playback as an unplayed item, and setup action buttons for play and add to playlist. The playback configuration's current progress is automatically combined with the first action button. As with the playback configuration on thumbnails, update the playback configuration in the details header as the playback state changes. This ensures that the state and progress are correct when the details header becomes visible again.\n\nNew in iOS 27 is a MiniPlayer for the now playing template. The MiniPlayer makes it easy to see what's playing and on a larger display you can even play, pause, or skip. All apps that show now playing will automatically show the MiniPlayer. The MiniPlayer is the best option for now playing in CarPlay, but if your app doesn't want to show the MiniPlayer … … set the now playing template's \"allowsMiniPlayer\" property to false, and the now playing icon will appear in the navigation bar instead of the MiniPlayer. Earlier this year we launched support for voice-based conversational apps. If your app has its own voice features, you can respond to questions and perform actions in the car. The Voice Control template presents a UI that shows status and control during voice conversations. Starting in iOS 27, the Voice Control template is available for all CarPlay app categories. The Voice Control template includes a prompt, and an animated icon to indicate the state of the conversation. Both the prompt and the icon are optional. Your app can now add up to two action buttons, plus leading and trailing navigation bar buttons. If your app is used to ask questions about destinations or contact information, action buttons are a good way to offer to start navigation or place a phone call.\n\nTo use URLs to perform those tasks, open the URL with CPTemplateApplicationScene to perform that request in CarPlay.\n\nThe Voice Control template is also available as an overlay. Instead of occupying the entire display, your Voice Control elements can appear overlaid on top of another template, such as the Map Template in a navigation app. Use the CPInterfaceController to show the voice control template as an overlay. Although the overlay supports the same text and buttons as the full voice control presentation, provide shorter text variants to better fit the available space when presented as an overlay. When supporting voice conversations, try using audio feedback to indicate the status of the conversation. Feedback sounds such as waiting sounds while the app is still preparing the conversation and processing sounds when the app is still preparing a response are helpful cues when interacting with an app primarily through speaking. Setup your AVAudioSession with the play and record category, use the default mode, and disable mixing.\n\nIn addition to the new template APIs, we've expanded the availability of existing templates to more categories of apps. For details about app categories and available templates, check the CarPlay Developer Guide.\n\nWe covered a lot of new UI enhancements available for apps in CarPlay. The easiest way for me to see all the improvements I made in Landmarks is right here on my Mac, using CarPlay Simulator. Let's take a look! Here is CarPlay Simulator and I've setup the configuration to represent a vehicle that supports video.\n\nFor vehicles that support video, an app with only the CarPlay video entitlement will appear on the CarPlay home screen. Here's the Landmarks app on the home screen, I'll launch it to browse videos.\n\nOn the top left, there's a tab bar.\n\nI added a new videos tab that only appears when video is supported.\n\nThe MiniPlayer in the top right offers a quick way to resume playback on the Alps video I was recently watching.\n\nI setup wide thumbnails for the landmarks. Since I was already watching the Alps video, the thumbnail's playback configuration shows the remaining time and current progress.\n\nOverlays on the thumbnails provide badging for newly added and live streaming videos.\n\nThe thumbnail for this event has a sports overlay showing the teams and scores. While browsing this list of landmarks, I wanted to know which landmark has the most visitors every year. I added this list row to activate a voice conversation where I can ask that question.\n\nThe voice control template is shown as an overlay, keeping the list behind it visible. But I want to get straight to watching a video.\n\nSelecting a thumbnail pushes into a list template that has a details header. The details header provides this large thumbnail, and additional information about the landmark using the title, subtitle, and body text.\n\nI also added two buttons for play and add to playlist.\n\nThe rest of the list is used to show related videos. Landmarks set the preferred presentation to video in the header's playback configuration. Since video is the preferred presentation and CarPlay Simulator is currently set to allow video playback, tapping play will show the video. I'll go ahead and play this video! The video player has menus to control subtitles and audio language selection, so your videos should include subtitles and additional languages when available. Notifications can appear over the video, so there's also a button to enable the Do Not Disturb focus mode without leaving the video.\n\nWith thumbnail overlays, the MiniPlayer, voice control, and playback configurations, Landmarks built a great interface for discovering and playing videos in the car. Now that we've seen all the new UI features in CarPlay framework, let's change directions to focus on a few more new features for navigation apps. Navigation apps now have more control over the primary interface area of the Map template, and Route sharing coordinates your app's routing with the vehicle. In iOS 27, your navigation app can show panels to create your own UI, independently from the flow of presenting trip and route options. Panels can also include multiple UI elements, providing more flexibility for your navigation UI.\n\nPanels are built by combining together a list of objects you already use in the CarPlay framework, such as trips, grids, route choices, route details, waypoints, and other list items.\n\nThe panel's button configuration sets up actions that will appear on the bottom of the panel, such as a \"Go\" or \"End\" button.\n\nWhen your navigation app uses panels, you control the primary interface area of the map template. By building up and then pushing panels, your app can show more content and controls while keeping the map visible. Some vehicles with driver assistance systems work best when the intended route is known. For example, vehicles may support automatic lane changes, or adjust their guidance systems to more closely match the route shown in your app. Also, for a certain route, electric vehicles may suggest charging stops depending on the vehicle's available range.\n\nWith Route sharing, these driver assistance features can work even when people use your CarPlay navigation app to get directions.\n\nRoute sharing requires iOS 26.4 or later, and a supported vehicle.\n\nYour app provides a route to the vehicle as an array of route segments which are geographic coordinates that are sent to the vehicle whenever the trip changes.\n\nLooking at the Landmarks app has inspired me to go on a camping trip. I'm leaving early in the morning and want to stop at a coffee shop along the way so I added two stops into a navigation app first coffee then a campground. Using the CarPlay framework, the navigation app constructed a trip with two route segments. Route sharing sent that trip to the vehicle. Since my car is an electric vehicle, it estimated the energy consumption along this trip and determined that this trip requires a charging stop along the way. The vehicle searched for the ideal charging station for this trip and sent that destination back to iOS.\n\nThis proposed waypoint is received by the navigation app via the map template. The navigation app then has a choice in how to handle the suggested waypoint. By returning updated travel estimates to the map template, the map template will automatically prompt the driver to accept the additional waypoint. Or the navigation app can not return travel estimates and instead directly manage confirmation of the additional waypoint. Either way, once the driver accepts the waypoint… … the navigation app updates the trip to have a new route segment.\n\nThen the updated trip is again shared with the vehicle. Now I'll stop at the charging station and then the coffee shop and both me and the car will get there energized.\n\nBoth the driver and your app control when Route sharing occurs. When pairing with a vehicle, the driver is prompted to approve Route sharing for that vehicle. The driver's approval allows any navigation app to share a route when connected to that vehicle. To enable sharing routes from your navigation app, opt-in to Route sharing using the Map template.\n\nIf your app determines that certain trips are not eligible, Route sharing can also be disabled for an individual trip.\n\nCarPlay Simulator makes testing your CarPlay app as easy as connecting to your Mac! For any category of app in CarPlay, CarPlay Simulator supports testing different screen sizes and vehicle configurations.\n\nCarPlay Simulator is now available in Device Hub. Navigation app developers should try out the new diagnostic tools for route sharing. For video app developers, download CarPlay Simulator using the Additional Tools for Xcode package.\n\nNow that you've seen the new template options, consider adding thumbnails, the details header, and voice control to your app in CarPlay. If your app has videos, make them available for browsing in CarPlay.\n\nFinally if you have a navigation app, check out the new panels and route sharing.\n\nThanks for following along as I pointed out these new features and of course I'm looking forward to seeing how you'll use them to rev up your app in CarPlay!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "6:45",
+ "title": "Disable the MiniPlayer",
+ "language": "swift",
+ "code": "// Disable the MiniPlayer\n\nCPNowPlayingTemplate.shared.allowsMiniPlayer = false"
+ },
+ {
+ "timestamp": "15:06",
+ "title": "Enable route sharing",
+ "language": "swift",
+ "code": "// Enable route sharing\n\nfunc mapTemplateShouldProvideRouteSharing(_ mapTemplate: CPMapTemplate) -> Bool { true }"
+ },
+ {
+ "timestamp": "15:12",
+ "title": "Disable route sharing for this trip",
+ "language": "swift",
+ "code": "// Disable route sharing for this trip\n\ntrip.routeSegmentsAvailableForRegion = false"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "CarPlay for developers",
+ "url": "https://developer.apple.com/carplay"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/212/4/c594f5de-1012-4f5a-bad4-95ca200f5f58/downloads/wwdc2026-212_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/212/4/c594f5de-1012-4f5a-bad4-95ca200f5f58/downloads/wwdc2026-212_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "216",
+ "year": "2025",
+ "title": "Turbocharge your app for CarPlay",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/216"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:12.427Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-213.json b/data/wwdc/videos/2026-213.json
new file mode 100644
index 0000000..2a455f4
--- /dev/null
+++ b/data/wwdc/videos/2026-213.json
@@ -0,0 +1,84 @@
+{
+ "id": "213",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/213/",
+ "title": "Translate your app using agents in Xcode",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Developer Tools"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hello! I'm Avery, an engineer on the Localization team.\n\nToday I'd like to talk to you about translating your app using agents in Xcode.\n\nWhen you localize your app into other languages, you expand your app's audience to millions more people around the world. This is why at Apple, we're always striving to make it as easy as possible to localize your app.\n\nIn the past, traditional machine learning models have had a tough time with many aspects of software localization. Modern LLMs have solved several of these problems and are great at general-purpose translation, but they can still struggle with software localization when they don't have the right context. For example, consider the word 'book'. Is this referring to something to read? Or perhaps an action I would take to reserve a room at a hotel.\n\nThese words look the same in English, but have completely separate translations in other languages! Without further context, the wrong translation could be chosen, which would be misleading. The good news is, Xcode has been gaining context about your strings since the introduction of String Catalogs! Two years ago, String Catalogs started tracking where in code your strings are used. Last year, they started tracking how your strings are used, so they could automatically generate descriptive comments. And this year, we're excited to bring all of that context together! Beginning in Xcode 27, you can now translate your strings directly in Xcode using coding agents! I'll show you how you can add translations to your app, some techniques for reviewing and iterating on your translations, and finally, best practices you can follow to get the most out of these features. Let's get started! I've been developing an app that helps me learn about different landmarks from around the world and I'd like to make my app available to people around the world too! Since I grew up in Canada, let's start with Canadian French. I'm using SwiftUI, which means my app's UI is already localizable. For example, I'm using APIs like Text and Button, which automatically expose their strings for localization. I'm in a good place to start translation, so I'll ask an agent to translate my app into Canadian French, using the New Conversation button in Xcode's toolbar.\n\nAnd with that, the agent and Xcode begin working together to translate my strings, step by step. The agent starts by telling Xcode to prepare the project for localization.\n\nXcode begins by adding the language to the project settings in our case, Canadian French. Then, Xcode builds all of your targets for your supported platforms. This is important to ensure that all of the localizable strings in your project are properly discovered. Finally, any newly discovered strings are added to String Catalogs. If there aren't any String Catalogs in your project yet, Xcode creates them automatically. By default, strings are added to a String Catalog called Localizable. Of course, you can also use custom table names, to help you organize your strings. For example, using the table name Greetings automatically puts your strings in a String Catalog called Greetings. Looks like Xcode is done preparing the project. Looking at the navigator on the left, I can see four brand new String Catalogs. Great! Next, the agent selects the strings that need translating. In my case, I've asked it to do the whole project, so it reads all four String Catalogs. Finally, the real work can begin! The agent splits the strings into batches, delegating the translation work to individual subagents.\n\nXcode gives subagents context about each string being translated. This can include information like where in code the string is used, or a list of strings that use similar terminology. It can even reference how the string was translated in other languages! Let's take a look at the String Catalog on the right, to see what one subagent did when translating %lld items.\n\nThe placeholder %lld will be replaced with a number at runtime. The string is varied by plural, which means in English, it says 'one item' or 'two items'. The subagent has also varied this string in Canadian French, allowing it to say 'un élément' or 'deux éléments', with different spellings. Other languages vary their plurals differently with more or fewer variations, Xcode makes sure subagents always know which variations are needed, no matter the language. There are a lot of strings left for the subagents to translate, so I'll take a quick break and have a snack.\n\nOk! It looks like the subagents completed their stack before I could finish mine.\n\nLet's see how it looks when I run the app in Canadian French.\n\nI'll select my scheme in the toolbar, edit the scheme, select Run, and navigate to Options.\n\nHere, I can change the app's language to Canadian French for the next debug run. Now I'll build and run the app. That looks great, très bien! I can see I'm on the right track to make my app available to more people all around the world. Now, the app is currently using 'lieux d'intérêt' as the Canadian French translation for 'landmarks'. This is a great, well-understood translation! However, since I grew up learning Canadian French, I have a couple of stylistic ideas that will help my app's translations really shine. The term 'attraits', or 'attractions', is often used by the Canadian tourism industry in a similar way to how I'm using the word 'landmarks' in my app. I like the idea of using this more laid-back term, especially since it's very familiar to French-speaking Canadians. And for a little extra flair, I'll change the app name itself to 'Attraits phares' or 'Flagship attractions'. It's just as easy to make these changes as it was to add the original translations! I'll ask the agent to make the adjustments, and check back in, in a couple of minutes.\n\nGreat! Looking at some of the String Catalogs on the right, I can see the agent found all of the relevant strings and updated their translations, like the app name, and 'Draw a sketch of this landmark'. So far, I've covered how easy it is to get started with your first translations. However, it's just as easy to make translation a part of your workflow when adding new features to an app that's already localized. In fact, I've just had a great idea for another feature I want to add to my app. Let's build and localize it! Since this is a new feature, I'll start a new conversation.\n\nI've asked the agent to add, and localize a fun label underneath the featured landmark, challenging people to discover all of the landmarks in the app. Once the feature is built, the agent will translate it into Canadian French. Looks like it's all done. And check out the String Catalogs on the right, the agent added plural variations for the new string in both English and Canadian French, and the Canadian French string correctly uses 'attraits' as the translation for 'landmarks'. This is really powerful! Without reading my previous conversation, while building a completely new feature, Xcode guided the agent to discover and reuse a translation for landmarks that it would not have chosen by default. It's this kind of consistency that helps make the app that much more cohesive in Canadian French! Now that I've got some translations in place, I'd like to talk about some techniques for reviewing and iterating on localized strings. Adding translations to your project is just one part of the story. It's important to make sure that everything is working as you expect at runtime in other languages. Different languages have different characteristics. For example, sentences in Canadian French are longer on average than their English counterparts. Since I just translated my new feature to Canadian French, I should make sure that all of the text still fits in my app's UI without truncating. I'll ask the coding agent to render the UI for my new feature in Canadian French and look for truncations.\n\nCheck out the preview on the right, looks like it spotted a problem! The text for my new feature is truncating before the end of the sentence. From here, I can investigate to see if this is simply a bug with my implementation, if this UI needs to be redesigned to accommodate the longer text, or if I should resort to asking the model for a shorter translation. I'll add that to my to-do list for later. You can use this technique to check for all kinds of issues that might crop up with different languages, including vertical clipping of text for tall languages like Thai, or incorrectly aligned views for right-to-left languages like Arabic. This tight feedback loop is extremely helpful for catching and fixing issues as you develop your features. To learn more about techniques and APIs for addressing issues around text layout, formatting, and more, watch \"Build multilingual-ready apps\" and \"Get it right (to left)\". Besides getting feedback from a coding agent, it's also very important to get feedback from other sources. As developers, one of our greatest strengths when using agents for programming is that we understand code. This means that we can tell if an agent is producing code that fits our needs, is missing some nuances, or is heading down the wrong path. That advantage disappears when using an agent to translate strings into languages we don't fluently speak. This is where tools like TestFlight really shine. Just as you'd ask people to test your exciting new features before you release them on the App Store, you should also ask native speakers to test your app in the languages you're adding. With TestFlight, people can easily share feedback containing suggestions, or screenshots of localization issues, so you can address them before you release your localized experience to the public. For example, I changed the translation for landmarks earlier because I speak Canadian French, but TestFlight would be a great place for me to receive similar suggestions for other languages! To learn more about collecting feedback for your app using TestFlight, check out the tech talk \"Get Started with TestFlight\". Before I wrap up, I'd like to mention a few tips and best practices that you can take advantage of to get the most out of the translation features in Xcode. Having a well-localized app means that all user-facing strings should be translated. Of course, this can only happen if your user-facing strings are localizable! As I mentioned near the beginning, SwiftUI code is localizable by default. In the rest of your code, you will likely need to use String(localized:) or other APIs to ensure your strings are localizable. For more details on the APIs you can use for localization, check out \"Code-along: Explore localization with Xcode\". Once you've made sure that all of your strings are localizable, consider the audience you're addressing with your app. For example, a banking app would likely use very different style and terminology than an app for children. By default, agents have access to Apple's translation expertise using language-specific style guides that ship in Xcode. However, they'll also respect any translation guidance you provide. For example, you can write a glossary containing specific translations you'd like to use for specific words, or a list of words that should always be left untranslated, like product names or trademarks, or even a plain-text description of the tone you wish your app to use. To provide this guidance, you can add a section about translation to the AGENTS.md file, or any other similar file, you may already have in your project. To avoid loading this additional context even when doing non-translation tasks, we recommend simply referring to a file called TRANSLATION.md, in which you write all of your translation guidance. The agent should only choose to read the document when working on translation-related tasks. Aside from translation guidance, there are a few other things to consider. Translating an app is a complex and long-running task, especially when ensuring translations use consistent terminology across the project. When thinking about which model to use, consider using one with a large context window that excels at completing extended requests, as these models are best suited for translation work. It's also important to note that some languages appear less frequently than others in the training data that most models use, which could cause agents to produce lower-quality translations for those languages. Some specific models may also perform better or worse at translating certain languages. Consult documentation from model providers for more information on the distribution of languages in their training data. And finally, if you're handling localizations exported from Xcode, you can check for the leveraged-mt state qualifier to understand which translations were provided by an agent. I encourage you to explore translating your app using agents in Xcode on your own. Try asking an agent to translate your app into a new language. Or, add a new feature, and translate it, into the languages your app already supports. Once you have translations, use the various agentic tools in Xcode to help you find and fix localization issues, and gather feedback from native speakers using tools such as TestFlight. When you have an idea of where it's needed, consider providing extra translation guidance to agents to help make sure all of your translations are great going forward. And finally, if you haven't already, check out \"Xcode, agents, and you\" to learn about all of the other amazing things you can do with agents in Xcode. Thank you for watching, and I hope these new translation features will help you reach new people all around the world with your apps. À bientôt!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:02",
+ "title": "Localizing strings in SwiftUI",
+ "language": "swift",
+ "code": "// Localizing strings in SwiftUI\n\nText(\"Hello, world!\", comment: \"A standard greeting\")"
+ },
+ {
+ "timestamp": "3:11",
+ "title": "Localizing strings in SwiftUI with custom table name",
+ "language": "swift",
+ "code": "// Localizing strings in SwiftUI with custom table name\n\nText(\"Hello, world!\", tableName: \"Greetings\", comment: \"A standard greeting\")"
+ },
+ {
+ "timestamp": "11:25",
+ "title": "Localizing strings elsewhere",
+ "language": "swift",
+ "code": "// Localizing strings elsewhere\n\nString(localized: \"Hello, world!\", comment: \"A standard greeting\")\n\nLocalizedStringResource(\"Hello World!\", bundle: #bundle, comment: \"A standard greeting\")"
+ },
+ {
+ "timestamp": "13:39",
+ "title": "Field for machine-translated strings in the XLIFF",
+ "language": "swift",
+ "code": "// Field for machine-translated strings in the XLIFF\n\n\n Grand Canyon\n Grand Canyon\n Name of the ‘Grand Canyon’ landmark.\n"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Localizing your app using agents",
+ "url": "https://developer.apple.com/documentation/Xcode/localizing-your-app-using-agents"
+ },
+ {
+ "title": "Expanding Your App to New Markets",
+ "url": "https://developer.apple.com/localization/"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/213/4/be1ee662-a447-4df4-89a5-5411447c0eeb/downloads/wwdc2026-213_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/213/4/be1ee662-a447-4df4-89a5-5411447c0eeb/downloads/wwdc2026-213_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "259",
+ "year": "2026",
+ "title": "Xcode, agents, and you",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/259"
+ },
+ {
+ "id": "225",
+ "year": "2025",
+ "title": "Code-along: Explore localization with Xcode",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/225"
+ },
+ {
+ "id": "10185",
+ "year": "2024",
+ "title": "Build multilingual-ready apps",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10185"
+ },
+ {
+ "id": "10107",
+ "year": "2022",
+ "title": "Get it right (to left)",
+ "url": "https://developer.apple.com/videos/play/wwdc2022/10107"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:12.488Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-215.json b/data/wwdc/videos/2026-215.json
new file mode 100644
index 0000000..3d2c648
--- /dev/null
+++ b/data/wwdc/videos/2026-215.json
@@ -0,0 +1,141 @@
+{
+ "id": "215",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/215/",
+ "title": "Get started with the HTML Model Element",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Safari & Web",
+ "Spatial Computing"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, my name is Aleksei an engineer from the Safari team. Today I'm thrilled to tell you about the HTML Model element, and how we're making it easier for developers to bring 3D content to the web across Apple platforms and beyond.\n\nWhether you're a web developer exploring 3D for the first time, or an experienced 3D artist looking to bring your work to the web, I've got everything you need to get started. We pioneered the Model element in visionOS — a native HTML element that makes adding a 3D model as simple as adding an image.\n\nNow the Model element comes to iOS, iPadOS, and macOS! The same markup, the same element works across Apple's platforms.\n\nYour 3D content reaches every Safari visitor whether they are on iPhone on the go, on iPad at the coffee shop, or on Mac at their desk.\n\nYou might be familiar with \"model-viewer\", the go-to JavaScript library for 3D today. It's served developers well, but the Model element is native: no extra library, rendered directly by the platform, with built-in stereoscopic rendering on visionOS. It's also an emerging web standard, so your code is future-proof.\n\nTo reach all platforms across the web, a polyfill is available for browsers that don't natively support the Model element yet.\n\nI'll cover that in more detail later. Let's say I'm working on an e-commerce platform for outdoor adventures and I already have images for the products. Today, I'll walk you through adding 3D models to the product pages.\n\nFirst, I'll show you how to get assets ready. Then, we'll dive into the Model element building up the experience one feature at a time.\n\nAnd finally, I'll show you command-line tools to optimize models for production. Let's get into it.\n\nWhat if you don't have 3D assets in the first place? We recommend a Capture, Convert, Create approach using your iPhone to scan real-world objects, converting existing files, or building from scratch in tools like Blender.\n\nBut there's also been a revolution in generative AI and with it an entirely new way to make the content you want. You can use images to generate a model that matches a real-world object, or a text prompt for more creative generation. Apps like Tripo3D and Meshy.ai are only a couple to choose from. My store already has a few products, but I want to add an essential: a camping mallet. I started with several images of a mallet and after a few minutes, my model was ready to use. I exported it as a USDZ file.\n\nThe Model element supports USDZ — Universal Scene Description, zipped into a single file. It packages everything a 3D model needs: geometry, materials, textures, animations.\n\nSafari supports other formats too, but for the best experience, I would recommend starting with USDZ. It's time to put a model on the page. Now, the mallet is ready as a USDZ file. I'll cover different ways of presenting the Model element. I'll talk about how to embed a model, know when it's ready to display, and handle fallbacks.\n\nHow to match the model to my page design. Let visitors interact and explore.\n\nI'll guide you on how to implement custom transforms and play animations.\n\nAnd bring models into the real world with AR Quick Look and spatial computing. OK, starting with loading a model.\n\nJust like tag or tag, you point tag at a file with the source attribute, and the browser handles the rest. No plugins required. You can also use the tag with a specified mime type again the same pattern as tag.\n\nIt's also important to think about fallback. The simplest approach: place an tag inside your tag. Older versions of Safari and browsers that don't yet support the Model element will render the image instead. Your visitors still get an idea of the product. 3D models can be tens of megabytes or more in size, so it can take a while to load. The ready promise lets you know when the model is actually loaded and ready to display.\n\nWhile the model is loading, it's a good idea to provide a visual cue like a spinner. Hide it when the ready promise resolves.\n\nAnd if something unexpected happens, handle it in the catch block and show your fallback content.\n\nYou can also polyfill the model functionality as well. A polyfill retrofits the API of a new standard via JavaScript letting you use modern features even before they land on every platform. The Model element samples at the W3C show an example of exactly this. If the native element isn't defined on the window, they load the polyfill library to fill in as much of the functionality as they can, as close to the native API as possible. Just make sure to test the functionality of both the polyfill and the native element, and remember that some features of the element can not be polyfilled, such as the stereoscopic display on spatial platforms like Apple Vision Pro.\n\nThe Model element is rendered in its own virtual space, so it won't pick up the page's background. To match your page design, set background-color directly on the model. Keep in mind: the background is always rendered opaque, even if you specify a color with transparency it will be converted. Now we have a model loaded and integrated into the existing page. Giving our visitors an option to interact with it is a natural next step. Let me show you how easy that is.\n\nThe stagemode attribute with value orbit lets your visitors rotate the model freely side-to-side and if they tilt it up or down, it gently springs back to its original angle. Interactive, but always looking its best. The attribute also makes the model slightly smaller — it's rescaled to ensure no parts get clipped during rotation. Sometimes you want to add custom interactivity to the experience. The entityTransform property lets you set exact viewing angles via JavaScript and I'm going to add two buttons to show how it works: one for Side view, and a Reset button to bring the model back to its original orientation. To use entityTransform, you'll need to disable orbit — either remove stagemode attribute or set it to \"none\".\n\nWhen you transform the model manually, parts of it may get clipped or even disappear if rotated out of the visible area. You might need to adjust the position to make it visible.\n\nTo implement a transform I create a DOMMatrix — this represents the model's orientation in 3D space — then call rotateSelf to define the rotation. Here we're rotating 135 degrees around the Y axis to get the side view.\n\nFinally, assign it to model.entityTransform to apply the change.\n\nCapture the initial transform up front then use it to reset the model back to its original orientation.\n\nThe views switched instantly from one to another. To make that transition smooth, you can animate the rotation with requestAnimationFrame. First, set up the state: the current angle, the animation duration in milliseconds — 500 gives you half a second which feels snappy but smooth — and a reference to cancel any inflight animation.\n\nThe animateTo first cancels any running animation so transitions don't conflict, captures the current angle as the starting point, and marks the start time.\n\nThen I add the step function. On each frame, it computes how far along the animation is, eases the rotation for a smooth finish, and updates entityTransform with a new DOMMatrix. While the animation is still running, it requests the next frame.\n\nFinally, assign animateTo calls to the buttons. Side view animates to 135 degrees… reset to 0.\n\nLet's see it in action. When a user taps Side button, the model smoothly rotates to show the side view. After tapping Reset it glides right back to where it started.\n\nCustom transforms and animations give you full control, but they require extra work: bounding boxes, clipping, manual animation code. If your use-case allows it, the orbit stagemode gets you interactive 3D with a single attribute. Choose what works best for your product.\n\nModels can also come to life with built-in animation. These animations are typically authored in 3D tools like Blender or Maya and baked into the USDZ file. The Model element plays the first animation track, and with a couple of lines of JavaScript, you can control the playback rate or even run it in reverse by providing a negative value.\n\nTo do that, I implement a simple play function that sets playbackRate on the model and calls model's play() method — that's it. A positive value plays forward, a negative one reverses it. Here I'm using 5 and -5 for a faster playback.\n\nSo far, the store visitors can explore products in 3D right on the page. But what if they want to see how it looks in their environment? Wrap the model in an tag with rel=\"ar\" attribute, point to the same resource, and on iOS and iPadOS, your customers get a full AR Quick Look experience.\n\nOn visionOS, the Model element was already making 3D content feel like a natural part of the web.\n\nWith stereoscopic rendering, models gain real depth. Your customers can pull the product out of the page and examine it as if it were right in their hands.\n\nvisionOS also supports immersive website environments which use the Model element and let you transport your audience right inside a scene, all from Safari.\n\nIf you want to learn more about this feature, be sure to check the \"Explore immersive website environments\" session. It explains the API in detail.\n\nWith the assets ready and the model elements in place, let's see it all come together.\n\nHere's our catalog page with 3D models.\n\nAnd the interactive exploration is my personal favorite. Your customers can rotate, tilt, and view every angle of the product, exploring it in the way they want. All running right in Safari.\n\nOur product page looks fantastic running locally, but you may find loading can take a while over the internet. It would be great if we could get these models smaller. To do that, I'm going to run a command-line tool called usdcrush on the boot model.\n\nAnd with no change in quality, the file goes from 7.9 MB down to 1.9 MB! That's crushing savings! When we open both versions side- by-side in Safari — they look identical. Same visual quality, significantly smaller file.\n\nIf you have a 3D file, but no image for it yet, you can use usdrecord tool to generate a thumbnail or a fallback image directly, specifying things like output format or rendering from a custom camera if there's one in the file. And unlike a screenshot, you can write a script — well, who am I kidding? Your favorite LLM can write the script — to run it across your entire catalog. Both these command-line tools are already installed on your Mac. They're part of a larger suite of tools for working with USD content. You can learn more about all of them and the overall USD ecosystem… in the WWDC24 session \"What's new in USD and MaterialX\". The Model element started on visionOS, and with iOS, iPadOS, and macOS support, it's expanding across the Apple family and we want to bring it to the web at large. Our team actively contributing to the model specification at the W3C, and we would love to hear from you. If there are features you would add or use cases we haven't considered, now is a great time to share your feedback. Web standards are shaped by the developer community, and your voice matters. For 3D content in the USDZ file format, the Alliance of OpenUSD has published the full specification, a clear, vendor-neutral reference. The alliance also provides conversion tools and resources to help you integrate USDZ into your existing content pipelines. That's the Model element — from assets to production. Now, let me tell you about next steps.\n\nTry to create a 3D model of your own using text-based prompts or images you already have.\n\nAdd a tag to your website, point it at a USDZ file, and see how it comes to life in Safari. Use the USD tools to optimize your assets. Play with it on different platforms and see how the element adapts.\n\nJoin the Immersive Web Community Group at the W3C. Bring your use cases, your feedback, and your ideas.\n\nCatch up on the related sessions. Like \"Immersive website environments.\" And for a deeper dive into spatial web features, see \"What's new for the spatial web\" from WWDC25.\n\nI can't wait to see what you build. Thank you and have a great WWDC!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "4:19",
+ "title": "Load a model",
+ "language": "swift",
+ "code": "\n\n\n\n\n \n"
+ },
+ {
+ "timestamp": "4:39",
+ "title": "Image fallback",
+ "language": "swift",
+ "code": "\n \n"
+ },
+ {
+ "timestamp": "5:09",
+ "title": "Ready promise",
+ "language": "swift",
+ "code": "\n\n"
+ },
+ {
+ "timestamp": "5:39",
+ "title": "Polyfill fallback",
+ "language": "swift",
+ "code": ""
+ },
+ {
+ "timestamp": "6:13",
+ "title": "Model background",
+ "language": "swift",
+ "code": "\n"
+ },
+ {
+ "timestamp": "6:47",
+ "title": "Stage mode",
+ "language": "swift",
+ "code": "\n"
+ },
+ {
+ "timestamp": "7:31",
+ "title": "Custom transforms",
+ "language": "swift",
+ "code": "\n\n\n\n"
+ },
+ {
+ "timestamp": "8:35",
+ "title": "Transition animation",
+ "language": "swift",
+ "code": ""
+ },
+ {
+ "timestamp": "10:07",
+ "title": "Animation playback",
+ "language": "swift",
+ "code": "\n\n\n\n"
+ },
+ {
+ "timestamp": "11:06",
+ "title": "AR Quick Look",
+ "language": "swift",
+ "code": "\n \n"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "WebKit.org - Theater Ticket Sales immersive website environment demo for Apple Vision Pro",
+ "url": "https://webkit.org/demos/model-demos/ticket-sales.html"
+ },
+ {
+ "title": "The HTML model element in Apple Vision Pro",
+ "url": "https://webkit.org/blog/17118/a-step-into-the-spatial-web-the-html-model-element-in-apple-vision-pro/"
+ },
+ {
+ "title": "GitHub: model element samples",
+ "url": "https://immersive-web.github.io/model-element-samples/"
+ },
+ {
+ "title": "WebKit.org – Report issues to the WebKit open-source project",
+ "url": "https://bugs.webkit.org"
+ },
+ {
+ "title": "AOUSD – Alliance for OpenUSD",
+ "url": "https://aousd.org"
+ },
+ {
+ "title": "w3.org – Model element",
+ "url": "https://immersive-web.github.io/model-element"
+ },
+ {
+ "title": "Submit feedback",
+ "url": "http://feedbackassistant.apple.com"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/215/4/b7d159c9-ee29-45d9-80f5-87b6a1c90565/downloads/wwdc2026-215_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/215/4/b7d159c9-ee29-45d9-80f5-87b6a1c90565/downloads/wwdc2026-215_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "320",
+ "year": "2026",
+ "title": "Explore immersive website environments in visionOS",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/320"
+ },
+ {
+ "id": "204",
+ "year": "2026",
+ "title": "What’s new in WebKit for Safari 27",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/204"
+ },
+ {
+ "id": "237",
+ "year": "2025",
+ "title": "What’s new for the spatial web",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/237"
+ },
+ {
+ "id": "10106",
+ "year": "2024",
+ "title": "What’s new in USD and MaterialX",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10106"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:12.623Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-216.json b/data/wwdc/videos/2026-216.json
new file mode 100644
index 0000000..b359175
--- /dev/null
+++ b/data/wwdc/videos/2026-216.json
@@ -0,0 +1,271 @@
+{
+ "id": "216",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/216/",
+ "title": "Create web extensions for Safari",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Store, Distribution & Marketing",
+ "Safari & Web"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi! I'm Kiara, an engineer on the Safari team. If you've ever had an idea for a feature you wanted to see in Safari, and wanted to turn your idea into a reality, this session is for you.\n\nI'll be breaking down everything you need to know to build and distribute a web extension for Safari.\n\nThere's a lot to cover, so feel free to take breaks or skip to sections that are most useful for you.\n\nSafari web extensions are packaged within an app. And honestly, they're some of my favorite things to get from the App Store. Whether it's blocking ads, building a custom new tab page, or enhancing the playback experience on your favorite streaming site.\n\nThey're small but they can meaningfully improve how you experience the web.\n\nApple is working with other browsers in the W3C Web Extensions Working Group to standardize the APIs used to build web extensions across browsers.\n\nSo if you have an extension that you've built for another browser, you can bring your extension to Safari.\n\nJump to the Packaging and Distribution section, where I'll show you how you can use App Store Connect to distribute your extension.\n\nToday, I'm covering all the bases by building a web extension from the ground up. I'll take you through the process of developing an extension and highlight the key APIs and features you can use to bring customizable experiences to Safari. I'll also show you how to test web extensions in Safari and how you can use TestFlight to share beta versions of your extension with users. And once I'm ready to share my creation with the world, I'll submit my extension to the App Store.\n\nTo get started, download the sample code project for this session. In it, you'll find all of the resources for the extension that I'll be building today. So you can follow along. In this session, I'll guide you through building a real extension — one that blocks content and modifies web pages. Then, I'll cover a couple of different options for how you can package and distribute your extension to the App Store. And to take the capabilities of your extension even further, I'll show you how your extension can work in tandem with its containing app.\n\nAnd at the end, the extension will work in Safari on iOS, iPadOS, macOS, and visionOS simultaneously. Because the beauty of web extensions is that they're all made up of HTML, CSS, and JavaScript.\n\nSo if you've done any web development before, you already know most of what you need. In today's session, I'll be building an extension that allows people to block distracting sites while they browse the web.\n\nAnd as you know, there are many rabbit holes you can go down while browsing.\n\nTake webkit.org for example. There are hundreds of articles on there and I can easily lose hours just reading about what's new in WebKit. So, I need an extension like this. Today, I'm going to build it, and it has two different blocking modes: a Light mode that allows up to 10 minutes of browsing on a site, perfect for reading a few WebKit articles, and a Full mode that redirects users the moment they try to navigate there. To get started, I'll open up my favorite code editor. You can also use Xcode, but any editor works to build an extension. In my code editor, I'll create a folder to add all of the files for this extension.\n\nTo lay the ground work, the first file every extension needs is a manifest. The manifest is a JSON formatted file that tells the browser what your extension is and what it can do.\n\nThink of the manifest as an ID card for you extension.\n\nIt'll contain information, like the extension's name, description, and version number. Next, I'm going to add the images folder that will hold my extension's icon. The icon can appear in a variety of places. Like the toolbar... or Extensions Settings. Depending on where it appears, different sizes will be needed. So, I'll add the icon as an svg. Safari handles scaling the icon perfectly so I can focus on what's important like hopping into my code editor to show what this looks like in practice.\n\nIn the manifest file, I've added the extension's icon. And the icon is located in my images folder. To see what this looks like, I'll save my changes and I'll head over to Safari to load the extension. And it's so easy to load an extension in Safari. All I have to do is open Safari Settings using Command Comma, and click on the Advanced Settings pane and check \"Show features for web developers\". This will enable the developer pane and from there I can add a temporary extension. Since it's an extension that hasn't had its code signature verified, I'll need to allow unsigned extensions. After allowing, I'll select the folder containing my extension's resources. And just like that, I've got my extension loaded in Safari! Most extensions will have some sort of UI for people to interact with. For my extension, I want to have a way for people to add distracting sites to a block list. So I'll need to add some custom UI.\n\nThere are a couple of ways I can do this. One way is with the extension's action button. This is the button that's added for the extension in Safari's toolbar. When clicked, Safari will display a popup with the UI that you've defined for it. The file name for the popup, or any resource that you define in the manifest, can have any name you'd like, as long as it's associated with the right manifest key so that Safari knows what it is and how it should be used.\n\nIn this example, the file used to load the default popup is set to \"popup.html\".\n\nFor my extension, displaying a blocklist in a popup might make the UI look a bit cramped, so instead, I'll display it using the extension's options page. This will be a full page where users can set settings for my extension. Now, I'm going to jump back into the code editor to add this change.\n\nIn the manifest, I have the options page defined. And I'll add a file for it in my extension's folder. Before adding the full UI for this page, I'll start off with something simple, like Hello World. In Safari, I can test my changes by reloading the extension. Nice! My new changes worked since my extension now has this Settings button. And clicking on the button opens my extension's options page! Now that I've got that set up, my extension will need to do more than just show \"Hello World\" for it to be of much use.\n\nSo I've designed a page that allows users to switch between a Light mode and a Full mode. I've already written the interface for this using HTML, CSS, and JavaScript. It's pretty and interactive, but I need to wire it up. So now I'll take a look at how to upgrade my extension to start blocking content. I'll do this using the declarative net request API, which will give my extension the ability to block, modify or redirect network requests. This API is what powers my favorite type of web extensions. With this capability, extensions can filter web content such as ads and trackers that can target users while they browse the web.\n\nBut in order for my extension to access these features, I'll need to add a permission for it. Permissions are how extensions tell Safari what they need access to. It can be things like accessing cookies, saving data to storage or writing to the clipboard. For content blocking I'll need to add the declarative net request permission and I can do this in my extension's manifest.\n\nWith this in place, I can start defining rules.\n\nA rule has an ID, a priority, and the type of action that should occur when the conditions are met. This rule, for example, will block all navigations to webkit.org. There are two ways I can define rules. One option is to define them in the manifest. These are called static rules. They're great when you already know what rules you want to use. But, if you need a little flexibility, you can add rules dynamically at runtime using JavaScript. I'm going the go with the dynamic approach because I won't know which sites to block until the user adds them to the list.\n\nI'll put this logic in a file named rules.js, inside the utilities folder. Then I'll use my host.js file to create the rule when a user adds a site to the blocklist. I'm going to jump back into my code and wire this up.\n\nIn my extension's manifest, I've added the declarative net request permission. And I've replaced the previous options page with the HTML, CSS, and JavaScript files that I already created for my extension. I also added the utilities folder with my two new files. To add a rule, I head over to my rules.js file. And since these rules specify an ID, I've added a helper method to map the host for the site to a unique integer ID. Now, I create a rule specifying the ID, the type as \"block\", and the urlFilter to match the host for the site. And then use the declarative net request updateDynamicRules API to add the rule to my extension. In my host file, when a site is added to the list, I can add the rule if the extension is in the full blocking mode. Going back to Safari, I reload to update my extension.\n\nIn the options page, I'll add webkit.org to the list.\n\nAnd when I go to the site, it's been blocked! My extension can now block navigations to sites added to the list. But, I don't really love that error page that shows up. I'd rather send users somewhere more intentional, like a custom page that I've designed for my extension.\n\nThat's where redirect rules come into play. This rule is similar to the previous block rule, except the type is redirect, and I can specify an extensionPath for the page the user will land on.\n\nBut before making this change, I need to add host permissions for my extension. To block a network request, extensions don't need access to the page. But for redirecting network requests, the extension does need access. So, in my extension's manifest I'll use the declarativeNetRequestWithHostAccess permission instead. And since my extension doesn't need to request access to any site upfront, I can use optional host permissions and request access to the site at runtime. Host permissions tell Safari which sites your extension wants access to. You can set them up as an array of match patterns, with each pattern consisting of a scheme, a host, and a path. Since any site can be added to the list, I'll use a pattern that can match against all URLs. If your extension needs explicit access to a site to work, you can use host permissions instead. But the extension doesn't automatically get access to the site. We designed the permissions model for extensions to respect user privacy.\n\nSince a user's browsing experience can expose personal data, we put the user in control and they decide which sites the extension can access.\n\nIf you explicitly request access, Safari will show a badge on the extension's action button. Clicking on the button brings up an alert, asking the user if they want to grant the extension access to the page. If the user chooses to allow access, the icon will become tinted, notifying them that the extension is active on that page.\n\nMy extension doesn't need access to any site upfront, which is why I've gone with optional host permissions. This way, I can request access for any site when my extension needs it. In the manifest, I've changed the permission to declarativeNetRequestWithHostAccess. And my extension can now request access to any site at runtime. Now, in my rules file, I'll create the redirect rule. It's very similar to the previous block rule, but now the type is redirect. And it has the path to my custom extension page. And I've added the resources for the page in my extension's folder.\n\nNow instead of blocking the navigation, I'll redirect users to a page that I've designed for my extension. And since my extension will need access to the site, I'll request access to the domain and subdomains using the permissions.request API.\n\nTo see what this experience looks like, in Safari, I'll update my extension.\n\nIn the options page, I'll add webkit.org.\n\nAnd now, before the site is added, I'm prompted to grant the extension access.\n\nGreat! That's exactly what I was expecting.\n\nI'll allow access and refresh the page.\n\nAmazing! The navigation was redirected to my custom extension page. My extension is really coming along! It can now redirect navigations to distracting sites. But let's be real.\n\nGoing no-contact with some of your favorite sites can be hard. So I'm going to add a mode that allows up 10 minutes of browsing — with a countdown timer right on the page. To make that happen, I'll need a way to inject content directly onto the page. This is where content scripts come into play.\n\nContent scripts give extensions the ability to read and modify the contents of a web page.\n\nScripts can be static — declared right in the manifest with the files and match patterns for the sites they'll run on. This works well if you already know which sites to target. But in my case, I won't know the sites until it's added to the list. So I'll add them on the fly, using the registered content scripts API. These work just like static scripts but they have two additional fields: an ID and a persistence flag. Setting this to true means that the scripts will remain after Safari relaunches.\n\nTo use this API, I'll add the scripting permission in the manifest and new scripting.js file in my utilities folder.\n\nHere, I'll define the content script. I'll give it an ID, the javascript file for the timer, the CSS for styling, and match patterns that cover the domain and any subdomains of the site. And I'll set this flag to true. Then, I'll add the script using the register content scripts API. Going back to my addHost method, now, when the user adds a site to the block list, I'll add a content script to show a timer for that site. When the extension is in the full blocking mode, the redirect will trigger before the page loads so I can always register the scripts.\n\nNow with the light blocking mode selected, I'll add webkit.org to the list.\n\nAnd when I go to the site, a 10-minute timer is shown on the page.\n\nMy extension is so close to being something I want to get out into the world, but there's something I want to fix first. As you may have noticed, every time I reload the extension, I keep having to add the same site back to the list. This is happening because I'm storing all of this information in memory, so when the extension reloads, that state is gone.\n\nI can keep this data around using the storage API.\n\nSafari supports two kinds of storage areas. Session storage is great for quick, in-memory stuff that doesn't need to survive a restart.\n\nBut I want my blocklist to stick around so local storage, which writes data to disk, is the right call here.\n\nTo use the storage API, I'll add the permission in the manifest.\n\nThen, I'll add a new file in my utilities folder. In this file, I'll define a few helper methods to update and get the hosts in storage. And a couple more to save and get the block mode.\n\nGoing back to my addHost method, now when the user adds a new site, I can update the list of hosts in storage.\n\nAnd I can use the stored list to display the block list. With this change, the blocklist will always render with the full list of sites that were added! Similarly, when the user switches between the two modes, I'll save the change to storage and if the extension is in the full blocking mode, I can create the redirect rules for all of the sites.\n\nTo see the benefits of using the storage API, in Safari, I'll change the blocking mode to \"Full\" and add a site to the list.\n\nNow, when I reload the extension, the changes that I just made are still there! Great! Using the storage API has tackled one persistence problem with my extension, but there's another one I need to fix. Registered content scripts persist across Safari restarts, but not across extension updates. So if a user updates my extension, they'll lose the content scripts. To fix that, I need a way for my extension to know it's been updated so that it can read the hosts from storage and recreate the scripts.\n\nThe perfect place for that is in a background page or a service worker.\n\nBoth can do the same things: like manage your extension's lifecycle, listen for browser events and pass messages between parts of your extension. Safari supports both, so it's really your preference! I like background pages since they have access to the DOM, so I'll go with that.\n\nTo add the background page, I'll specify it in the manifest.\n\nThen, I'll add the file to my extension's folder. Here, I'll register for the onInstalled event. This will let my extension know that it's been updated to a newer version. When this happens, I'll read the hosts from storage and re-register the content scripts.\n\nGetting my extension hooked up to read and write from storage has made such a huge improvement. I think it's time to take a look at how I can get my extension on the App Store.\n\nOne way for me to do this is with App Store Connect. App Store Connect is where you can upload, submit, and manage your extensions, whether you've just built yours or your looking to bring an existing one to Safari.\n\nAnd the best part? You can do this from any browser, without using a Mac.\n\nTo get started, I'll head over to developer.apple.com and enroll in the Apple Developer Program. After enrolling, I'll go to appstoreconnect.apple.com. Since Safari web extensions need to be packaged within a containing app, I can use App Store Connect to create this app for me.\n\nWhen creating the app, I'll need to specify a few things, such as the platforms I want my extension available on. I'll choose iOS and macOS, which will make my extension available on iPhone, iPad, Mac, and as a compatible app on Apple Vision Pro. I'll also set the bundle identifier which is a unique ID for my app. After adding all the details, I'll switch over to the tab for Xcode Cloud and scroll down to the Safari Web Extension Packager to upload my extension's resources. Once it's uploaded, the Safari Web Extension Packager will handle packaging my extension in a matter of minutes! Once it's finished, I can view any issues or take next steps to test my extension using TestFlight.\n\nTestFlight allows me distribute beta builds so I can continuously make improvements and implement user feedback before submitting my extension to the App Store. Once I'm ready to submit, I'll head over to the Distribution tab to add my finishing touches.\n\nLike a screenshot of my extension in action. And a description to help users understand the features and capabilities of my extension.\n\nAfter adding all the details, I'll select the build, and submit it for review.\n\nAnd that's how you can distribute your extension using App Store Connect! Throughout this session, I've shown how standard WebExtension APIs and features come together to build a customizable browsing experience. But what if I told you that your extension can go beyond just the web platform.\n\nI'm going to guide you on how you can use native messaging to access features that the platform offers. From here, I'll need to use Xcode. The simplest way for me to do this is with the Safari Web Extension Packager tool.\n\nRunning this command in Terminal will create and launch an Xcode project for me. It'll contain my app and web extension. From there, I can hook them up to send and receive messages. We call this native messaging. Think of it as three people passing notes.\n\nThe JavaScript in my extension kicks things off. The App Extension in the middle catches that message and hands it to the native app. Then the app does its thing and sends the result back the same way.\n\nFor my app, I'm going to have it help the extension protect its settings by requiring bio authentication before a change is made to the block list.\n\nTo get started, I'll add the nativeMessaging permission in my extension's manifest.\n\nThen, in my extension's background page, I'll add a method to send a message from my extension to its native app. The message I get back will let me know if the authentication succeeded. In order for my app to receive the message, I'll need to modify the SafariWebExtensionHandler, a class contained in a file the packager tool created for me. And the great thing is, it comes with a template that already allows my app to receive messages from my extension. All I need to do is make a few tweaks, like parsing the message for the requestBioAuth key. Then, I'll use a System API to request user authentication with biometrics and once the user has authenticated, the app sends the message back to the web extension with the result.\n\nI'm going to hop into Xcode to build the extension and test my changes in Safari.\n\nIn Xcode, I have the changes made to send and receive messages to my extension. And in the Project Navigator, my project has all of the resources for my web extension.\n\nI'll use the Command+B shortcut to build the extension. And in Safari, I'll enable the extension, open the options page, and add webkit.org to the list.\n\nAnd before it gets added, I'm prompted to authenticate with touch ID.\n\nAnd after authenticating, the site is added to the list.\n\nAnd that's how you can use native messaging to have your app and web extension work together. And with that, I can proudly say my extension is ready for distribution. I'll do this using Xcode.\n\nI'll start by building an archive. And since I previously used App Store Connect to create a build, I'll want to make sure this build number is one step higher than my last build. And in the Organizer window I'll distribute my extension.\n\nAnd that's how you can create and distribute a Safari web extension from the ground up.\n\nWe covered a lot today.\n\nWhether you're just starting out or looking to take an extension further, I hope you feel ready to take your idea and turn it into something real. I can't wait to see what you build.\n\nIf you haven't already, download the sample code project to play around with some of the APIs we featured today. You can learn more about them by checking out the cross-browser documentation for web extensions on MDN.\n\nAnd finally, provide feedback through Feedback Assistant. Or file a bug on bugs.webkit.org as you test your web extensions on Safari 27.\n\nThanks for joining me on this journey and have a great WWDC!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:44",
+ "title": "Manifest file",
+ "language": "swift",
+ "code": "{\n \"manifest_version\": 3,\n \"name\": \"Shiny OnTrack\",\n \"description\": \"Stay on track while you browse the web\",\n \"version\": 1.0\n}"
+ },
+ {
+ "timestamp": "4:29",
+ "title": "Adding an extension icon",
+ "language": "swift",
+ "code": "{\n \"manifest_version\": 3,\n \"name\": \"Shiny OnTrack\",\n \"description\": \"Stay on track while you browse the web\",\n \"version\": 1.0,\n\n \"icons\": {\n \"512\": \"images/icon.svg\"\n }\n}"
+ },
+ {
+ "timestamp": "5:30",
+ "title": "Adding an action button",
+ "language": "swift",
+ "code": "{\n \"manifest_version\": 3,\n \"name\": \"Shiny OnTrack\",\n \"description\": \"Stay on track while you browse the web\",\n \"version\": 1.0,\n\n \"action\": {\n \"default_popup\": \"popup.html\"\n }\n}"
+ },
+ {
+ "timestamp": "6:17",
+ "title": "Adding custom UI to your extension",
+ "language": "swift",
+ "code": "{\n \"manifest_version\": 3,\n \"name\": \"Shiny OnTrack\",\n \"description\": \"Stay on track while you browse the web\",\n \"version\": 1.0,\n \n \"options_ui\": {\n \"page\": \"options.html\"\n }\n}"
+ },
+ {
+ "timestamp": "6:30",
+ "title": "Including the UI in the extension manifest",
+ "language": "swift",
+ "code": "{\n \"manifest_version\": 3,\n \"name\": \"Shiny OnTrack\",\n \"description\": \"Stay on track while you browse the web\",\n \"version\": 1.0,\n\n \"icons\": {\n \"512\": \"images/icon.svg\"\n },\n\n \"options_ui\": {\n \"page\": \"options.html\"\n }\n}"
+ },
+ {
+ "timestamp": "6:40",
+ "title": "Hello World",
+ "language": "swift",
+ "code": "\n\n
\n
Hello World
\n \n"
+ },
+ {
+ "timestamp": "8:18",
+ "title": "Adding declarativeNetRequest permission",
+ "language": "swift",
+ "code": "{\n \"manifest_version\": 3,\n \"name\": \"Shiny OnTrack\",\n \"description\": \"Stay on track while you browse the web\",\n \"version\": 1.0,\n\n \"icons\": {\n \"512\": \"images/icon.svg\"\n },\n\n \"options_ui\": {\n \"page\": \"options.html\"\n },\n \n \"permissions\": [ \"declarativeNetRequest\" ]\n}"
+ },
+ {
+ "timestamp": "8:22",
+ "title": "Blocking network requests",
+ "language": "swift",
+ "code": "// block rule\n{\n id: 1,\n priority: 1,\n action: {\n type: \"block\"\n },\n condition: {\n urlFilter: \"||webkit.org\",\n resourceTypes: [ \"main_frame\" ]\n }\n}"
+ },
+ {
+ "timestamp": "8:41",
+ "title": "Modifying network requests",
+ "language": "swift",
+ "code": "{\n \"manifest_version\": 3,\n \"name\": \"Shiny OnTrack\",\n \"description\": \"Stay on track while you browse the web\",\n \"version\": 1.0,\n\n \"icons\": {\n \"512\": \"images/icon.svg\"\n },\n\n \"options_ui\": {\n \"page\": \"options.html\"\n },\n \n \"permissions\": [ \"declarativeNetRequest\" ],\n\n \"declarativeNetRequest\": {\n \"rule_resources\": [\n {\n \"id\": \"ruleset_id\",\n \"enabled\": true,\n \"path\": \"rules.json\"\n }\n ]\n }\n}"
+ },
+ {
+ "timestamp": "8:50",
+ "title": "Updating dynamic rules",
+ "language": "swift",
+ "code": "await browser.declarativeNetRequest.updateDynamicRules({\n addRules: [ rule ]\n})"
+ },
+ {
+ "timestamp": "9:19",
+ "title": "Wiring up the static declarativeNetRequest rules",
+ "language": "swift",
+ "code": "{\n \"manifest_version\": 3,\n \"name\": \"Shiny OnTrack\",\n \"description\": \"Stay on track while you browse the web\",\n \"version\": 1.0,\n\n \"icons\": {\n \"512\": \"images/icon.svg\"\n },\n\n \"options_ui\": {\n \"page\": \"options.html\"\n },\n \n \"permissions\": [ \n \"declarativeNetRequest\" \n ]\n}"
+ },
+ {
+ "timestamp": "9:40",
+ "title": "Adding block rules dynamically",
+ "language": "swift",
+ "code": "// A helper function to map the host to the declarative net request rule ID.\nexport function hostToRuleID(host) {\n\tlet hash = 0;\n\tfor (let i = 0; i < host.length; i++) {\n\t\thash = ((hash << 5) + hash) + host.charCodeAt(i);\n\t\thash |= 0;\n\t}\n\treturn Math.abs(hash) || 1;\n}\n\nfunction createBlockRule(host) {\n\treturn {\n\t\tid: hostToRuleID(host),\n\t\tpriority: 1,\n\t\taction: {\n\t\t\ttype: \"block\"\n\t\t},\n\t\tcondition: {\n\t\t\turlFilter: `||${host}`,\n\t\t\tresourceTypes: [\"main_frame\"]\n\t\t}\n\t}\n}\n\nexport async function createRules(hosts) {\n\ttry {\n\t\tawait browser.declarativeNetRequest.updateDynamicRules({\n\t\t\taddRules: hosts.map(createBlockRule)\n\t\t})\n\t} catch {\n\t\tconsole.log(\"Failed to create declarative net request rules\")\n\t}\n}"
+ },
+ {
+ "timestamp": "10:10",
+ "title": "Handling adding hosts to the settings",
+ "language": "swift",
+ "code": "import { createRules, removeAllRules, removeRule } from './rules.js'\n\nexport async function addHost(host, blockingMode) {\n if (!host)\n return\n \n if (blockingMode === \"full\")\n await createRules([host])\n}"
+ },
+ {
+ "timestamp": "10:48",
+ "title": "Redirecting network requests",
+ "language": "swift",
+ "code": "{\n id: 1,\n priority: 1,\n action: {\n type: \"redirect\",\n redirect: {\n extensionPath: \"/blocked.html\"\n }\n },\n condition: {\n urlFilter: \"||webkit.org\",\n resourceTypes: [ \"main_frame\" ]\n }\n}"
+ },
+ {
+ "timestamp": "11:17",
+ "title": "Declaring optional host permissions",
+ "language": "swift",
+ "code": "{\n \"manifest_version\": 3,\n \"name\": \"Shiny OnTrack\",\n \"description\": \"Stay on track while you browse the web\",\n \"version\": 1.0,\n\n \"icons\": {\n \"512\": \"images/icon.svg\"\n },\n\n \"options_ui\": {\n \"page\": \"options.html\"\n },\n \n \"permissions\": [ \"declarativeNetRequestWithHostAccess\" ],\n \"optional_host_permissions\": [ \"https://webkit.org/*\" ]\n\n}"
+ },
+ {
+ "timestamp": "11:54",
+ "title": "Declaring optional host permissions for all sites",
+ "language": "swift",
+ "code": "{\n \"manifest_version\": 3,\n \"name\": \"Shiny OnTrack\",\n \"description\": \"Stay on track while you browse the web\",\n \"version\": 1.0,\n\n \"icons\": {\n \"512\": \"images/icon.svg\"\n },\n\n \"options_ui\": {\n \"page\": \"options.html\"\n },\n \n \"permissions\": [ \"declarativeNetRequestWithHostAccess\" ],\n \"optional_host_permissions\": [ \"*://*/*\" ]\n\n}"
+ },
+ {
+ "timestamp": "13:12",
+ "title": "Add the redirect rule",
+ "language": "swift",
+ "code": "// A helper function to map the host to the declarative net request rule ID.\nexport function hostToRuleID(host) {\n\tlet hash = 0;\n\tfor (let i = 0; i < host.length; i++) {\n\t\thash = ((hash << 5) + hash) + host.charCodeAt(i);\n\t\thash |= 0;\n\t}\n\treturn Math.abs(hash) || 1;\n}\n\nfunction createBlockRule(host) {\n\treturn {\n\t\tid: hostToRuleID(host),\n\t\tpriority: 1,\n\t\taction: {\n\t\t\ttype: \"block\"\n\t\t},\n\t\tcondition: {\n\t\t\turlFilter: `||${host}`,\n\t\t\tresourceTypes: [\"main_frame\"]\n\t\t}\n\t}\n}\n\nfunction createRedirectRule(host) {\n\treturn {\n\t\tid: hostToRuleID(host),\n\t\tpriority: 1,\n\t\taction: {\n\t\t\ttype: \"redirect\",\n\t\t\tredirect: { extensionPath: \"/blocked.html\" }\n\t\t},\n\t\tcondition: {\n\t\t\turlFilter: `||${host}`,\n\t\t\tresourceTypes: [\"main_frame\"]\n\t\t}\n\t}\n}\n\nexport async function createRules(hosts) {\n\ttry {\n\t\tawait browser.declarativeNetRequest.updateDynamicRules({\n\t\t\taddRules: hosts.map(createRedirectRule)\n\t\t})\n\t} catch {\n\t\tconsole.log(\"Failed to create declarative net request rules\")\n\t}\n}"
+ },
+ {
+ "timestamp": "13:42",
+ "title": "Dynamically ask for host permissions",
+ "language": "swift",
+ "code": "import { createRules, removeAllRules, removeRule } from './rules.js'\n\nexport async function addHost(host, blockingMode) {\n if (!host)\n return\n \n const granted = await browser.permissions.request({\n origins: [`*://${host}/*`, `*://*.${host}/*`]\n })\n if (!granted)\n return\n \n if (blockingMode === \"full\")\n await createRules([host])\n}"
+ },
+ {
+ "timestamp": "14:55",
+ "title": "Defining content scripts",
+ "language": "swift",
+ "code": "{\n \"manifest_version\": 3,\n \"name\": \"Shiny OnTrack\",\n \"description\": \"Stay on track while you browse the web\",\n \"version\": 1.0,\n\n \"icons\": {\n \"512\": \"images/icon.svg\"\n },\n\n \"options_ui\": {\n \"page\": \"options.html\"\n },\n \n \"permissions\": [ \"declarativeNetRequestWithHostAccess\" ],\n \"optional_host_permissions\": [ \"*://*/*\" ],\n \n \"content_scripts\": [\n {\n \"js\": [ \"content.js\" ],\n \"css\": [ \"content.css\" ],\n \"matches\": [ \"*://*.webkit.org/*\" ]\n }\n ]\n}"
+ },
+ {
+ "timestamp": "15:13",
+ "title": "Dynamically registering content scripts",
+ "language": "swift",
+ "code": "let script = {\n id: \"id\",\n js: [ \"content.js\" ],\n css: [ \"content.css\" ],\n matches: [ \"*://*.webkit.org/*\" ],\n persistAcrossSessions: true\n}\n\nawait browser.scripting.registerContentScripts([ script ])"
+ },
+ {
+ "timestamp": "15:31",
+ "title": "Adding the scripting permission",
+ "language": "swift",
+ "code": "{\n \"manifest_version\": 3,\n \"name\": \"Shiny OnTrack\",\n \"description\": \"Stay on track while you browse the web\",\n \"version\": 1.0,\n \n \"icons\": {\n \"512\": \"images/icon.svg\"\n },\n \n \"options_page\": \"options.html\",\n \n \"permissions\": [\n \"declarativeNetRequestWithHostAccess\",\n \"scripting\"\n ],\n \n \"optional_host_permissions\": [ \"*://*/*\" ]\n}"
+ },
+ {
+ "timestamp": "15:41",
+ "title": "Registering content scripts",
+ "language": "swift",
+ "code": "// scripting.js\n\nfunction contentScript(host) {\n return {\n id: `cs-${host}`,\n js: [ \"content.js\" ],\n css: [ \"content.css\" ],\n matches: [ `*://${host}/*`, `*://*.${host}/*` ],\n persistAcrossSessions: true\n }\n}\n\nexport function registerScripts(hosts) {\n const scripts = hosts.map(contentScript)\n try {\n await browser.scripting.registerContentScripts(scripts)\n } catch {\n console.log(\"Failed to register content scripts\")\n }\n}"
+ },
+ {
+ "timestamp": "16:02",
+ "title": "Adding a host",
+ "language": "swift",
+ "code": "// host.js\n\nexport async function addHost(host, blockMode) {\n if (!host)\n return\n\n const granted = await browser.permissions.request({\n origins: [`*://${host}/*`, `*://*.${host}/*`]\n })\n\n if (!granted)\n return\n\n if (blockingMode === \"full\")\n await createRules([ host ])\n\n await registerScripts([ host ])\n}"
+ },
+ {
+ "timestamp": "17:06",
+ "title": "Web extensions storage APIs",
+ "language": "swift",
+ "code": "await browser.session.storage.set({\n key: value\n})\n\nawait browser.local.storage.set({\n key: value\n})"
+ },
+ {
+ "timestamp": "17:21",
+ "title": "Adding storage permission to the web extension manifest.json",
+ "language": "swift",
+ "code": "{\n \"manifest_version\": 3,\n \"name\": \"Shiny OnTrack\",\n \"description\": \"Stay on track while you browse the web\",\n \"version\": 1.0,\n \n \"icons\": {\n \"512\": \"images/icon.svg\"\n },\n \n \"options_page\": \"options.html\",\n \n \"permissions\": [\n \"declarativeNetRequestWithHostAccess\",\n \"scripting\",\n \"storage\"\n ],\n \n \"optional_host_permissions\": [ \"*://*/*\" ]\n}"
+ },
+ {
+ "timestamp": "17:30",
+ "title": "Saving data with storage",
+ "language": "swift",
+ "code": "// storage.js\n\nexport async function updateHosts(hosts) {\n await browser.storage.local.set({ hosts: hosts })\n}\n\nexport async function getHosts() {\n const { hosts = [] } = await browser.storage.local.get(\"hosts\")\n return hosts\n}\n\nexport async function saveBlockMode(mode) {\n await browser.storage.local.set({ blockMode: mode })\n}\n\nexport async function getBlockMode() {\n const { blockMode = \"full\" } = await browser.storage.local.get(\"blockMode\")\n return blockMode\n}"
+ },
+ {
+ "timestamp": "17:41",
+ "title": "Persisting hosts to storage",
+ "language": "swift",
+ "code": "// host.js\n\nexport async function addHost(host, blockMode) {\n if (!host)\n return\n\n const granted = await browser.permissions.request({\n origins: [`*://${host}/*`, `*://*.${host}/*`]\n })\n\n if (!granted)\n return\n\n if (blockingMode === \"full\")\n await createRules([ host ])\n\n await registerScripts([ host ])\n\n let existingHosts = await getHosts()\n let updatedHosts = [ ...existingHosts, host ]\n await updateHosts(updatedHosts)\n}"
+ },
+ {
+ "timestamp": "17:51",
+ "title": "Reading from storage",
+ "language": "swift",
+ "code": "// options.js\n\nlet existingHosts = await getHosts()\nlet blockMode = await getBlockMode()\n\ndisplayBlocklist(existingHosts)"
+ },
+ {
+ "timestamp": "18:00",
+ "title": "Switching block modes",
+ "language": "swift",
+ "code": "// host.js\n\nexport async function userDidSwitchMode(blockMode) {\n await saveBlockMode(blockMode)\n\n if (blockMode === \"full\") {\n let hosts = await getHosts()\n await createRules(hosts)\n } else\n await removeAllRules()\n}"
+ },
+ {
+ "timestamp": "19:01",
+ "title": "Adding a background script",
+ "language": "swift",
+ "code": "{\n \"manifest_version\": 3,\n \"name\": \"Shiny OnTrack\",\n \"description\": \"Stay on track while you browse the web\",\n \"version\": 1.0,\n \n \"icons\": {\n \"512\": \"images/icon.svg\"\n },\n \n \"options_page\": \"options.html\",\n \n \"permissions\": [\n \"declarativeNetRequestWithHostAccess\",\n \"scripting\",\n \"storage\"\n ],\n \n \"optional_host_permissions\": [ \"*://*/*\" ],\n \n \"background\": {\n \"scripts\": [ \"background.js\" ],\n \"type\": \"module\"\n }\n}"
+ },
+ {
+ "timestamp": "19:39",
+ "title": "Background script",
+ "language": "swift",
+ "code": "// background.js\n\nimport { registerScripts } from \"./utilities/scripting.js\"\nimport { getHosts } from \"./utilities/storage.js\"\n\nbrowser.runtime.onInstalled.addListener(async (details) => {\n if (details.reason !== \"update\")\n return\n\n const hosts = await getHosts()\n await registerScripts(hosts)\n})"
+ },
+ {
+ "timestamp": "22:49",
+ "title": "Package your web extension into an app for Xcode",
+ "language": "swift",
+ "code": "xcrun safari-web-extension-packager --copy-resources /path/to/ShinyOnTrack"
+ },
+ {
+ "timestamp": "23:32",
+ "title": "Adding the nativeMessaging permission",
+ "language": "swift",
+ "code": "{\n \"manifest_version\": 3,\n \"name\": \"Shiny OnTrack\",\n \"description\": \"Stay on track while you browse the web\",\n \"version\": 1.0,\n \n \"icons\": {\n \"512\": \"images/icon.svg\"\n },\n \n \"options_page\": \"options.html\",\n \n \"permissions\": [\n \"declarativeNetRequestWithHostAccess\",\n \"scripting\",\n \"storage\",\n \"nativeMessaging\"\n ],\n \n \"optional_host_permissions\": [ \"*://*/*\" ],\n \n \"background\": {\n \"scripts\": [ \"background.js\" ],\n \"type\": \"module\"\n }\n}"
+ },
+ {
+ "timestamp": "23:40",
+ "title": "Sending a native message",
+ "language": "swift",
+ "code": "// background.js\n\nimport { registerScripts } from \"./utilities/scripting.js\"\nimport { getHosts } from \"./utilities/storage.js\"\n\nbrowser.runtime.onInstalled.addListener(async (details) => {\n if (details.reason !== \"update\")\n return\n\n const hosts = await getHosts()\n await registerScripts(hosts)\n})\n\nexport async function requestBioAuth() {\n const message = { message: \"requestBioAuth\" }\n const response = await browser.runtime.sendNativeMessage(message)\n return response?.success\n}"
+ },
+ {
+ "timestamp": "23:55",
+ "title": "Handling native messages",
+ "language": "swift",
+ "code": "// SafariWebExtensionHandler.swift\n\nimport LocalAuthentication\n\nclass SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {\n func beginRequest(with context: NSExtensionContext) {\n let request = context.inputItems.first as? NSExtensionItem\n let message = request?.userInfo?[SFExtensionMessageKey] as? [String: Any]\n\n if message?[\"message\"] as? String == \"requestBioAuth\" {\n let lAContext = LAContext()\n Task {\n do {\n let success = try await lAContext.evaluatePolicy(\n .deviceOwnerAuthenticationWithBiometrics,\n localizedReason: \"Authenticate to change blocked sites\"\n )\n self.reply(context: context, success: success)\n } catch {\n self.reply(context: context, success: false)\n }\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "24:25",
+ "title": "Replying to a native message",
+ "language": "swift",
+ "code": "// SafariWebExtensionHandler.swift\n\nimport LocalAuthentication\n\nclass SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {\n func beginRequest(with context: NSExtensionContext) {\n let request = context.inputItems.first as? NSExtensionItem\n let message = request?.userInfo?[SFExtensionMessageKey] as? [String: Any]\n\n if message?[\"message\"] as? String == \"requestBioAuth\" {\n let lAContext = LAContext()\n Task {\n do {\n let success = try await lAContext.evaluatePolicy(\n .deviceOwnerAuthenticationWithBiometrics,\n localizedReason: \"Authenticate to change blocked sites\"\n )\n self.reply(context: context, success: success)\n } catch {\n self.reply(context: context, success: false)\n }\n }\n }\n }\n\n private func reply(context: NSExtensionContext, success: Bool) {\n let response = NSExtensionItem()\n response.userInfo = [SFExtensionMessageKey: [\"success\": success]]\n context.completeRequest(returningItems: [response], completionHandler: nil)\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "w3.org — W3C WebExtensions Community Group",
+ "url": "https://www.w3.org/community/webextensions/"
+ },
+ {
+ "title": "Packaging and distributing Safari Web Extensions with App Store Connect",
+ "url": "https://developer.apple.com/documentation/SafariServices/packaging-and-distributing-safari-web-extensions-with-app-store-connect"
+ },
+ {
+ "title": "WebKit.org – Report issues to the WebKit open-source project",
+ "url": "https://bugs.webkit.org"
+ },
+ {
+ "title": "Submit feedback",
+ "url": "http://feedbackassistant.apple.com"
+ },
+ {
+ "title": "MDN Web Docs - Web Extensions API",
+ "url": "https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/216/5/4fceecc8-1e28-465c-b894-fd0d03067c18/downloads/wwdc2026-216_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/216/5/4fceecc8-1e28-465c-b894-fd0d03067c18/downloads/wwdc2026-216_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "204",
+ "year": "2026",
+ "title": "What’s new in WebKit for Safari 27",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/204"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:13.560Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-219.json b/data/wwdc/videos/2026-219.json
new file mode 100644
index 0000000..8f62240
--- /dev/null
+++ b/data/wwdc/videos/2026-219.json
@@ -0,0 +1,89 @@
+{
+ "id": "219",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/219/",
+ "title": "Enhance the accessibility of your reading app",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Accessibility & Inclusion",
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi! My name is Josh, and I'm a Software Engineer on the Accessibility team. Today, I'm going to talk about how to make your long form text or reading app accessible to everyone on Apple platforms.\n\nReading long-form content is fundamentally different from navigating UI: it's about moving fluidly through text, not just moving between UI elements like controls.\n\nApple's frameworks come built-in with accessible text in mind. But, there's more work you can do as a developer to enrich and extend the accessibility experience with long form text.\n\nToday, I'm going to share some best practices and techniques for you to consider as you build your long-form content.\n\nFirst, I'll talk about what characteristics make up a great reading experience for someone using VoiceOver, or another assistive technology. Then, I'll show how you can use and extend views from UIKit and SwiftUI using rich APIs designed specifically for the reading experience.\n\nAnd last, I'll cover how you can make custom text in your app accessible to VoiceOver, Speak Screen, or the Accessibility Reader.\n\nStarting off, I'll discuss what makes a great accessible experience in an app that displays long form content. Today, I want to build an app so that I can share recommendations and travel tips for one of my favorite cities: Chicago.\n\nMy app has paginated content, with multiple paragraphs and text that wraps across multiple lines. I want to make sure that anyone using an assistive technology has a great experience with this app.\n\nIn this session, I'll focus on two popular assistive technologies built into Apple platforms: VoiceOver and Speak Screen.\n\nVoiceOver is Apple's built-in screen reader, designed for individuals who are blind or low vision. When I activate it, I can hear whatever is highlighted by the cursor. Morning. Heading. We started out our morning in Lincoln Park, strolling through the trails and admiring the views of the Chicago skyline.\n\nSpeak Screen is designed to read aloud all of the content on a given page, from top to bottom, highlighting while it is speaking. When turned on, I can initiate it by dragging down with two fingers from the top of the screen.\n\nMidday. At lunchtime, we walked along the Chicago river. The river-front path gave us great views of the city's magnificent architecture. Our favourite view was from the middle of the DuSable Bridge, where we could look straight down the river.\n\nKeeping these technologies in mind, I've made three goals to improve the interaction between those features and my app. Specifically, I want to make sure my app offers granular text navigation, so that VoiceOver and Speak Screen can fluidly move through the text. I also want to make sure I'm developing a continuous reading experience, so someone using an assistive technology doesn't encounter any interruptions. Lastly, I want to make sure my app provides comprehensive text selection.\n\nI'm going to focus on these main goals during the rest of this video, and make sure that my travel app satisfies them all.\n\nApple's frameworks provide many text components that are accessible right out of the box, so now I'll focus on what those are, what they provide you, and how you can extend them with additional functionality.\n\nBoth UIKit and SwiftUI provide accessible text views, that allow for line, word, and character navigation with VoiceOver and Speak Screen, alongside accessible text selection.\n\nYou might already be familiar with UIAccessibilityReadingContent, which is a great way to make full page content accessible. While I'm not going to focus on that protocol, you can still use and adopt it on top of everything I'll discuss today. To learn more, check out \"Creating an Accessible Reading Experience\". Today, I'm going to focus on UITextInput, a higher fidelity protocol that native text views use, and one that you can adopt as well on custom views.\n\nStandard text views across the system adopt the UITextInput protocol. With UIKit using UITextView on iOS will give you a rich text experience from the get go, as will TextEditor in SwiftUI. You can even use a simple SwiftUI Text view with selection enabled and benefit from these features on all Apple platforms. For those of you building macOS apps, using AppKit's NSTextView, or the SwiftUI Views discussed, will give you these benefits as well. When the constraints of your app allow, you should always try to use these components.\n\nIn my Travel Guide app, I chose to use UITextView's for each individual paragraph for its accessible properties. The unique layout I designed required me to use separate text views for each paragraph, rather than one that contained multiple paragraphs. I'll first assess how I'm doing with my goal of providing granular text navigation.\n\nVoiceOver has a setting that allows someone to choose what granularity of text is read when touching their finger on the screen. I have my preference set to lines, so with VoiceOver on, I am able to tap on any line on the screen and hear the line read aloud. We started out our morning in Lincoln Park, strolling through the trails and admiring... VoiceOver also provides options to change the way it moves, through a feature called rotors. The active rotor can be changed using a two finger rotation gesture to switch modes. Now, I will switch into the lines rotor using that gesture, and swipe down with one finger to find the next line on the page.\n\nLines.\n\n...the views of the Chicago skyline.\n\nNow, I'll try moving from the end of this paragraph to the first line of the next one.\n\nRight now, because each of these paragraphs are separate views, VoiceOver is stuck navigating by line within the paragraph, so someone can't fully explore the page by line and is why that sound is played.\n\nTo allow VoiceOver to move between paragraphs seamlessly, iOS 18 introduced the text navigation APIs. For each text element you want to connect, return the next and previous accessible text element that VoiceOver should navigate to.\n\nFor example, if I have two paragraph views, I can return Paragraph 2 from Paragraph 1's accessibilityNextTextNavigationElement method, and Paragraph 1 from Paragraph 2's accessibilityPreviousTextNavigationElement.\n\nHere, I have the controller for the pages in my Travel Guide app. During setup, when the configureNavigationElements codepath is run, I set the the proper navigation element in each direction, where applicable.\n\nNow that I've implemented it, VoiceOver can move past the end of one paragraph and onto the first line of the next. Before we left the park, we made sure to stop in the free zoo to check out all of the...\n\nAnd if you are using SwiftUI, starting in iOS 27, linking multiple text elements together using the accessibilityLinkedGroup modifier will achieve the same effect. For example, I have an equivalent page view here with two selectable text elements. By linking them both with accessibilityLinkedGroup using the same id and namespace, they will get the text navigation behavior.\n\nAnd if you are using AppKit on Mac, check out accessibilitySharedTextUIElements for a similar result. Now I know that VoiceOver can navigate around the pages of my app with different text granularities, without any unexpected gaps. But, I also set out to make sure that the continuous reading experience of my app is as smooth as possible.\n\nPaginated content, by nature, requires swiping between pages. The goal is for assistive technologies to interact with this content seamlessly, without the pages getting in the way. VoiceOver and speak screen both have features that allow someone to read all content, from start to end, without having to swipe.\n\nI'll explore my current app experience with Speak Screen. To do a read all, I'll swipe down from the top of my screen with two fingers. Midday. At lunchtime, we walked along the Chicago river. The river-front path gave us great views of the city's magnificent architecture. Our favorite view was from the middle of the DuSable Bridge, where we could look straight down the river.\n\nYou'll notice that Speak Screen stopped reading when it got to the bottom of the page. With paginated content, the best experience for a read all would be to move through all of the pages, advancing when appropriate, similar to an audiobook.\n\nHere I have my apps page view controller again. In my viewDidLoad override, I can apply the causesPageTurn trait to the last paragraph on my page, which is available in both UIKit and SwiftUI. And when paired with accessibilityScroll, Speak Screen and VoiceOver will automatically scroll the page when it reaches the end.\n\nI'll try using Speak Screen with that trait applied to my last paragraph.\n\nMidday. At lunchtime, we walked along the Chicago river. The river-front path gave us great views of the city's magnificent architecture. Our favorite view was from the middle of the DuSable Bridge, where we could look straight down the river. Evening. To end the day, we walked along the lakefront alongside groups of runners and cyclists. This gave us another great view of the skyline, towering over the waters of Lake Michigan.\n\nGreat! Speak Screen automatically moved focus to the next page when it finished reading, just like I'd expect.\n\nIf you recall from earlier, the last behavior I want to validate is how text selection works with VoiceOver.\n\nIn my app, I added a feature to save selected content for referencing later by using a button in the toolbar. I need to make sure this feature is accessible.\n\nI'm using a UITextView here, which already has accessible selection. You'll get the same experience by using TextEditor or text with selection enabled in SwiftUI. But, I also want people to be able to discover this 'Save recommendation' feature for their selected text. Visually, I added this button to my toolbar to save the current selection, but I can make this even more discoverable to VoiceOver through the edit rotor.\n\nTo do this, I can create a custom action and add it to VoiceOver's edit rotor by specifying the edit category when building my action. In my case, I'll override accessibilityCustomActions on my paragraph UITextView subclass, and add my Save Recommendation custom action alongside any actions from the super implementation.\n\nBe sure to use the edit category when you have a custom action that would be associated with text selection, rather than a generic action.\n\nNow, I'm going to turn VoiceOver on to try it out.\n\nTo select text, I'll switch to the text selection rotor, switch to word edit mode, and increase my selection by swiping right.\n\nText selection. Swipe right to expand selection. Swipe left to shrink selection. Word selection. \"Our favorite view was from the DuSable Bridge...\" selected.\n\nWith the text selected, I'll switch to the edit rotor, and activate the Save Selection action to save it.\n\nLines. Words. Characters. Edit.\n\nSave selection. Text saved. Great! Now I have an accessible app experience using system text views, and unlocked a new set of accessible reading features by adopting APIs. Line navigation across elements, continuous reading, and text selection all work as I'd expect.\n\nAnd the best part is that VoiceOver and Speak Screen aren't the only technologies to benefit from these changes. Since iOS 26, someone can open the Accessibility Reader, a tool designed to display text content for easier consumption. I have added the reader control to my control center, so pressing that opens my app's content in the Accessibility Reader. Implementing accessible text practices like I've shared so far will make the reader experience better for your content as well.\n\nThat's how you make standard text views accessible for reading content, and while I'd always recommend reaching for those views first, not every situation allows for them. Now I'm going to focus on what you should do when you are using custom text, or custom text elements, in your app to make them accessible.\n\nUsing custom text is a common pattern seen in dedicated reading apps to support advanced typography, share the code across a developer's applications, or displaying of scanned pages. When I travel, I like to take hand written notes on the places I go, so I've decided to replace my text views with scanned in pages from my notebook to give it a more personal touch. Unfortunately, this means that I've lost the accessibility behavior that UITextView gave me for free, including the most basic thing: reading out the text. Morning. Heading. Image.\n\nThe best way to make this content accessible is by using the UITextInput protocol, which can be adopted on any accessibility element. This protocol can make rendered text or text in images, for example, just as accessible as if they were in standard text views.\n\nFully implementing UITextInput gives you the same text experience you'd get as if you were using a native text view. You'll get line-by-line touch exploration with VoiceOver, granular navigation with the VoiceOver rotor and Speak Screen, and text selection.\n\nTo implement this protocol, you will have to solve for a few problems. You'll need to manage the geometry of your text, and compute selection rectangles for a given range, for example in the selectionRects method.\n\nWhen an assistive technology queries for a range in your view, you'll need to be able to return just that portion of text. And importantly, you'll need to provide a tokenizer, which will help manage navigation by line, sentence, word, or character.\n\nAnd these are just a few things you'll need to implement. To get all of the accessibility benefits of this protocol, make sure you implement it in its entirety.\n\nIn my app, I've implemented this protocol on my accessibility elements to make the text accessible. Here, I've implemented the selectionRects method from this protocol, which determines how VoiceOver and other assistive technology 'highlight' my content.\n\nSince I'm working with handwriting from an image, I can use the known height and width of each line to compute the approximate rects for a given range using a custom function, selectionRectFromImage. I then use this information to build out my array of selection rects, which I'll return from this method.\n\nI'll also complete implementations for the rest of the methods in the protocol, such as grabbing the right substring for textInRange and providing a tokenizer. In my case, I've subclassed the UITextInputStringTokenizer provided by UIKit to create a custom tokenizer that works with my implementation, so I'll return that.\n\nLastly, I want my selection experience to feel complete visually with selection handles and highlights. To do this, I added a UITextInteraction to my page view, and call the input delegate when my selection changes, so the system knows to update the visuals. This isn't required as part of the UITextInput implementation, but rounds out my app by matching expectations about the experience in a standard text view.\n\nUITextInput also works great in conjunction with the causesPageTurn and navigation element APIs, so I've made sure to implement those as well in the new version of my app.\n\nI've finished updating my app, taking the time to carefully implement the rest of the UITextInput protocol on my scanned in text and ensuring that I've implemented all of the APIs necessary. So, now I am going check out the VoiceOver experience.\n\nFirst, I'm going to navigate by line.\n\nLines. We started out our morning in Lincoln Park, admiring the views of the Chicago skyline. The zoo had lots of animals.\n\nNow, I will select some text by switching into the text selection rotor and swiping right. Text selection. Swipe right to expand selection. Swipe left to shrink selection. Line selection. \"The zoo had lots of animals\" selected. Lines. Words. Characters. Edit. Save selection. Text saved.\n\nAnd finally, I can try a read-all. Morning. Heading. We started out our morning in Lincoln Park, admiring the views of the Chicago skyline. The zoo had lots of animals. Midday. Heading. At lunchtime, we walked along the Chicago river. The view from the DuSable Bridge was perfect for photos! Amazing! It all works seamlessly.\n\nNow that I've covered what makes a great reading experience, and the APIs that make it possible, it's time for you to examine your own app.\n\nAudit your app with VoiceOver on, trying out the read all gesture, navigate using the lines rotor, and selecting text. If you are using standard text views, consider adopting causesPageTurn and the text navigation element APIs for a smooth cross-page reading behavior. If you use custom rendered text, adopt UITextInput. Doing this work will enable a great experience for everyone who downloads your app. Happy reading!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "7:29",
+ "title": "Link text elements together with navigation APIs",
+ "language": "swift",
+ "code": "// Link text elements together with navigation APIs\n\nimport UIKit\n\nclass TravelGuidePageController: UIViewController {\n\n var paragraphs: [TravelGuideParagraph]\n\n func configureNavigationElements() {\n for (index, paragraph) in paragraphs.enumerated() {\n if index + 1 < paragraphs.count {\n paragraph.accessibilityNextTextNavigationElement = paragraphs[index + 1]\n }\n if index - 1 >= 0 {\n paragraph.accessibilityPreviousTextNavigationElement = paragraphs[index - 1]\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "7:59",
+ "title": "Link text elements together with a linked group",
+ "language": "swift",
+ "code": "// Link text elements together with a linked group\n\nimport SwiftUI\n\nstruct PageView : View {\n @Namespace private var pageNamespace\n var paragraphs: [String\n var pageNumber: Int\n\n var body: some View {\n Text(paragraphs[0])\n .textSelection(.enabled)\n .accessibilityLinkedGroup(id: pageNumber, in: pageNamespace)\n\n Text(paragraphs[1])\n .textSelection(.enabled)\n .accessibilityLinkedGroup(id: pageNumber, in: pageNamespace)\n }\n}"
+ },
+ {
+ "timestamp": "9:50",
+ "title": "Turn pages automatically after reading",
+ "language": "swift",
+ "code": "// Turn pages automatically after reading\n\nimport UIKit\n\nclass TravelGuidePageController: UIViewController {\n\n override func viewDidLoad() {\n super.viewDidLoad()\n self.lastParagraphView.accessibilityTraits.insert(.causesPageTurn)\n }\n\n override func accessibilityScroll(_ direction: UIAccessibilityScrollDirection) -> Bool {\n moveToPage(direction)\n var scrollString = \"Page \\(currentPage) of \\(pages.count)\"\n UIAccessibility.post(notification: .pageScrolled, argument: scrollString)\n return true\n }\n}"
+ },
+ {
+ "timestamp": "11:45",
+ "title": "Add actions to the editor rotor",
+ "language": "swift",
+ "code": "// Add actions to the editor rotor\n\nimport UIKit\n\nclass TravelGuideParagraph: UITextView {\n\n override var accessibilityCustomActions: [UIAccessibilityCustomAction]? {\n get {\n let saveAction = UIAccessibilityCustomAction(name: \"Save Recommendation\") { _ in\n self.saveRecommendation()\n }\n saveAction.category = UIAccessibilityCustomAction.editCategory\n return (super.accessibilityCustomActions ?? []) + [saveAction]\n }\n set { }\n }\n\n private func saveRecommendation() -> Bool {\n ...\n return true\n }\n}"
+ },
+ {
+ "timestamp": "16:10",
+ "title": "Adopt UITextInput",
+ "language": "swift",
+ "code": "// Adopt UITextInput\n\nimport UIKit\n\nclass ScannedPage: UIView, UITextInput {\n\n override init(frame: CGRect) {\n super.init(frame: frame)\n let interaction = UITextInteraction(for: .nonEditable)\n interaction.textInput = self\n addInteraction(interaction)\n }\n \n func selectionRects(for range: UITextRange) -> [UITextSelectionRect] {\n var rects: [UITextSelectionRect] = []\n\n let startLine = lineIndex(for: range.start)\n let endLine = lineIndex(for: range.end)\n\n for line in startLine...endLine {\n let rect = selectionRectFromImage(for: range, in: line)\n rects.append(rect)\n }\n\n return rects\n }\n \n func text(in range: UITextRange) -> String? {\n let nsRange = nsRange(from: range)\n guard let range = Range(nsRange, in: scannedText) else {\n return nil\n }\n return String(scannedText[range])\n }\n\n var tokenizer: any UITextInputTokenizer { CustomHandwritingTokenizer(textInput: self) }\n\n weak var inputDelegate: UITextInputDelegate?\n \n var selectedTextRange: UITextRange? {\n // Update visuals when assistive technologies change selection\n willSet { inputDelegate?.selectionWillChange(self) }\n didSet { inputDelegate?.selectionDidChange(self) }\n }\n \n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "accessibilityNextTextNavigationElement",
+ "url": "https://developer.apple.com/documentation/ObjectiveC/NSObject-swift.class/accessibilityNextTextNavigationElement"
+ },
+ {
+ "title": "editCategory",
+ "url": "https://developer.apple.com/documentation/UIKit/UIAccessibilityCustomAction/editCategory"
+ },
+ {
+ "title": "accessibilityLinkedGroup(id:in:)",
+ "url": "https://developer.apple.com/documentation/SwiftUI/View/accessibilityLinkedGroup(id:in:)"
+ },
+ {
+ "title": "causesPageTurn",
+ "url": "https://developer.apple.com/documentation/SwiftUI/AccessibilityTraits/causesPageTurn"
+ },
+ {
+ "title": "UITextInput",
+ "url": "https://developer.apple.com/documentation/UIKit/UITextInput"
+ },
+ {
+ "title": "Accessibility for UIKit",
+ "url": "https://developer.apple.com/documentation/UIKit/accessibility-for-uikit"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/219/4/da70a3a7-e193-4513-904f-991788c1fa81/downloads/wwdc2026-219_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/219/4/da70a3a7-e193-4513-904f-991788c1fa81/downloads/wwdc2026-219_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "248",
+ "year": "2019",
+ "title": "Creating an Accessible Reading Experience",
+ "url": "https://developer.apple.com/videos/play/wwdc2019/248"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:12.978Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-220.json b/data/wwdc/videos/2026-220.json
new file mode 100644
index 0000000..45e3993
--- /dev/null
+++ b/data/wwdc/videos/2026-220.json
@@ -0,0 +1,81 @@
+{
+ "id": "220",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/220/",
+ "title": "Refine accessibility for custom controls",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Accessibility & Inclusion",
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Khin, a software engineer on the accessibility team.\n\nCustom UI controls empower people to do unique and creative things in your app. They let people use gestures and interactions that go beyond standard controls.\n\nAt Apple, we believe that technology works best when it works for everyone. That means bringing a great experience to people using assistive technologies like VoiceOver, Switch Control, and more.\n\nMaking control accessible ensures that everyone gets access to the very thing your app was built to do.\n\nIn this session, I'll explore how you can make any control in your app accessible. First, I'll start by explaining some guiding principles you can use. And then I'll show you how those get applied to a few complex controls.\n\nNow let's talk about the guiding principles. Consider this slider. It's a standard slider from SwiftUI.\n\nEven if this is your first time encountering it, you may already know a lot about it from its appearance.\n\nIt contains a track and on top of it, a handle. The handle appears about halfway along its track. The handle also seems like someone can grab it, so it can be moved by dragging. Using this visual information, you gather a lot of implicit cues about this control. You can tell that this control represents a continuous value.\n\nYou also know what the current value is. In this case, about half of the maximum.\n\nYou can also identify the gesture for actions, like changing its value.\n\nThe handle appears to be draggable, and when you do, the feedback is immediate, the track fills, and the handle moves, giving clear feedback that something happened. Nobody had to explain any of that. It was understood at a glance. Your brain processed the shape, the position, the expected behavior, all in a fraction of a second.\n\nThat's how powerful visual information is.\n\nNow, what if someone can't see the screen? The track, the handle, the position, none of it is known. Not everyone will have access to visual information, so it's important to consider how people using assistive technologies will interact with controls like these.\n\nHere's how VoiceOver describes this slider. VoiceOver is a built-in screen reader on Apple platforms. It lets people who are blind or low vision use gestures to interact with their device. \"Brightness, 50%, adjustable.\" \"Swipe up or down with one finger to adjust the value.\" Using VoiceOver, someone is able to understand what the slider controls, says the label, \"Brightness\". It also indicates that this control has a value and it's currently set to 50%. It's also clear what actions someone can take. It reads adjustable and provides a hint on how to adjust the value. As the value changes, announces the new value in real time. That's the feedback someone gets.\n\nSomeone using VoiceOver can get a great experience, even without access to the control 's visuals.\n\nConsider the information people get from the visual form of a control. Use each of these as your guiding principle for your own accessibility experience. Ensure that the purpose of the control is understood. If it expresses a value, make that value available to assistive technologies. Make it clear what action someone can take and how they get feedback as they use the control.\n\nHere's another example, a custom control for my coffee maker's app. Some mornings are full cup mornings, some mornings are half. Depends if I wake up to my alarm or to screaming cats. So, I build an app that lets me control the amount of coffee brewed in with one simple gesture. I drug up for more coffee and drug down for less. The fill level represents how many ounces to be brewed.\n\nCurrently, this control doesn't provide any additional information for accessibility. As I swipe right to select the control, VoiceOver doesn't describe what it does or how someone changes its value. \"Settings.\" \"Button.\" \"6 ounces.\" \"Drag up or down on the cup.\" It's not clear how to interact with this control yet.\n\nBut don't worry. This can be proved in a few simple steps.\n\nTo do this, I'll revisit the guiding principles I explored earlier.\n\nHere's the implementation for the CoffeeDispenserView. Currently, it just declares a coffee level as a State and passes it to the slider. First, I'll mark the slider as an accessibility element. To give it a clear purpose, I use the accessibilityLabel modifier to call it \"Coffee Dispenser\". And then I'll use the accessibilityValue modifier to announce the current fill level.\n\nI also want to give VoiceOver the ability to adjust the coffee slider the same way a built-in slider does. I'll add the same interaction to this control. I start by adding a trait call .adjustable.\n\nThis tells VoiceOver that this control can be adjusted with a swipe. Then I define what each swipe does with the .accessibilityAdjustableAction Modifier. The closure provides a direction parameter, either .increment or .decrement. The closure should handle each case.\n\nWith those changes, here's the new VoiceOver experience.\n\nI'll swipe right to select the control and then swipe up and down to adjust the amount of coffee I want.\n\n\"Coffee dispenser, 6 ounces, adjustable.\" \"Swipe up or down with one finger to adjust the value.\" \"7 ounces.\" \"8 ounces.\" \"7 ounces.\" \"6 ounces.\" People can now adjust the coffee level using VoiceOver one ounce at a time.\n\nBut what if someone is feeling particular and wants to adjust by half an ounce? For precise control, VoiceOver has that build-in capability called the passthrough gesture.\n\nTo perform a passthrough gesture, someone using VoiceOver can double tap and hold to activate. The gesture starts at your control's accessibilityActivationPoint. As the person's finger moves around, VoiceOver sends touch events directly to the control.\n\nThis gives someone more precise, fine-grained control over their value.\n\nFor the coffee slider, the accessibility activation point is at the center by default. I'll set it to match the current fill level. This way, the gesture always starts right at the coffee level, giving room to adjust whichever direction makes sense.\n\nIt's also important to give people feedback during the passthrough gesture.\n\nTo do this, I post the accessibility announcement when the value changes. But not every change though. That would get noisy. Instead, I track what was last spoken and when. If the value has actually changed, and at least .3 seconds have passed, then I announce. Otherwise, I skip it.\n\nThis way, people using VoiceOver can hear meaningful updates.\n\nNow I'll double tap and hold, then move my finger up to try a passthrough gesture to fill my coffee level up to 9.5 ounces. \"Coffee dispenser, 6 ounces, adjustable.\" \"Swipe up or down with one finger to adjust the value.\" \"Six... 6.4 ounces.\" \"Six... Six... Se... Se... Seve... 8.3 oun... 8.4 ounces.\" \"8.5 oun... 8.6 ounces.\" \"8.7 ounces.\" \"8.8 ou... 9... 9.5 ounces.\" That's great. The controller is now draggable and gives meaningful updates. The experience now delivers on the guiding principles I shared earlier with the label, value, actions and announcements.\n\nNow that I've explored basic custom control, let's take a look at a few more complex examples.\n\nHere is one of my favorite features on iOS, Background Sounds. Let's me play ambient audio, like rain or ocean waves, to help me focus and relax.\n\nYou can find it right in the Accessibility Settings.\n\nInside settings, there is an equalizer control. It's a two-dimensional pad. You can move the handle at the center anywhere on the surface.\n\nThis pad has two dimensions you can move around to adjust the sound's tone. Visually, there are frequency and amplitude symbols for each axis. When you grab the handle, you can actually explore the space of these two values at the same time.\n\nWith the slider, the adjustable traits provide two actions, increment and decrement, on a single axis.\n\nBut with this control, there are two axes, horizontal and vertical. Adding the adjustable trait would only cover one direction, so it's not the ideal solution.\n\nThis is where custom actions come in. Custom actions let you expose common actions of a control.\n\nEach action has a label that VoiceOver reads out loud and a closure that runs when activated. Unlike the adjustable action, custom actions support any operation that you defined, not limited to a single axis.\n\nFor this equalizer pad, here's how custom actions are added. On the equalizer pad view, the accessibilityAction modifier is added four times. Each action has a descriptive name. Move up, move right, move down, move left.\n\nEach one moves a single axis by a fixed step clamped within this range. These actions make it possible to explore the space really just by performing actions that are already familiar to people who rely on assistive technologies.\n\nHere's the experience of using equalizer pad with custom actions. I'll swipe up and down to select an action and double tap to perform it. \"Filter chart, frequency 0, amplitude 10.\" \"Double tap and hold, then drag to adjust filters.\" \"The bounds of the chart axis are set to -100 to 100.\" \"Swipe up or down to select a custom action.\" \"Then double tap to activate.\" \"Move down.\" \"Move up.\" \"Move right.\" \"Move left.\" \"Move right.\" \"Move up.\" \"Frequency 0, amplitude 20.\" \"Frequency 0, amplitude 30.\" \"Frequency 0, amplitude 40.\" Someone can navigate to 2D space through actions, and the sound itself provides clear feedback.\n\nThe equalizer pad is a great example of how the platform applies each of the guiding principles to its own experiences.\n\nNow, I want to show you an app that I've been working on with its own custom control.\n\nIf you're anything like me, you love your pets and when you're away at work, you really miss them. To keep me company during the day, I built an app that lets me play with my cat right from my screen. Pet it, and it purrs. Tap it, and get a meow. Pinch it, and well, it's a cat — so it hisses right back at you! Just like the real thing. I haven't added any accessibility support for this control yet. I'll check out the default experience with VoiceOver. \"Cat fill. Space. Image.\" \"Touch the cat to interact.\" It just says an image on the screen. Patting, tapping, and pinching are all the gestures that this control supports, but VoiceOver doesn't know they exist.\n\nAll of that charm, the purring, the kneading, the angry meow, it's all there, waiting to be shared with people who rely on assistive technologies. VirtualCat is a Swift UI view containing the interactive cat surface. To express the control purpose, I add an accessibility label that reads \"Virtual Cat\". Then I set the accessibility value to the cat's current reaction.\n\nNow I need a way to explore the gestures for interacting with a cat to VoiceOver. People could use the pass-through gesture, but it might not be the best fit here. For example, people might want to do this action over and over again or use multiple gestures. So for this control, I'll use direct touch. The direct touch API marks a region of the screen as a direct touch area. When you add the allowsDirectInteraction trait, touch events pass straight to the control, rather than being processed by VoiceOver. This way, people can interact with the control directly, allowing them to use all the gestures it supports. You can customize this behavior with direct touch options. First option is .requiresActivation. When set, the control won't respond to direct touch until someone double taps.\n\nThis allows someone to drag their finger across the screen without accidentally activating it.\n\nAnd unlike the passthrough gesture, direct touch stays active until focus moves to another element.\n\nAnother option is .silentOnTouch. When set, VoiceOver stays completely silent when someone touches the area. This is for controls that provide their own audio feedback where VoiceOver speech would talk over the audio from the control itself.\n\nTo make this control interactive, I'll add the .accessibilityDirectTouch modifier with .requiresActivation option.\n\nKeep in mind, not everyone is going to be able to perform direct touch gestures. So whenever possible, try to expose other ways to interact with the control. For example, using custom actions.\n\nHere's the updated experience with VoiceOver. I can double tap to activate the direct touch and then I'll try patting, tapping, and pinching the cat.\n\n\"Virtual cat, sleeping.\" \"Activate to start direct touch interaction.\" \"Actions available.\" \"Virtual cat.\" \"Sleeping.\" With these changes, someone using VoiceOver knows the description of the control and gestures to perform a direct touch. They also get feedback from the interactions. This brings everything that's delightful about this app to people using assistive technologies.\n\nMake your own custom interactive controls accessible to everyone. Turn on VoiceOver, open your app to find opportunities for improvement. When building custom controls, consider whether people can understand the control purpose, value, actions, and feedback. Think about direct touch when your controls rely on gestures which pass-through isn't best supported for, and provide custom actions whenever possible.\n\nThis way, people using Switch Control, Voice Control, and other assistive technologies can access the same interactions.\n\nThanks for watching.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "5:01",
+ "title": "Improve accessibility for coffee dispenser",
+ "language": "swift",
+ "code": "// Improve accessibility for coffee dispenser\n\nimport SwiftUI\n\nstruct CoffeeDispenserView: View {\n @State var coffee: Double = 0.0\n var body: some View {\n CoffeeSlider(value: coffee)\n .accessibilityElement()\n .accessibilityLabel(\"Coffee Dispenser\")\n .accessibilityValue(\"\\(Int(coffee)) ounces\")\n .accessibilityAddTraits(.adjustable)\n .accessibilityAdjustableAction { direction in\n switch direction {\n case .increment:\n increaseCoffeeAmount()\n case .decrement:\n decreaseCoffeeAmount()\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "7:05",
+ "title": "Set the accessibility activation point",
+ "language": "swift",
+ "code": "// Set the accessibility activation point\nimport SwiftUI\n\nstruct CoffeeDispenserView: View {\n @State var coffee: Double = 0.0\n\n var body: some View {\n CoffeeSlider(value: coffee)\n .accessibilityActivationPoint(\n UnitPoint(x: 0.5, y: 1 - coffee)\n )\n }\n}"
+ },
+ {
+ "timestamp": "7:27",
+ "title": "Post accessibility announcements",
+ "language": "swift",
+ "code": "// Post accessibility announcements \n\nimport SwiftUI\n\nstruct CoffeeDispenserView: View {\n @State var coffee: Double = 0.0\n \n var body: some View {\n CoffeeSlider(value: coffee)\n // ...\n .onChange(of: coffee) { _, newValue in\n if sufficientTimeSinceLastAnnouncement() && valueHasChanged() {\n cacheLastSpokenValue(newValue)\n AccessibilityNotification\n .Announcement(newValue)\n .post()\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "10:13",
+ "title": "Add custom actions",
+ "language": "swift",
+ "code": "// Add custom actions\n\nimport SwiftUI\n \nstruct EqualizerView: View {\n var body: some View {\n EqualizerPad()\n .accessibilityActions(\"Move Up\") {\n increaseY(by: 10)\n }\n .accessibilityActions(\"Move Right\") {\n increaseX(by: 10)\n }\n .accessibilityActions(\"Move Down\") {\n decreaseY(by: 10)\n }\n .accessibilityActions(\"Move Left\") {\n decreaseX(by: 10)\n }\n }\n }"
+ },
+ {
+ "timestamp": "12:47",
+ "title": "Customize accessibility for the interactive cat surface",
+ "language": "swift",
+ "code": "// Customize accessibility for the interactive cat surface\n\nimport SwiftUI\n\nstruct VirtualCat: View {\n var cat: CatModel\n var body: some View {\n InteractiveCatSurface()\n .accessibilityLabel(\"Virtual Cat\")\n .accessibilityValue(cat.currentReaction.description)\n .accessibilityDirectTouch([.requiresActivation])\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Accessible controls",
+ "url": "https://developer.apple.com/documentation/SwiftUI/Accessible-controls"
+ },
+ {
+ "title": "Accessible descriptions",
+ "url": "https://developer.apple.com/documentation/SwiftUI/Accessible-descriptions"
+ },
+ {
+ "title": "Accessibility fundamentals",
+ "url": "https://developer.apple.com/documentation/SwiftUI/Accessibility-fundamentals"
+ },
+ {
+ "title": "Creating accessible views",
+ "url": "https://developer.apple.com/documentation/SwiftUI/creating-accessible-views"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/220/4/945f8d34-8427-4476-ae75-34edc4a9c3f9/downloads/wwdc2026-220_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/220/4/945f8d34-8427-4476-ae75-34edc4a9c3f9/downloads/wwdc2026-220_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "10073",
+ "year": "2024",
+ "title": "Catch up on accessibility in SwiftUI",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10073"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:13.102Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-221.json b/data/wwdc/videos/2026-221.json
new file mode 100644
index 0000000..0ea2b1c
--- /dev/null
+++ b/data/wwdc/videos/2026-221.json
@@ -0,0 +1,79 @@
+{
+ "id": "221",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/221/",
+ "title": "Prepare your tvOS apps for Dynamic Type",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Accessibility & Inclusion",
+ "Audio & Video"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Isis, and I'm an engineering manager on the accessibility team. This year, there is great news for accessibility on tvOS 27. Large Text support is now available, bringing system-wide text scaling to every app on the platform. Many people need or prefer different text sizes, and making your app dynamic helps everyone get a great experience. Your tvOS app customers have been waiting for this accessibility feature. And I'll show you how to make the most out of it in your apps. In addition, you'll be able to indicate support for Larger Text in your Accessibility Nutrition Labels for tvOS in the App Store. This is a great way to reach users who specifically look for accessible apps that support larger text.\n\nIn this session, I'll start by covering how Large Text works on tvOS and where people can find it. Then, I'll go into identifying small parts of your app that you may need to adjust for larger text sizes.\n\nAnd finally, I'll touch on a few examples of how to adapt layout in response to text size.\n\nLet's start with an overview. If you're familiar with Dynamic Type on iOS, you have a head start! Dynamic Type works the same way on tvOS.\n\nUIKit and SwiftUI can adapt text sizes automatically based on what someone prefers. This gives people control over the text size to match their needs and comfort.\n\nPeople can turn on larger text sizes in the Settings app. By navigating to Accessibility, Display, then Text Size.\n\nThey'll have the option of choosing text sizes starting at Large all the way up to Accessibility XXXL. I'm working on a media app that lets people discover and watch their favorite movies. Here's how this app adapts to these larger text sizes. This media app has a tab bar at top for navigation. There's a large title, with a few action buttons, and a gallery of movie posters to explore in a collection view below that.\n\nWith larger text sizes turned on, the navigation title at the top has become much larger.\n\nIn the tab bar, each label is now bigger, and all of the text throughout the interface has scaled up significantly. Standard UIKit and SwiftUI components like Labels, Buttons and Navigation tabBars handle this automatically. For your apps, your job is just to identify and update any custom elements that may need attention. When examining your app with Large Text, there are a few types of issues to identify. When your app runs with larger text sizes, you'll want to ensure that you avoid fixed font sizes that don't allow your text to scale.\n\nInterfaces that don't adapt to dynamic type, like hard-coded sizes or constraints, may cause issues like truncation of text or clipping of UI elements.\n\nYou may also need to adjust layouts for the padding and spacing needed when some elements grow to larger sizes.\n\nHere's an example from my media app where the description of a movie is displayed.\n\nThere is a title shown at the top, with text labels and controls towards the bottom.\n\nWith text set to the largest size, most of the text has grown larger… But, there's one text element on the left that isn't scaling. That is the caption that reads \"Signup information\" above the controls that let someone sign up, buy, or rent a movie. Now, a caption like this may have hard-coded text sizes for a specific layout reason. For example, it might have originally been in a container with static dimensions or rigid constraints. While this can seem appealing for creating predictable layouts, it's not recommended. This approach lacks flexibility and can't adapt to larger text. Instead, adapt the layout to be flexible and remove the hard-coded values. Here is the code representing the Description View of my media app.\n\nIt has a VStack, containing the \"Signup information\" caption, and an HStack containing the buttons and other text information about the movie. This code contains a few different challenges for larger text. It specifies a fixed font size, which will not adapt when someone changes their size preference.\n\nAnd on the Text view, there is a fixed width constraint to 300 points.\n\nAs text grows, 300 points may not be wide enough for the text, which could lead to text truncation.\n\nTo make this caption adjust to larger text sizes, I'll replace the hard-coded font style with a semantic text style. In this case, I'll use \"caption\".\n\nWith those changes, the text size is now dynamically growing, but the content is now truncated.\n\nTo fix this, replace fixed widths with flexible constraints that let the view grow as needed.\n\nOn the text view, I'll replace the fixed width with the parameter maxWidth and set it to infinity. This tells SwiftUI to use as much width as needed for the content.\n\nWith those changes, my text view scales beautifully with larger text, and has enough space to display its content. Perfect! To find these types of common issues in your app, search for hard-coded text sizes and migrate to standard styles instead.\n\nSearch your app for hard-coded height and width constraints and use flexible constraints to allow your content to grow.\n\nIf you're using UIKit, the approach is similar with one additional step. Replace hard-coded fonts with text styles, and set adjustsFontForContentSizeCategory to true. This tells UIKit to automatically update when preferences change.\n\nSometimes fixing hard-coded values isn't enough. You may want to adapt the layout in response to larger text, while preserving the original layout for default sizes.\n\nConsider this collection view. It contains six movie posters with a title underneath each. With a standard text size, the full title fits comfortably in the space below the image.\n\nWith Large Text enabled, there isn't enough room to fit six titles horizontally. This layout will need to be adapted when the text is larger. Here is the SwiftUI code for the view. It presents a horizontal scroll view, with a LazyHStack. Inside a container, I have a button for each cell.\n\nTo adapt this layout, I will first read the dynamicTypeSize environment key path. Then, I'll change the columnCount parameter on the cell in the containerRelativeFrame modifier.\n\nWhen larger text sizes are enabled, I'll set the layout to show 4 columns at a time, instead of 6.\n\nThis provides wider cell dimensions and gives each title more room to grow. To accommodate even longer text, consider a custom marquee strategy.\n\nHere's another example where I'll adjust layout in this app. This part of my app contains cards for different types of content, like a video of a beach, or a video of camping.\n\nEach of these content cards contains an image, the title, and a subtitle. With larger text, there isn't enough visual padding between elements, and text is easily truncated with the space that it has.\n\nOne solution is to provide a conditional layout for when larger sizes are turned on. Here's how a conditional layout can be achieved in SwiftUI.\n\nFirst, read the dynamicTypeSize environment value to detect when accessibility sizes are turned on.\n\nThen, create a VStackLayout or an HStackLayout depending on whether larger text is being used. AnyLayout lets you abstract over these two types. Use the new layout like any other stack. It will dynamically switch between horizontal and vertical depending on the text size.\n\nIn UIKit, use UIStackView and update the axis property based on preferredContentSizeCategory. isAccessibilityCategory.\n\nCall registerForTraitChanges and pass UITraitPreferredContentSizeCategory to update your layout in response to size changes while your app is running.\n\nHere it is in action in my app's content cards. When larger text sizes are enabled, the layout switches to a vertical stack. This lets the title and subtitle grow to the entire width of the cell, rather than sharing the width with the image. This layout also allows the cells to become taller to provide more room for the content.\n\nNow you're ready to test your app with larger text sizes on tvOS. Discover where you can make refinements by using system text styles and tailoring your layouts to prioritize text legibility.\n\nHere's your action plan. Use standard text styles instead of hard-coded fonts.\n\nTest systematically with Large Text enabled.\n\nAdapt layouts when needed for best experience. And indicate support for Larger Text in your app's Accessibility Nutrition Labels for tvOS.\n\nNow it's your turn to make your tvOS app accessible for everyone! Thanks for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "4:58",
+ "title": "Adopt standard text styles",
+ "language": "swift",
+ "code": "// Adopt standard text styles\n\nVStack(spacing: 20) {\n Text(\"Signup information\")\n .font(.caption.bold())\n .lineLimit(1)\n .foregroundStyle(.secondary)\n .frame(width: 300, alignment: .leading)\n HStack(alignment: .top, spacing: 40) { \n //* ... *//\n }\n}"
+ },
+ {
+ "timestamp": "5:10",
+ "title": "Use flexible constraints",
+ "language": "swift",
+ "code": "// Adopt standard text styles\n\nVStack(spacing: 20) {\n Text(\"Signup information\")\n .font(.caption.bold())\n .lineLimit(1)\n .foregroundStyle(.secondary)\n .frame(maxWidth: .infinity, alignment: .leading)\n HStack(alignment: .top, spacing: 40) { \n /* ... */\n }\n}"
+ },
+ {
+ "timestamp": "5:55",
+ "title": "Dynamic Type with text styles in UIKit",
+ "language": "swift",
+ "code": "// Hard coded text size in UIKit\n\ntitleLabel.font = UIFont.boldSystemFont(ofSize: 28)\n\n// Dynamic Type with text styles in UIKit\n\ntitleLabel.font = UIFont.preferredFont(forTextStyle: .headline)\ntitleLabel.adjustsFontForContentSizeCategory = true"
+ },
+ {
+ "timestamp": "7:09",
+ "title": "Adapt layout in response to dynamic type",
+ "language": "swift",
+ "code": "// A view that shows a collection of movie posters\n\nstruct MovieShelf: View {\n @Environment(\\.dynamicTypeSize) private var dynamicTypeSize\n var body: some View {\n ScrollView(.horizontal) {\n LazyHStack(spacing: 40) {\n ForEach(Asset.allCases) { asset in\n Button { \n /* ... */\n } label: {\n asset.portraitImage\n Text(asset.title)\n }\n .containerRelativeFrame(\n .horizontal,\n count: dynamicTypeSize.isAccessibilitySize ? 4 : 6,\n spacing: 40)\n }\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "8:07",
+ "title": "Provide a conditional layout for when larger sizes are turned on",
+ "language": "swift",
+ "code": "// A view that shows content in a card\n\nstruct CardContentView: View {\n @Environment(\\.dynamicTypeSize) private var dynamicTypeSize\n var asset: Asset\n\n var body: some View {\n let layout = dynamicTypeSize.isAccessibilitySize ?\n AnyLayout(VStackLayout(alignment: .leading, spacing: 10)) :\n AnyLayout(HStackLayout(alignment: .top, spacing: 10))\n layout {\n /* ... */\n }\n }\n}"
+ },
+ {
+ "timestamp": "8:31",
+ "title": "UIKit adaptive layout that responds to content size changes",
+ "language": "swift",
+ "code": "// UIKit adaptive layout that responds to content size changes\n\nclass AdaptiveLayoutViewController: UIViewController {\n let stackView = UIStackView()\n \n override func viewDidLoad() {\n super.viewDidLoad()\n updateLayout()\n\n let sizeTraits: [UITrait] = [UITraitPreferredContentSizeCategory.self]\n registerForTraitChanges(sizeTraits, action: #selector(updateLayout))\n }\n\n private func updateLayout() {\n if traitCollection.preferredContentSizeCategory.isAccessibilityCategory {\n stackView.axis = .vertical\n } else {\n stackView.axis = .horizontal\n }\n }\n\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Applying custom fonts to text",
+ "url": "https://developer.apple.com/documentation/SwiftUI/Applying-Custom-Fonts-to-Text"
+ },
+ {
+ "title": "Scaling fonts automatically",
+ "url": "https://developer.apple.com/documentation/UIKit/scaling-fonts-automatically"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/221/5/ada10ebd-34f8-4f57-92b5-4b3cd6281267/downloads/wwdc2026-221_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/221/5/ada10ebd-34f8-4f57-92b5-4b3cd6281267/downloads/wwdc2026-221_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "10074",
+ "year": "2024",
+ "title": "Get started with Dynamic Type",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10074"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:13.739Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-222.json b/data/wwdc/videos/2026-222.json
new file mode 100644
index 0000000..d00835a
--- /dev/null
+++ b/data/wwdc/videos/2026-222.json
@@ -0,0 +1,116 @@
+{
+ "id": "222",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/222/",
+ "title": "Meet the new MetricKit",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "System Services"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Yonni, and I'm an engineer on the MetricKit team. Great apps and games monitor and optimize their performance out in the real world, on real devices. MetricKit is the framework that can provide you real insights into the quality of your app's experience. In this session, I will start off with an introduction to MetricKit, including what's new in iOS 27.\n\nThen, I'll show you how to start receiving your first metric report.\n\nYour first diagnostic report. And finally, I'll explore how to get even more rich data by connecting performance problems to specific areas in your app.\n\nI'll start with an overview of the framework.\n\nOptimizing your app's performance is a process. You start by collecting data, and analyze it to identify problems. For each problem, you triage it to find the root cause, fix it, and go back to step one to monitor the results. MetricKit is the collection piece in that workflow.\n\nThe framework provides two types of data: metrics and diagnostics. Metrics give you a sense of whether an area of performance is improving or worsening overall, while diagnostics tell you which code path is causing a performance problem.\n\nIn iOS 27, the framework has been rebuilt from the ground up with a contextually rich and expressive modern Swift-first API.\n\nThe new MetricKit APIs are the future of the framework. All of the advances I'll be discussing today are exclusive to this new set of APIs. Metrics are your app's ongoing health signal.\n\nLaunch time, hangs, and animation metrics tell you how responsive and smooth your app feels. A slow launch can frustrate people and lead them to leave your app, while a fast launch gets people right into your app's core experiences.\n\nResource consumption metrics like CPU, GPU, Disk writes, and network transfers tell you how hard your app is working and how it's affecting device health.\n\nFor example, MetricKit launch times like the time to first draw metric, provide a histogram of launch counts that fall within certain time range buckets. This graph shows the time it took for the app to launch, every time someone opened it over the course of a day.\n\nMost of the launches took between 510 and 540 milliseconds, with a few outliers.\n\nYou can also track ongoing performance by deriving your own insights from the data MetricKit provides.\n\nFor example, MetricKit reported that for this app, it had a total hang time on average of 3 seconds while the app was used for 30 minutes. That information can be used to derive an average hang rate of 6 seconds per hour.\n\nIf you aggregate this data across all devices, this can give you a measurable signal of how your app's performance is trending.\n\nIn iOS 27, MetricKit can provide each metric as a function of the app's state.\n\nFor example, when measuring hang time in an app that has multiple tabs, MetricKit can provide this metric intersected with when the active tab is tab 1, tab 2 or tab 3. I'll go into this more later. In iOS 27, MetricKit now also provides a new metric - Metal frame rate. Frame rate is a key metric for game developers to understand render performance. To learn more about optimizing your game for the platform, check out the session \"Find and fix performance issues in your Metal game\".\n\nIn addition to metrics, MetricKit also provides diagnostics. Diagnostics contain useful information that helps you identify which code path caused a performance problem so you can investigate and fix it.\n\nIn iOS 27, MetricKit provides memory exception diagnostics. So when your app or extension is terminated for exceeding its memory limit, you get more insight on what happened.\n\nI'll dive in to explore how your app can get performance metrics.\n\nAs people use your app throughout the day, MetricKit continuously collects metrics like app launches, hangs, memory, and CPU and delivers it to your app in a daily report.\n\nIn this report, MetricKit provides an entry that spans the full day usage of the app. It also provides separate entries for smaller breakdowns, typically a few hours each. These smaller breakdowns are only present when there are metrics associated with them. Here's how the data is structured. Inside each interval, metrics are organized into metric groups. Each group represents an aspect of the system, things like .cpu, .memory, .display, and .gpu. Inside a group, you'll find individual performance metrics.\n\nI'll work through this in code.\n\nYour entry point is the MetricManager class. To receive reports, you await them through the metricReports property.\n\nThis setup should be done at app start up to avoid any data loss from delayed subscription. MetricManager should be kept alive so that the streams can continue to deliver reports as subsequent data becomes ready.\n\nWith just these few lines, your app is now receiving structured metric data.\n\nTypically, you may want to send these metrics to a server so you can examine your app's health across many devices. MetricReports are Codable, which makes it easy to encode into a format like JSON to send to the server.\n\nJust create a JSONEncoder and encode the entire report.\n\nIf you just want to access a particular group of metrics, or a particular value, you can also inspect the metric report further. To do this, iterate through your intervalEntries. This includes a full-day aggregated entry and smaller breakdown windows when available. Then, filter the metrics down to the group you are interested in. In this case, memoryMetrics contains only metrics in the memory group.\n\nFinally, switch over the metric cases to access the type of metric you're interested in, as well as the value of that metric in this report. In this case, the code only handles the peakMemory value. Perform this work in a detached task or a dedicated service class as soon as your app launches.\n\nGoing back to the workflow. You have now completed the collection phase. And ready to move on to analysis.\n\nAnalyzing metrics across all devices is a data science problem. To enable this analysis, you'll want to set up a server that can ingest each of these reports and aggregate them according to the dimensions that you care about.\n\nYou'll need to decide what the best statistical analysis is for the data that you want to generate, and the insights that you want to find.\n\nUsing your custom aggregation, this can give you a baseline, an idea of how your app is performing already. Then, monitor your aggregated metrics to detect when things are getting better or worse.\n\nI've shown how you could identify issues in your app using metrics. Now, I will cover how you can fix these issues using diagnostics.\n\nMetrics are a great way to monitor your app. As you collect metrics and monitor performance over time, you can enter the triage phase of your workflow. Diagnostics are particularly helpful in this phase.\n\nWhen something goes wrong, like a crash or a hang, the system captures a diagnostic on device. A diagnostic report packages up the details and delivers it immediately to your app through MetricKit.\n\nInside, you get useful information to triage the issue. For example, many diagnostics include backtraces that show you the exact call stack at the time of the event.\n\nOne of the most important diagnostics is for crashes. Crash diagnostics not only provide a backtrace, but they'll also indicate why your app was terminated and an exception type that tells you what kind of failure it is.\n\nIn iOS 27, a termination category now indicates how each crash was accounted for in metrics.\n\nThat way, if abnormal terminations are trending up, you can correlate those directly with individual diagnostics.\n\nIn this example, the symbolicated backtrace begins in the system at thread start. As execution flows downward, it crosses into the app's code. To find the crash site, you can follow the calls all the way down. Here, execution reaches the app's submitReport() function and stops. This indicates that this is the point of failure in the execution path.\n\nNow, you can use this information to target fixes in this function.\n\nTo get diagnostic reports, you await on the diagnosticReports of your MetricManager instance. Like MetricReports, start listening to this stream as soon as a your app launches on a detached task or a dedicated service class.\n\nJust like MetricReports, DiagnosticReports are Codable. This code awaits incoming diagnosticReports, then encodes them into JSON using a JSONEncoder.\n\nNow, you can send all of this diagnostic information to your analytics server.\n\nDiagnostics reports are also structured, so you can pick and choose what you want to receive. For example, this code awaits diagnosticReports, and switches on the cases of different types of diagnostics. In the case of a crash diagnostic, it extracts the backtrace, the reason, and the category. Now, this information can be processed by the app, like sending it up to a server. In the case of hang diagnostics, it can use the hang case to process that report differently.\n\nI've covered how to get metric and diagnostic reports for your app. Next, I'm going to show you how to contextualize this data.\n\nSo far, the metrics and diagnostics provided by MetricKit, represent the overall picture of the app's performance. But, to investigate individual issues, you might need richer, more granular data that tells you more about the state of the app, like what the user flow is or how the app is configured. MetricKit can give you data contextualized to meaningful information you defined about your app.\n\nHere's an example. I have an expense reporting app that allows employees to scan receipts, submit expenses and track their spending by categories and budgets. These functions are organized in a Reports tab and a Spending tab.\n\nI'm interested in the scroll hitch metric. It indicates that during the course of the day, the app had a total hitch time of 4.5 seconds while scrolling for 5 minutes. That is a hitch rate of 15 milliseconds per second. But that's an average scroll hitch rate over all app usage, even if someone is going back and forth between the Reports tab and the Spending tab. To know what conditions of the app this metric was collected under, you can report app states through the StateReporting framework. I'll explore that now.\n\nStates are information you define that describes your app's configuration or behavior, so that MetricKit can aggregate metrics as a function of those characteristics. As people use your app, parameters might change depending on how they're using it.\n\nIn the expense app, people might move between tabs during their usage. They could be on the Reports tab to add a new expense, leave the app, and come back to the Spending tab when they want to check on their daily meal budget. As the app transitions between these states, it reports these transitions. Then, MetricKit can intersect them with metric and diagnostic data.\n\nYou can also add more details for each state by adding a custom structured type. For the expense app, this could be details about the items in the view, like whether the list of transactions is considered a small, medium or large list and whether the transactions on that list are sorted.\n\nNow, instead of getting a single blended metric across all of these states, like a total scroll hitch rate of 15 ms/s, metrics are reported for each individual state. And in the expense app, there are individual metrics for each tab. In this example, scrolling on the Spending tab was incredibly smooth, with a hitch rate of just 1 ms/s. But, when scrolling through the Reports tab, the hitch rate spiked to 71 ms/s. With this granularity, you can make a much more focused conclusion: the Spending tab is performing great! But the Reports tab is experiencing critical interruptions, and that's exactly where your optimization effort should focus.\n\nEach state you provide is scoped to a domain. A domain describes a function or area of an app. A domain can only have one active state at a given time.\n\nSeparate domains allow multiple states to be in flight at the same time. In the expense app, I'm testing out an experimental change, and I want to know if it helps with performance. With the experiment turned on, expenses are fetched in small batches from the database. Turned off, it uses larger batches instead. By placing the tab state and the batch size state in separate domains, MetricKit will deliver separate metrics for each tab and each batch size.\n\nStates follow a transition model. Your app reports the state it's moving to, and MetricKit tracks how long it remains in that state. There's no start or end pairs - the app reports the condition it is in, at any given time.\n\nTo report states in your app, start by importing the StateReporting framework. Then, create a domain - typically a reverse DNS string - and register it when you set up your MetricManager instance.\n\nFinally, report the transitions as your app enters the states that you define. In this case, the app transitions to a state identified by the string \"Reports\".\n\nIf you want to further granularize your data, you can provide additional structured information for these states by defining your own struct with the ReportableMetadata macro. Then, create a new StateReporter with this metadata type.\n\nFinally, report transitions by including the label and your custom type. This example transitions to the \"Reports\" state, and also provides a ViewConfiguration struct that includes values for the listSize and whether items on this list are sorted.\n\nBefore adding states to your app, your metric report provides broad metrics across all usages of your app. The stateEntries property contains state-aware metrics. This is empty when there are no states reported. After adding states, your MetricReport will now have StateEntry values. State entries provide another way to perceive your app's metrics. Each state has its own StateEntry with metric values aggregated across the time spent in that individual state. When you're ready to send this data to your analytics server, you can choose to group your MetricReport data by state reporting domain. Configure your JSONEncoder to group state entries by each domain. Set the key encodingFormatKey on the encoder's userInfo property to the value byStateReportingDomain. Now, when the report is encoded, both state entries and interval entries, will contain your app's performance metrics, grouped by each domain and state that exists in the report.\n\nHere are some best practices to keep in mind when defining states.\n\nDomains should be narrowly scoped so that each app area can have its state and ability to understand data for those states.\n\nState transitions should represent stable, meaningful phases, not transient UI events.\n\nCarefully consider what each state means, so that if a regression appears, the state will give enough information to target your fix. Carefully plan out the number of state transitions in your app and how you plan to interpret the data that each state and domain generates. Too many states can result in data that's too granular and can actually make it harder to interpret the overall picture. There are also upper limits to the number of states to minimize overhead.\n\nFinally, use the Points of Interest instrument to validate that the states you report match your expectations before you ship.\n\nMetricKit lets you find and fix performance problems faster than ever. Continue to monitor the data on an ongoing basis to target and plan your performance work.\n\nUse MetricManager to start collecting performance metrics to monitor your app's health.\n\nAnalyze diagnostics to identify specific opportunities for improvements.\n\nContextualize the data you get by reporting important states of your app.\n\nExplore the new types of data provided by MetricKit, like memory diagnostics and Metal frame rate metric.\n\nFinally, if you're using the MXMetricManager API, migrate over to the new MetricManager API to take advantage of all these new capabilities. Thank you and have a great WWDC!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "4:59",
+ "title": "Receive metrics from MetricKit",
+ "language": "swift",
+ "code": "// Receive metrics from MetricKit\n\nimport MetricKit\n\nlet manager = MetricManager()\n\nfor await report in manager.metricReports {\n processReport(report)\n}"
+ },
+ {
+ "timestamp": "5:25",
+ "title": "Send your metrics to the server",
+ "language": "swift",
+ "code": "// Send your metrics to the server\n\nimport MetricKit\n\nfor await report in manager.metricReports {\n let jsonData = try JSONEncoder().encode(report)\n sendToServer(jsonData)\n}"
+ },
+ {
+ "timestamp": "5:44",
+ "title": "Access your performance metrics",
+ "language": "swift",
+ "code": "// Access your performance metrics\n\nimport MetricKit\n\nfor await report in manager.metricReports {\n let intervalEntries = report.intervalEntries\n let fullDayEntry = intervalEntries.fullDayEntry\n \n for entry in intervalEntries {\n let memoryMetrics = entry.values.filter { $0.metricGroup == .memory }\n \n for metric in memoryMetrics {\n switch metric {\n case .peakMemory(let peak):\n processPeakMemory(peak)\n default: break\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "8:59",
+ "title": "Receive diagnostics",
+ "language": "swift",
+ "code": "// Receive diagnostics\n\nimport MetricKit\n\nlet manager = MetricManager()\n\nfor await report in manager.diagnosticReports {\n processReport(report)\n}"
+ },
+ {
+ "timestamp": "9:14",
+ "title": "Send your diagnostic data to the server",
+ "language": "swift",
+ "code": "// Send your diagnostic data to the server\n\nimport MetricKit\n\nfor await report in manager.diagnosticReports {\n let jsonData = try JSONEncoder().encode(report)\n sendToServer(jsonData)\n}"
+ },
+ {
+ "timestamp": "9:39",
+ "title": "Access your diagnostic data",
+ "language": "swift",
+ "code": "// Access your diagnostic data\n\nimport MetricKit\n\nfor await report in manager.diagnosticReports {\n switch report.result {\n case .crash(let crash):\n let backtrace = crash.callStackTree\n let reason = crash.terminationReason\n let category = crash.terminationCategory\n processCrash(backtrace: backtrace, reason: reason, category: category)\n case .hang(let hang):\n processHangDiagnostic(hang)\n default: break\n }\n}"
+ },
+ {
+ "timestamp": "13:57",
+ "title": "Receive MetricKit data with states",
+ "language": "swift",
+ "code": "// Receive MetricKit data with states\n\nimport MetricKit\nimport StateReporting\n\nlet domain = StateReportingDomain(\"com.metrickitsample.tabs\")\nlet manager = MetricManager(enabledStateReportingDomains: [domain])\n\n\n// Report transitions throughout the app\n\nlet reporter = StateReporter.reporter(for: domain.rawValue)\nreporter.reportTransition(to: \"Reports\")"
+ },
+ {
+ "timestamp": "14:21",
+ "title": "Define custom structured types",
+ "language": "swift",
+ "code": "// Define custom structured types\n\nimport StateReporting\n\n@ReportableMetadata\nstruct ViewConfiguration {\n let listSize: String\n let isSorted: Bool\n}\n\nlet reporter = StateReporter.reporter(\n for: domain.rawValue,\n stableMetadata: ViewConfiguration.self\n)\n\nreporter.reportTransition(\n to: \"Reports\",\n stableMetadata: ViewConfiguration(listSize: \"large\", isSorted: false)\n)"
+ },
+ {
+ "timestamp": "15:29",
+ "title": "Send encoded metric reports to the server",
+ "language": "swift",
+ "code": "// Send encoded metric reports to the server\n\nimport MetricKit\n\nfor await report in manager.metricReports {\n let encoder = JSONEncoder()\n \n let formatKey = MetricReport.encodingFormatKey\n encoder.userInfo[formatKey] = MetricReport.EncodingFormat.byStateReportingDomain\n \n let jsonData = try encoder.encode(report)\n sendToServer(jsonData)\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Getting started with StateReporting",
+ "url": "https://developer.apple.com/documentation/StateReporting/getting-started-with-statereporting"
+ },
+ {
+ "title": "Analyzing app performance with MetricKit",
+ "url": "https://developer.apple.com/documentation/MetricKit/analyzing-app-performance-with-metrickit"
+ },
+ {
+ "title": "Monitoring app performance with MetricKit",
+ "url": "https://developer.apple.com/documentation/MetricKit/monitoring-app-performance-with-metrickit"
+ },
+ {
+ "title": "Track performance by app state using MetricKit",
+ "url": "https://developer.apple.com/documentation/MetricKit/track-performance-by-app-state-using-metrickit"
+ },
+ {
+ "title": "MetricKit",
+ "url": "https://developer.apple.com/documentation/MetricKit"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/222/4/86b76599-f095-4bd8-8004-f1dbd1bacb84/downloads/wwdc2026-222_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/222/4/86b76599-f095-4bd8-8004-f1dbd1bacb84/downloads/wwdc2026-222_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "388",
+ "year": "2026",
+ "title": "Find and fix performance issues in your Metal games",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/388"
+ },
+ {
+ "id": "268",
+ "year": "2026",
+ "title": "Profile, fix, and verify: Improve app responsiveness with Instruments",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/268"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:13.609Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-223.json b/data/wwdc/videos/2026-223.json
new file mode 100644
index 0000000..c6a452e
--- /dev/null
+++ b/data/wwdc/videos/2026-223.json
@@ -0,0 +1,114 @@
+{
+ "id": "223",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/223/",
+ "title": "Live Activities essentials",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Adi, and I'm a System Experience Engineer.\n\nToday, I'll cover the essentials for building Live Activities and how to make them shine on every screen. I'll start with an overview of the experience that Live Activities offer, and I'll go into how your app can create them and keep them up-to-date.\n\nThen, I'll touch on how you can further optimize them for your app.\n\nLive Activities are a great way for apps to provide timely and glanceable updates for something that's happening right now - like this one, from the MLB app. It keeps track of someone's favorite teams and shows a Live Activity when they're playing. People stay up-to-date with the score, the current inning, and key game updates, right from the Lock Screen, And on the Home Screen or when using apps, they appear right in the Dynamic Island, so people never miss a thing.\n\nLive Activities can also expand to show more information from the Dynamic Island. This occurs when an alerting update happens or when someone long-presses on it. The expanded view provides even more space to let people know about essential updates.\n\nAnd in iOS 27, Live Activities are visible in the Dynamic Island, when in portrait and landscape.\n\nThey also appear in other places too, like in StandBy, when iPhone is in landscape and charging.\n\nWhen your app runs a Live Activity on iPhone, it automatically appears on other Apple devices. Including right on Apple Watch, in the Smart Stack.\n\nIn the macOS menu bar or right on the CarPlay Dashboard. This makes it super easy for people to glance at the key information they need from your app, no matter where they are.\n\nTo bring Live Activities to your app, I'll cover how they get created, and how your app keeps them up-to-date.\n\nIt starts by planning your data model so that your Live Activity's updates can be fast and efficient. Then, create the basic views in each of its key presentations. Finally, I'll provide updates, either from the app when it's running or through push notifications in the background. I'll start with the data model.\n\nTo explore how to do this, I'll add a Live Activity to an app that I'm working on. It lets me order coffee from a local coffee shop. I can pick my favorite drink and customize it, just the way I like it. When I'm done, I send my order to the store for pickup.\n\nThe first step to building a Live Activity is to come up with a great design.\n\nThey're meant to provide immediate, glanceable information. You'll want to craft a design that prioritizes the key things people need to know over time. For inspiration, check out the Human Interface Guidelines for help designing your Live Activity and check out the session, \"Design dynamic Live Activities\". For this app, I'll use my initial designs for the Lock Screen to start planning the data model. It will have different presentations for when the order is placed, when it's being worked on, when it's ready for pickup, and a brief opportunity to leave feedback after I'm done. I'll need to consider which parts of the data are static, and which parts of the data are dynamic. That's because Live Activities treat static and dynamic data differently in order to provide efficient updates. Only the dynamic data can be updated during the lifetime of a Live Activity.\n\nData that never changes will be contained in a struct, that conforms to the ActivityAttributes protocol. Values that do change over time, are part of a separate ContentState struct.\n\nIn the coffee order, there are a few things that are static and won't change. An order always stays with the same coffee shop, so the name of the shop is static. The drink that someone orders also won't change but the status of the order, as well as the remaining time, are dynamic. The app will update those values over time.\n\nTo create a data model for this Live Activity, I'll import ActivityKit and create a DrinkOrderAttributes struct conforming to ActivityAttributes.\n\nThen, I'll add properties for all of the static data: the name of the shop, the drink for the order, and a unique identifier that my server uses to track each order.\n\nThen, I'll add a ContentState struct containing the dynamic data.\n\nThis includes the order's phase, like whether it's being prepared, or ready for pickup, as well as the estimated ready time, and the rating left for the order.\n\nAnd that's it! Now that I have the data model in place, I'll build the views for the Live Activity.\n\nThe interface for a Live Activity is built using WidgetKit. To get started, add a widget extension to your app, if you don't already have one. There, you'll provide an ActivityConfiguration that describes its views. Each presentation of Live Activity is a SwiftUI view, which reads the attributes and content state provided to it. If you're new to SwiftUI, check out the video \"SwiftUI essentials\".\n\nFor the drink order, I'll start by creating an ActivityConfiguration in my widget extension.\n\nBy specifying the DrinkOrderAttributes type, these views will be associated with the right Live Activity.\n\nThe content closure provides the SwiftUI view that should appear. In this case, I've a view called ActivityView. To show the right values, like whether the coffee is being prepared or ready, it uses the context parameter. The context contains the attributes and the most recent content state that should be rendered. Next, I'll provide views for the Dynamic Island.\n\nThe first three are: compactLeading, compactTrailing, and minimal views. These views are small and appear when someone isn't actively interacting with it. Carefully consider which information is essential from the larger presentation to appear in these views.\n\nFor the coffee order, the leading view shows a symbol for the type of drink I ordered, while the trailing view has a label for the order's phase. I'll also provide the minimal view. This view may appear when multiple Live Activities are running.\n\nMinimal views should provide the most essential information that someone will glance at. For this order, it provides a circular gauge showing the time remaining.\n\nFinally, I'll build up the expanded view in the Dynamic Island. This view is much larger and can accommodate more information, similar to the Lock Screen view.\n\nIt consists of multiple regions that surround the sensors on iPhone. In each closure, I'll provide a view for each of the regions I need in a DynamicIslandExpandedRegion block.\n\nNow that the data model and views are built, it's time to start the Live Activity and keep it up-to-date.\n\nLive Activities can be started in a few different ways. Using the ActivityKit framework, you can start one directly any time your app is running in the foreground or a Live Activity can be scheduled to start in advance at a specific time. Alternatively, you can also start it from a push notification.\n\nThe simplest way is to use ActivityKit. For my app, I'll start by checking if Live Activities are authorized. Then, I'll create an instance of the DrinkOrderAttributes struct for the order. I'll fill in the static data, like the coffee shop name and the drink ordered.\n\nThen, I'll construct the contentState, which contains the initial values for the dynamic data of this Live Activity.\n\nI'll set the first phase of the order, the ordered phase, and most drinks are ready in 15 minutes... so I'll set an initial ready time of 15 minutes from now.\n\nThe contentState of a Live Activity also has a staleDate. A staleDate lets you specify when this content should be considered out-of-date. When the content is stale, your Live Activity views can indicate that. For now, I won't set a staleDate for a coffee order.\n\nThen, I'll request the system start a Live Activity using the attributes and content I've defined.\n\nWhile the Live Activity is running, it's just as easy to update. Call the .update method on the activity with a new ContentState, as well as a new staleDate. Live Activities can also be updated with push notifications. There are two strategies to choose from.\n\nFirst, you can broadcast updates. This is a great choice when you have hundreds, thousands or more people running the same Live Activity at the same time. With this strategy, your server sends updates to everyone using a broadcast channel. Then, you configure the Live Activity to subscribe to that channel for its updates.\n\nThe other strategy, is to use push notifications. This is great for all other use cases. The server can send push notifications to target updates to specific devices. For this strategy, you'll obtain a push token for the Live Activity and use that to send each update. To learn more, the documentation has a great guide on how to use ActivityKit push notifications.\n\nSo far, I've covered just the first steps with Live Activities. Next, I'll share how to go even further to optimize them.\n\nI'll discuss adding more customized presentations and bringing interactivity to the views.\n\nI'll start with further refining their presentations.\n\nIn iOS 27, the Dynamic Island compact and minimal views are visible in both portrait and landscape. In portrait, compact views are flexible in width but in landscape, they don't have room to grow in width. Your Live Activity needs to account for when the Dynamic Island is constrained in width like this. Here's the implementation of the CompactTrailingView for the coffee order. It's a SwiftUI view that either shows the estimated time for the drink order, if there's one available, or it shows the label for the order's phase, like the string \"Ready\".\n\nI'll start by adding the environment value isDynamicIslandLimitedInWidth, then I'll adjust the View body. When the Dynamic Island is limited in width, I'll display an alternate trailing view that shows an icon of the order's progress instead.\n\nNow, the new trailing view fits nicely into the limited width.\n\nAnother presentation to consider is StandBy, which can appear when iPhone is charging in landscape.\n\nIn this presentation, the Lock Screen view is used, scaled up to 200%. The gradient background that works great on Lock Screen makes the activity feel small and doesn't fill the screen, leaving a lot of blank space. To fix this, I'll adjust how my Live Activity's view displays its background. I'll add the showsWidgetContainerBackground @Environment value to the view. This value is true on the Lock Screen, so there, I'll apply my gradient .background. Next, I'll use the .activityBackgroundTint on the View to set a recognizable background color otherwise.\n\nNow, the Live Activity presents an edge-to-edge background tint color in StandBy.\n\nLive Activities also appear in the Smart Stack on Apple Watch and in CarPlay. To customize them for these places, just add support for the small device family.\n\nLive Activities are automatically forwarded from iPhone to CarPlay and use my ActivityView by default. This view looks great on Lock Screen but doesn't fit well in the space.\n\nTo adapt for this layout, first declare support for the small activity family. This indicates to the system that your views are adapted to this smaller format.\n\nThen, add the activityFamily @Environment value to the View and provide a customized view for this presentation when the value of activityFamily is .small.\n\nNow the Live Activity looks great everywhere!! People love being able to get access to your app's glanceable information while on-the-go.\n\nTo learn even more about adapting views for the small activity family, check out the session, \"Bring your Live Activity to Apple Watch\".\n\nLive Activities can be a great opportunity for people to take quick, immediate actions. You can provide this by adding interactivity. When an order is complete in the coffee app, I want to make it easy for people to rate their order. To do this, each button in the Live Activity is associated with an App Intent. When someone taps a button, the system executes the associated intent.\n\nIn the implementation, the RateDrinkIntent conforms to LiveActivityIntent, and it takes two parameters, the ID for the order and a boolean describing whether the rating was positive or not.\n\nThe perform method is where the app's logic is implemented. When either button is tapped, this function will run. The app can implement this method to do all of the work involved, like sharing this rating back with the server and updating the database.\n\nFinally, I'll update the ratings Button View, which is used in both my ActivityView and DynamicIslandExpandedRegion. I'll add two buttons for each intent, and can create a RateDrinkIntent for each action.\n\nLive Activities are a great way to keep people updated about things they care about that are happening in real time. Now you're empowered to get started with them in your apps.\n\nSome next steps are - build Live Activities with the ActivityKit and WidgetKit frameworks; use attributes and content state to model your static and dynamic data for efficient updates; create outstanding presentations on the Lock Screen, in the Dynamic Island, and more; use ActivityKit and push notifications for timely updates to its data; And curate each experience, like in landscape, in StandBy, on Apple Watch, and others. Thanks for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "4:16",
+ "title": "Define initial Live Activity",
+ "language": "swift",
+ "code": "// Define initial Live Activity.\n\nimport ActivityKit\nimport Foundation\n\npublic struct DrinkOrderAttributes: ActivityAttributes {\n let shopName: String\n let drink: Drink\n let orderID: UUID\n\n public struct ContentState: Codable, Hashable {\n var phase: DrinkOrder.Phase = .waiting\n var estimatedReadyDate: Date\n var rating: DrinkOrder.Rating?\n }\n}"
+ },
+ {
+ "timestamp": "5:35",
+ "title": "Create each Live Activity view",
+ "language": "swift",
+ "code": "// Create each Live Activity view\n\nimport ActivityKit\nimport SwiftUI\nimport WidgetKit\n\nstruct DrinkOrderLiveActivity: Widget {\n var body: some WidgetConfiguration {\n ActivityConfiguration(for: DrinkOrderAttributes.self) { context in\n ActivityView(context: context)\n } dynamicIsland: { context in\n DynamicIsland {\n DynamicIslandExpandedRegion(.leading) {\n ExpandedLeadingView(context: context)\n }\n DynamicIslandExpandedRegion(.center) {\n ExpandedCenterView(context: context)\n }\n DynamicIslandExpandedRegion(.trailing) {\n ExpandedTrailingView(context: context)\n }\n DynamicIslandExpandedRegion(.bottom) {\n ExpandedBottomView(context: context)\n }\n } compactLeading: {\n CompactLeadingView(context: context)\n } compactTrailing: {\n CompactTrailingView(context: context)\n } minimal: {\n MinimalView(context: context)\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "7:43",
+ "title": "Start and update a Live Activity",
+ "language": "swift",
+ "code": "// Start a Live Activity\n\nfunc launchLiveActivity(order: DrinkOrder) throws {\n guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }\n let attributes = DrinkOrderAttributes(shopName: \"Coffee Shop\", drink: order.drink, orderID: order.id)\n let estimatedReadyDate = Date.now + (15 * 60)\n let contentState = DrinkOrderAttributes.ContentState(phase: .waiting, estimatedReadyDate: estimatedReadyDate)\n let activityContent = ActivityContent(state: contentState, staleDate: nil)\n let activity = try Activity.request(attributes: attributes, content: activityContent)\n\n}\n\n// Update a Live Activity\n\nawait activity.update(\n ActivityContent(\n state: DrinkOrderAttributes.ContentState(\n phase: .preparing,\n estimatedReadyDate: estimatedReadyDate\n ),\n staleDate: nil\n )\n)"
+ },
+ {
+ "timestamp": "10:33",
+ "title": "Optimize for limited width in the Dynamic Island",
+ "language": "swift",
+ "code": "// Optimize for limited width in the Dynamic Island\n\nstruct CompactTrailingView: View {\n @Environment(\\.isDynamicIslandLimitedInWidth) var isDynamicIslandLimitedInWidth\n var context: ActivityViewContext\n var body: some View {\n if isDynamicIslandLimitedInWidth {\n StepProgressIconView(context: context)\n } else if context.state.phase.showsTimer {\n EstimatedReadyView(context: context, font: .system(.body).monospacedDigit())\n .multilineTextAlignment(.trailing)\n .frame(maxWidth: maximumTimerLabelWidth)\n } else {\n OrderPhaseLabelView(context: context, font: .caption2.bold(), color: .brown)\n .multilineTextAlignment(.trailing)\n }\n }\n}"
+ },
+ {
+ "timestamp": "11:34",
+ "title": "Extend background color in StandBy",
+ "language": "swift",
+ "code": "// Extend background color in StandBy\n\nstruct ActivityView: View {\n\n @Environment(\\.showsWidgetContainerBackground) var showsWidgetContainerBackground\n var context: ActivityViewContext\n\n var body: some View {\n DetailView(context: context)\n .background {\n if showsWidgetContainerBackground {\n LinearGradient.barista\n }\n }\n .activityBackgroundTint(.espresso)\n }\n}"
+ },
+ {
+ "timestamp": "12:30",
+ "title": "Add support for activityFamily small",
+ "language": "swift",
+ "code": "// Add support for activityFamily small\n\nimport ActivityKit\nimport SwiftUI\nimport WidgetKit\n\nstruct DrinkOrderLiveActivity: Widget {\n var body: some WidgetConfiguration {\n ActivityConfiguration(for: DrinkOrderAttributes.self) { context in\n ActivityView(context: context)\n } dynamicIsland: { context in\n DynamicIsland {\n DynamicIslandExpandedRegion(.leading) {\n ExpandedLeadingView(context: context)\n }\n DynamicIslandExpandedRegion(.center) {\n ExpandedCenterView(context: context)\n }\n DynamicIslandExpandedRegion(.trailing) {\n ExpandedTrailingView(context: context)\n }\n DynamicIslandExpandedRegion(.bottom) {\n ExpandedBottomView(context: context)\n }\n } compactLeading: {\n CompactLeadingView(context: context)\n } compactTrailing: {\n CompactTrailingView(context: context)\n } minimal: {\n MinimalView(context: context)\n }\n }\n .supplementalActivityFamilies([.small])\n }\n}"
+ },
+ {
+ "timestamp": "12:43",
+ "title": "Optimize for small family",
+ "language": "swift",
+ "code": "// Optimize for small family\n\nstruct ActivityView: View {\n @Environment(\\.showsWidgetContainerBackground) var showsWidgetContainerBackground\n @Environment(\\.activityFamily) var activityFamily\n\n var context: ActivityViewContext\n\n var body: some View {\n contentView\n .background {\n if showsWidgetContainerBackground {\n LinearGradient.barista\n }\n }\n .activityBackgroundTint(.espresso)\n }\n\n @ViewBuilder\n var contentView: some View {\n if activityFamily == .small {\n SmallView(context: context)\n } else {\n DetailView(context: context)\n }\n }\n}"
+ },
+ {
+ "timestamp": "13:36",
+ "title": "Add interactivity with App Intents",
+ "language": "swift",
+ "code": "// Add interactivity with App Intents\n\nstruct RateDrinkIntent: LiveActivityIntent {\n static var title: LocalizedStringResource = \"Rate Drink\"\n\n @Parameter(title: \"Order ID\")\n var orderID: String\n\n @Parameter(title: \"Positive\")\n var isPositive: Bool\n\n func perform() async throws -> some IntentResult {\n await updateLocalDatastore(rating: isPositive ? .great : .poor, dismissPolicy: .after(.now + 15))\n return .result()\n }\n}"
+ },
+ {
+ "timestamp": "14:06",
+ "title": "Associate an intent with a button",
+ "language": "swift",
+ "code": "// Associate an intent with a button\n\nstruct RatingButtons: View {\n var context: ActivityViewContext\n var body: some View {\n HStack(spacing: 12) {\n Button(intent: RateDrinkIntent(\n orderID: context.attributes.orderID.uuidString, isPositive: false)) {\n Label(\"Not Good\", systemImage: \"hand.thumbsdown.fill\")\n }\n .buttonStyle(RatingButtonStyle(color: .red))\n\n Button(intent: RateDrinkIntent(\n orderID: context.attributes.orderID.uuidString, isPositive: true)) {\n Label(\"Great\", systemImage: \"hand.thumbsup.fill\")\n }\n .buttonStyle(RatingButtonStyle(color: .green))\n }\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Human Interface Guidelines: Live Activities",
+ "url": "https://developer.apple.com/design/human-interface-guidelines/live-activities"
+ },
+ {
+ "title": "Starting and updating Live Activities with ActivityKit push notifications",
+ "url": "https://developer.apple.com/documentation/ActivityKit/starting-and-updating-live-activities-with-activitykit-push-notifications"
+ },
+ {
+ "title": "ActivityKit",
+ "url": "https://developer.apple.com/documentation/ActivityKit"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/223/4/9098c495-ea8b-44f9-b852-f6eb64840161/downloads/wwdc2026-223_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/223/4/9098c495-ea8b-44f9-b852-f6eb64840161/downloads/wwdc2026-223_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "10068",
+ "year": "2024",
+ "title": "Bring your Live Activity to Apple Watch",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10068"
+ },
+ {
+ "id": "10150",
+ "year": "2024",
+ "title": "SwiftUI essentials",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10150"
+ },
+ {
+ "id": "10194",
+ "year": "2023",
+ "title": "Design dynamic Live Activities",
+ "url": "https://developer.apple.com/videos/play/wwdc2023/10194"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:13.656Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-224.json b/data/wwdc/videos/2026-224.json
new file mode 100644
index 0000000..0a92703
--- /dev/null
+++ b/data/wwdc/videos/2026-224.json
@@ -0,0 +1,102 @@
+{
+ "id": "224",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/224/",
+ "title": "Expand the capabilities of your Virtualization app",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "System Services"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hello! I'm Ronnie Misra from the Virtualization team. In this session, I'll explore how you can add advanced capabilities to your Virtualization app. You can use the Virtualization framework to create apps that power full, desktop experiences or enable sophisticated developer workflows, like testing collaborative Mac apps, and networking between devices. You can also build command line tools and automation to enable consistent testing in controlled environments.\n\nToday, I'll take you through some new and existing APIs to make your Virtualization apps even more powerful. I'll cover: automating the setup of Virtual Macs, and attaching USB devices to virtual machines with the Accessory Access framework. I'll explore configuring advanced network topologies, and how to create high-performance, efficient disk images with the DiskImageKit framework. Finally, I'll cover creating custom, high-performance virtual devices with Virtio. First, I'll show you how Virtualization enables you to automate macOS guest provisioning. After you install macOS into a virtual Mac, you can set it up exactly the same way as you would set up a physical Mac. You use the same Setup Assistant you are already familiar with. This gives you an easy way to create a user account and configure common settings. For automation use cases, however, it would be convenient to be able to programmatically set up a virtual Mac.\n\nThe Virtualization framework now lets your app specify provisioning options when a virtual Mac is started. You provide a full name, username, and password, and optionally enable auto-login or remote login via SSH.\n\nWhen the guest boots for the first time, these parameters are automatically passed to Setup Assistant. A user is created with the specified credentials, and auto-login and remote login are enabled if requested. To use the macOS guest provisioning API, you first create a VZMacGuestProvisioningOptions object with your desired settings. Here, I have created provisioningOptions that will create a user account, enable auto login, and also enable SSH. You then construct a VZMacOSVirtualMachineStartOptions object and set its guest provisioning to the provisioningOptions you just constructed. Finally, you start your virtual Mac using these startOptions. When the guest boots, these provisioning options will be used to automate provisioning of the virtual Mac. Now, I'll show you this in action. I've modified the macOS virtual machine sample app to make use of the macOS guest provisioning API. I have already installed macOS into a new virtual Mac, but I have not booted it yet. I'll double-click the app to boot the virtual Mac for the first time.\n\nThe app prompts me for Provisioning Options. Because I have previously run this app, it remembers my preferred provisioning options to create a user for Jane Appleseed and enable automatic login and remote login. I'll now click OK to accept those settings.\n\nMy app has now started the virtual Mac with those provisioning options. The Virtualization framework will pass those options into the guest, so I don't have to navigate through setup assistant manually. Setup assistant automatically creates a new user. Setup assistant has now created the new account. Because my provisioning options indicated that automatic login should be enabled, the guest has logged into the account. I'll now open a Finder window in my virtual Mac.\n\nThe sidebar shows the username jappleseed, which matches the username I provided. Now, I'll open up System Settings and browse to the Remote Login sharing setting.\n\nSystem Settings confirms that Remote Login has been enabled. My virtual Mac is now ready to go! All I had to do was boot it. Note that these settings are only honored if the guest has not already been set up. If a user has already been created in the guest, provisioning options passed on subsequent boots will be ignored.\n\nWhen using these APIs, be thoughtful about how you handle passwords, and consider the security implications for your app. For example, instead of hardcoding a password in your code, you might want to read it from the Keychain, or a configuration file, or an environment variable. Next, I'll talk about attaching USB accessories using Accessory Access.\n\nSome virtual machine use cases require the ability to grant the guest access to a USB accessory connected to the host. For example, someone may want to make use of a USB drive from inside of a virtual machine. At the same time, people should remain in control of their devices. Accessory Access is a new framework designed to support making USB devices available to macOS and Linux virtual machines. A key principle of Accessory Access is that people should have explicit control of which devices are attached to which apps. People have visibility into what apps are using their devices, and can attach and detach devices at any time. Accessory Access supports device hot plugging. When someone grants an app access to a device, it can be attached to the virtual machine at runtime without changing the VM's static configuration.\n\nBefore I dive into the details, here is Accessory Access in action. I'm running the macOS virtual machine sample app I showed you before. I'll now connect a USB drive to my Mac.\n\nThe icon for my drive has appeared on the desktop. Also, because the virtual machine app is running and has indicated interest in storage devices, an accessory icon is now present in the menu bar. I'll now select my disk in the accessory menu and attach it to my app.\n\nSince I attached this drive to my virtual Mac, my host has unmounted the drive, and the guest has mounted it. Now I'll safely eject the drive from inside the virtual Mac.\n\nThen I'll use the accessory menu to release the drive back to my host.\n\nNow that I have released the drive to my host, the host has remounted the drive. This demonstrates how Accessory Access makes it easy to use your USB accessories from a virtual machine.\n\nTo use Accessory Access, your app registers a listener with matching criteria describing the types of devices it is interested in. You can filter by device class and subclass, vendor ID and product ID, or other criteria.\n\nWhen a matching device is connected to the Mac, the Accessory Access menu extra appears. From here, someone can decide to attach the device to your app.\n\nIf the device is attached to your app, your app's listener object will be notified of the newly attached device. To use Accessory Access, you start by creating an array of AAUSBAccessoryMatchingCriteria objects describing the types of devices you are interested in. You can use an empty array to express interest in all USB devices. Then use AAUSBAccessoryManager to register a listener. This listener should implement the AAUSBAccessoryListener protocol. registerListener will return any accessories that were previously attached to your application. When someone attaches a device to your app, your listener's usbAccessoryDidConnect function will be called. In this function, you can attach the device to your virtual machine. VZVirtualMachine requires modifications to happen on its own queue. On that queue, you can use the VZUSBPassthroughDeviceConfiguration class to create a VZUSBPassthroughDevice, and then attach this device to one of the virtualMachine's USB controllers.\n\nIn order for your app to use Accessory Access, add the Claim USB Accessory capability to your Xcode target's capabilities. Remember that people can choose to attach or detach devices from your app at any time. Your app should handle these events gracefully. Consult the Accessory Access documentation for details on supported device types.\n\nIn macOS 26 and later, your app can use the vmnet framework to configure virtual network interfaces. The Virtualization framework makes it easy to configure isolated virtual machines with basic NAT or bridge networking. For more advanced use cases, however, you may want to have more control of how VMs interact with each other or with the external network. For example, you may want to test connections to your server virtual machine from clients on either the same or a different network. The vmnet framework allows you to create custom network topologies to support these advanced use cases.\n\nUsing the vmnet framework, you can create custom network topologies for your macOS and Linux VMs. vmnet allows you to control how your VMs can communicate with each other. vmnet also allows you to configure various parameters of those custom networks. For example, you can configure the DHCP settings for the network, or add rules to forward TCP or UDP host ports to specific virtual machines.\n\nTo use vmnet with the Virtualization framework, you first create a vmnet network configuration object. You use that configuration to construct a vmnet network object. You can then use that network object to construct a network device attachment. This network device attachment is then attached to a network device configuration, which is in turn added to a virtual machine configuration. Finally, you use that virtual machine configuration to construct a virtual machine. If you want a second virtual machine to use the same vmnet network, you follow the same steps to configure that second virtual machine, making sure to use the same vmnet network object.\n\nNow I'll show you these steps in code. You first create a vmnet configuration object using vmnet_network_configuration_create. vmnet provides several functions to customize that network: for example, you can configure the network's DHCP settings, enable port forwarding, etc.\n\nOnce you have a network configuration object, you can use vmnet_network_create to construct a vmnet network object. You then construct a VZVmnetNetworkDeviceAttachment to allow Virtualization to use the vmnet network you just created.\n\nNext, set the attachment on a VZVirtioNetworkDeviceConfiguration object. This networkDeviceConfiguration is added to the array of networkDevices on your VZVirtualMachineConfiguration object. And finally, this configuration will be used to construct your VZVirtualMachine. A vmnet network object is a reference counted Objective-C object. The network goes away when the last reference is released. This also implies that a vmnet network is not persisted when your app quits. If you want to create a consistent network configuration, your app must persist your vmnet settings itself.\n\nvmnet provides the vmnet_network_copy_serialization and vmnet_network_create_with_serialization APIs to allow you to transfer a vmnet network across an XPC connection from one process to another.\n\nThis is useful if you would like to run multiple VMs in separate processes but connect them to the same network. Next, I'll show you how to use DiskImageKit to efficiently work with disk images. The Virtualization framework supports using standard raw disk image files to back virtual machine disks. This simple format maps disk blocks to file blocks one-to-one. This simplicity means that the format is widely supported by existing software. However, this simplicity comes with a cost. Raw disk images cannot inherently represent sparsity for example, a 100 gigabyte disk is represented by a 100 gigabyte file. This also makes snapshots expensive. Snapshotting a VM's disk requires making a copy of the entire disk.\n\nDiskImageKit is a new framework in macOS 27 that is designed to make disk image management more efficient. It supports the Apple Sparse Image Format or ASIF that was introduced in macOS 26. DiskImageKit allows you to construct a stack of images, allowing writes to go into an overlay layer while leaving the base layer unmodified. DiskImageKit also supports raw disk images. When constructing a stacked image, DiskImageKit supports a few different types of layers. The bottom layer of a stacked image is called the base layer. This layer can be of any format that is supported by DiskImageKit. Upper layers are always ASIF images. These layers can be either cache or overlay layers. A cache layer can be used to improve performance when underlying layers exist on slow storage like a remote network filesystem. When processing a read request, if the cache layer cannot satisfy the read, DiskImageKit will satisfy the read from lower layers, but store a copy of the data in the cache layer. Subsequent reads of the same data will then be read from the cache.\n\nOverlay layers can be used to implement copy-on-write semantics for snapshots. When writing to a layered image, if DiskImageKit encounters a writable overlay while traversing the stack, it will store the writes in that layer.\n\nDiskImageKit allows read-only layers to be shared by multiple concurrent stacks. This allows efficient reuse of shared content between multiple virtual machines while keeping their independent writes separate.\n\nASIF images are sparse. This means that an ASIF image may logically represent more blocks than are actually stored in the image. When reading from an ASIF file, blocks that are not stored in the image are treated as if they were zero-filled.\n\nI'll walk through how DiskImageKit would process read and write requests for an example stack. In this example, the base layer has content for blocks 0, 1 and 4. The cache layer does not have content for any blocks. The overlay layer has updated content for block 4, and also has content for block 5. Note that a layer can have a different logical size than the layers above or below it.\n\nTo satisfy a read of block 0, DiskImageKit will traverse the layers of the stacked image until it finds a layer that contains the content for this block. This read will be satisfied by the base layer. Because there was a cache layer, DiskImageKit will cache the contents of this block and then return the contents to the caller. Subsequent reads of this same block will be satisfied by the cache layer.\n\nWhen writing block 2, DiskImageKit will discover that the top layer is an overlay and store the content there.\n\nTo use DiskImageKit images with Virtualization, you first start by creating a DiskImage object.\n\nIf you'd like to use a layered image, you create multiple DiskImage objects and then append them in order.\n\nYou can then construct a VZDiskImageStorageDeviceAttachment from your stackedImage. This can then be attached to a storageDeviceConfiguration, for example a VZVirtioBlockDeviceConfiguration.\n\nAdd this storageDeviceConfiguration to your virtual machine configuration's storageDevices. Finally, create your VZVirtualMachine using this configuration.\n\nWhen using stacked images, it is worth noting that shallow stacks perform better. There is a performance cost to increasing the depth of a disk image stack. Keep in mind that a virtual machine is comprised of more than just disk images. For example, a virtual Mac has an auxiliary storage file, and a VM using the EFI boot loader has an EFI variable store file. If you want to clone a VM and use a shared base layer, remember that you must duplicate those other files. Finally, I'll show you how the custom Virtio APIs allow you to build custom communication channels between your app and your Linux virtual machines.\n\nAlthough the Virtualization framework already supports a wide range of standard device classes, some use cases may require something more specialized. Perhaps you want to implement a custom protocol for performance-critical communication between host and guest. Maybe you want to implement a coprocessor, such as a Virtio crypto device. You might want to provide efficient guest access to machine learning accelerators. That's where the custom Virtio device API comes in.\n\nVirtio is an industry standard for paravirtualized devices. It's the protocol used to implement many of the built in Virtualization devices. In macOS 27, the Virtualization framework allows you to implement your own Virtio devices, allowing custom communication between your host app and your Linux virtual machines. This is especially useful for performance-critical scenarios where you need low-latency, high-throughput communication.\n\nThe Virtio protocol makes use of memory buffers shared between the guest and the host. These buffers are organized into Virtio queues. Virtio is designed to minimize the number of context switches between the guest and the host. The device driver in the guest notifies the device running on the host when data has been enqueued. Similarly, the host can use an interrupt to inform the guest driver about enqueued data.\n\nIn macOS 27, the VZCustomVirtioDevice class can be used to implement your custom device. Your app sets a delegate on the device. This delegate will be notified when the guest enqueues data on its queue. You can also initiate activity in the guest by triggering an interrupt on the device.\n\nTo use the custom Virtio device API, you start by creating a VZCustomVirtioDeviceConfiguration object. You configure this object with your device's Virtio device identity, its PCI class and subclass, and the number of Virtio queues your device uses.\n\nYou also set a provider on the configuration. VZCustomVirtioDeviceDelegateProvider is used to configure a delegate that implements the VZCustomVirtioDevice- ConfigurationDelegate protocol.\n\nYou then add this deviceConfiguration to your virtualMachineConfiguration's customVirtioDevices array, and create a VZVirtualMachine using that configuration.\n\nWhen the virtual machine is started, a VZCustomVirtioDevice object is created, and your configuration delegate's didCreateDevice function is called. In this function, you should set the device's delegate. This delegate should implement the VZCustomVirtioDeviceDelegate protocol. You can also hang onto the device itself so that your device can trigger guest interrupts.\n\nThere are several functions in the VZCustomVirtioDeviceDelegate protocol that can be used to monitor the device's lifecycle and interact with the device. The didReceiveNotificationFor function is where you implement logic to dequeue elements from your device's queue, process those elements, and then return them to the queue.\n\nRemember that custom devices require custom drivers to allow the guest to use your device. Virtio queues are designed to provide efficient communication between the guest and the host. Make sure to follow best practices when designing your guest driver to make optimal use of Virtio queues. Before I wrap up, I want to briefly mention some other advancements to Virtualization that can really make your app shine.\n\niCloud support is particularly valuable for desktop experiences, allowing people to access their iCloud data and services in the VM. EFI Secure Boot hardens Linux VMs with modern security features. macOS guests can take advantage of Metal features like argument buffers and indirect command buffers.\n\nYou're now ready to add even more capabilities to your Virtualization app. Automate the setup of macOS user accounts by configuring them with provisioning options. Use the Accessory Access framework to attach USB devices to a VM.\n\nCustomize networking for your VMs by building your own network topology and configuring port forwarding. Use the DiskImageKit framework to create efficient, sparse disk images. And for custom, high-performance device needs in Linux guests, consider creating custom devices with Virtio.\n\nThanks for watching! Have a great WWDC.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "1:57",
+ "title": "Provision a macOS guest",
+ "language": "swift",
+ "code": "import Virtualization\n\nlet provisioningOptions = VZMacGuestProvisioningOptions()\nprovisioningOptions.fullName = fullName\nprovisioningOptions.username = username\nprovisioningOptions.password = password\nprovisioningOptions.logsInAutomatically = true\nprovisioningOptions.enablesRemoteLogin = true\n\nlet startOptions = VZMacOSVirtualMachineStartOptions()\ntry startOptions.setGuestProvisioning(provisioningOptions)\n\ntry await virtualMachine.start(options: startOptions)"
+ },
+ {
+ "timestamp": "7:12",
+ "title": "Register an Accessory Access listener",
+ "language": "swift",
+ "code": "import AccessoryAccess\n\nlet criteria: [AAUSBAccessoryMatchingCriteria] = []\nlet accessories = try await AAUSBAccessoryManager.shared.registerListener(self, matchingCriteria: criteria)\n\nfor accessory in accessories {\n // Handle previously attached accessories.\n}"
+ },
+ {
+ "timestamp": "7:39",
+ "title": "Respond to USB accessory connection",
+ "language": "swift",
+ "code": "import AccessoryAccess\nimport Virtualization\n\nclass AccessoryListener: NSObject, AAUSBAccessoryListener {\n func usbAccessoryDidConnect(_ usbAccessory: AAUSBAccessory) {\n virtualMachine.queue.async {\n do {\n let configuration = VZUSBPassthroughDeviceConfiguration(device: usbAccessory)\n let device = try VZUSBPassthroughDevice(configuration: configuration)\n self.virtualMachine.usbControllers.first?.attach(device: device) { error in\n // Handle error if necessary...\n }\n } catch {\n // Handle error...\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "10:04",
+ "title": "Create a custom vmnet network",
+ "language": "swift",
+ "code": "import Virtualization\nimport vmnet\n\nvar status: vmnet_return_t = .VMNET_FAILURE\nguard let networkConfiguration =\n vmnet_network_configuration_create(.VMNET_SHARED_MODE, &status) else { ... }\n\nguard let network =\n vmnet_network_create(networkConfiguration, &status) else { ... }\n\nlet attachment = VZVmnetNetworkDeviceAttachment(network: network)\n\nlet networkDeviceConfiguration = VZVirtioNetworkDeviceConfiguration()\nnetworkDeviceConfiguration.attachment = attachment\n\nvirtualMachineConfiguration.networkDevices = [networkDeviceConfiguration]\n\nlet virtualMachine = VZVirtualMachine(configuration: virtualMachineConfiguration)"
+ },
+ {
+ "timestamp": "14:54",
+ "title": "Use DiskImageKit with Virtualization",
+ "language": "swift",
+ "code": "import DiskImageKit\nimport Virtualization\n\nlet baseImage = try DiskImage(opening: .open(url: baseLayerURL, mode: .readOnly))\nlet cacheImage = try baseImage.appending(.asifLayer(url: cacheLayerURL, type: .cache))\nlet overlayImage = try DiskImage(opening: .open(url: overlayLayerURL))\nlet stackedImage = try cacheImage.appending(overlayImage)\n\nlet storageDeviceAttachment = try VZDiskImageStorageDeviceAttachment(diskImage: stackedImage)\n\nlet storageDeviceConfiguration =\n VZVirtioBlockDeviceConfiguration(attachment: storageDeviceAttachment)\n\nvirtualMachineConfiguration.storageDevices = [storageDeviceConfiguration]\n\nlet virtualMachine = VZVirtualMachine(configuration: virtualMachineConfiguration)"
+ },
+ {
+ "timestamp": "17:41",
+ "title": "Configure a custom Virtio device",
+ "language": "swift",
+ "code": "import Virtualization\n\nlet deviceConfiguration = VZCustomVirtioDeviceConfiguration()\n\n// Virtio entropy device.\ndeviceConfiguration.deviceID = 4\n// PCI class for crypto devices.\ndeviceConfiguration.pciClassID = 0x10\n// PCI subclass for network and computing encryption controllers.\ndeviceConfiguration.pciSubclassID = 0x00\n// An entropy device uses a single Virtio queue.\ndeviceConfiguration.virtioQueueCount = 1\n\ndeviceConfiguration.provider =\n VZCustomVirtioDeviceDelegateProvider(deviceQueue: deviceQueue, delegate: provider)\n\nvirtualMachineConfiguration.customVirtioDevices = [deviceConfiguration]\n\nlet virtualMachine = VZVirtualMachine(configuration: virtualMachineConfiguration)"
+ },
+ {
+ "timestamp": "18:20",
+ "title": "Attach a delegate to a VZCustomVirtioDevice",
+ "language": "swift",
+ "code": "import Virtualization\n\nclass DeviceConfigurationDelegate: NSObject, VZCustomVirtioDeviceConfigurationDelegate {\n func customVirtioConfiguration(_ deviceConfiguration: VZCustomVirtioDeviceConfiguration,\n didCreateDevice device: VZCustomVirtioDevice) {\n device.delegate = deviceDelegate\n self.device = device\n }\n}"
+ },
+ {
+ "timestamp": "18:42",
+ "title": "Process Virtio queue elements",
+ "language": "swift",
+ "code": "import Virtualization\n\nclass DeviceDelegate: NSObject, VZCustomVirtioDeviceDelegate {\n func customVirtioDevice(_ device: VZCustomVirtioDevice,\n didReceiveNotificationFor queue: VZVirtioQueue) {\n while let element = queue.nextElement() {\n // Process element...\n element.returnToQueue()\n }\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "DiskImageKit",
+ "url": "https://developer.apple.com/documentation/DiskImageKit"
+ },
+ {
+ "title": "Accessory Access",
+ "url": "https://developer.apple.com/documentation/AccessoryAccess"
+ },
+ {
+ "title": "vmnet",
+ "url": "https://developer.apple.com/documentation/vmnet"
+ },
+ {
+ "title": "Virtual I/O Device (VIRTIO) Version 1.4",
+ "url": "https://docs.oasis-open.org/virtio/virtio/v1.4/virtio-v1.4.html"
+ },
+ {
+ "title": "Virtualization",
+ "url": "https://developer.apple.com/documentation/Virtualization"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/224/5/33a91529-8caf-409e-9c54-1b8952744651/downloads/wwdc2026-224_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/224/5/33a91529-8caf-409e-9c54-1b8952744651/downloads/wwdc2026-224_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "389",
+ "year": "2026",
+ "title": "Discover container machines",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/389"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:14.300Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-226.json b/data/wwdc/videos/2026-226.json
new file mode 100644
index 0000000..288bceb
--- /dev/null
+++ b/data/wwdc/videos/2026-226.json
@@ -0,0 +1,125 @@
+{
+ "id": "226",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/226/",
+ "title": "Create live communication experiences",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "System Services"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Yaseen, a Software Engineer at Apple.\n\nIn this session, I'll show you how to deliver a rich, native conversation UI that puts your app right where people need it, from a full-screen presentation on the Lock Screen to seamless multitasking with the Dynamic Island. It starts the moment a conversation comes in. When your app adopts this API, its conversations get a full-screen presentation on the Lock Screen, complete with the contact's name, photo, and a standard set of controls. This is exactly what appears when the phone rings.\n\nApps that adopt this API get the same presentation.\n\nConversations can also show up in Phone app Recents and in contact details. Recents shows who the person spoke with and when, and they can tap to start a new conversation.\n\nLiveCommunicationKit is the modern way to create communication apps that integrate with these system experiences. If you have an app that uses traditional approaches, like the CXProvider API, now is a great time to move over to LiveCommunicationKit. It provides a more flexible and feature-rich API to integrate all of the different types of real-time conversations that your app supports.\n\nI'll start with the core concepts: what a conversation is; how it moves through its lifecycle; and how your app communicates with the system. Then I'll walk through receiving conversations, from waking your app with a push notification to presenting the conversation on the Lock Screen. Then, starting outgoing conversations from inside your app and making them available through Siri and Recents.\n\nAnd finally, group conversations, managing participants and merging conversations together.\n\nTo explore how all of this works, I'll follow a group of college friends, David, Ryan, Andre, and Adam, as they use my audio conversation app to plan their annual reunion trip. But first, a quick introduction to how LiveCommunicationKit works. Every experience I just walked through is driven by a single object - a conversation. A conversation represents a single real-time interaction between people. It lives only as long as someone is in it. When everyone leaves, it's gone. A conversation has two parts: handles, which represent the people in the conversation, and capabilities, which describe what the conversation can do. I'll start with handles.\n\nA handle identifies a person. It has three properties: kind, value, and display name. I'll go through each one.\n\nThe kind tells the system what type of identifier the handle is: a phone number, an email address or a generic string. If your app already identifies people by phone number or email, setting the right kind allows the system to match the person's handle to a saved contact and show their name and photo in the system conversation UI.\n\nThe value is the identifier itself - the phone number, email address, or generic string. This is what the system uses to look up the contact and what your app gets back when someone redials from Recents.\n\nAnd the display name is what the system shows when it can't match the handle to a contact. Set this to the name your app already knows for the person so the conversation UI always has something to display.\n\nCapabilities tell the system what a conversation can do. The system uses them to decide which controls to show and which gestures to enable so that the conversation UI only offers what your app actually supports.\n\nThe system shows a standard set of in-conversation controls: Mute, Speaker, Keypad and More. Some of them only appear when my app opts in. Here, the pausing capability is declared so a long press on the Mute button puts the conversation on hold. Without that capability, the long press does nothing. The video capability tells the system this is a video conversation. The video button itself is enabled by my app's provider configuration, which I'll cover later.\n\nCapabilities can change over the lifetime of a conversation; when this one upgrades from audio to video, my app updates the capabilities and the system reflects the change immediately.\n\nNow that you know what makes a conversation, I'll walk through how it moves through its lifecycle with the system.\n\nWhen your app first reports a conversation to the system, the conversation starts in the idle state, and the device starts ringing. While it's ringing, your app can begin its local setup. When the person answers, your app sets the state to joining. The system updates the UI to show the conversation as connecting while your app finishes setting up and gets ready to join. No audio or video capture happens yet. When your app finishes preparing, it sets the state to joined and starts capturing and sending audio and video, and the conversation is now live. If the person switches to AirPods or connects to car Bluetooth, your app gets a route change notification and updates its capture pipeline to match.\n\nIf your app declares the pausing capability, the system enables its hold control. When someone holds the conversation, the system asks your app to pause. Your app pauses its media streams and reports the new state. When the person resumes the conversation, the system asks your app to resume the conversation's media streams and report the change. When the conversation ends, your app sets the state to leaving. This is where your app tears down the conversation and cleans up its connections.\n\nAfter teardown, your app sets the state to left, and the conversation is over.\n\nHow does your app actually drive all of this? I'll start with the architecture.\n\nYour app drives the conversation lifecycle through the ConversationManager and its delegate. Your app reports conversations and events through the manager. Every time you tell the system about a new conversation or about a change to an existing one, you go through the manager. That's how your conversations show up on the Lock Screen, in the Dynamic Island, and everywhere else.\n\nThe delegate is where your app responds to the system. Whenever something needs to happen on a conversation, the action arrives at a delegate method, and your app does the work to fulfill it.\n\nThey communicate through actions. When someone interacts with the system UI - for example, accepting a conversation from the Lock Screen or ending one from the Dynamic Island - the system creates an action and sends it to your delegate. And when someone taps a button in your app's own UI, your app creates the action instead. Every interaction, whether it starts from the system UI or from within the app, flows through the same delegate callback, so there's exactly one place to put the logic for each action. That single code path means there's no duplicated state management and no risk of the app and the system getting out of sync. Before my app can report any conversations, it needs a ConversationManager. Here's how my app creates one.\n\nThe ConversationManager's configuration tells the system everything it needs to present and manage conversations for your app. You can update this configuration at any time during the app's lifetime.\n\nHere, my app provides a ringtone from its bundle and a PNG of its icon. The system presents both alongside my app's conversations across the system UI.\n\nNext are the conversation group limits. When conversations get merged together, they form a group. These values cap how many groups can exist at once, and how many conversations each group can hold. Then, there's whether my app's conversations show up in the Phone app's Recents list (for something like a one-time room that doesn't support redialing, pass 'false' to keep it out); and whether the app supports video, which enables the video button in the system UI; and, which handle types my app supports.\n\nWith that, my app creates its ConversationManager. Because the manager is needed for the entire lifetime of the app, my app creates it right at launch.\n\nFinally, my app sets the manager's delegate.\n\nTo continue conversations when the app is backgrounded or the device is locked, my app registers for the Audio and Voice over IP background modes in the app target's capabilities in Xcode.\n\nWith the ConversationManager configured, I'll walk through an incoming conversation from start to finish.\n\nDavid wants to start planning this year's reunion trip so he starts a conversation with Adam to talk about potential destinations.\n\nAdam's device is locked, but when David's conversation comes in, it appears right on the lock screen. Here's how my app makes this happen.\n\nWhen David starts the conversation with Adam, the app on his device builds a payload with two fields: a handle representing David's phone number and a unique identifier for the conversation.\n\nDavid's app then sends that payload to my app's server, and the server forwards it to Adam's device.\n\nWhen Adam's device receives the push, my app wakes up and decodes the payload.\n\nIt then uses the decoded handle to build a Conversation.Update. This update also includes the conversation's capabilities, in this case, video, pausing, and merging.\n\nMy app then uses the update to report the conversation to the ConversationManager, and the system updates its UI to match.\n\nPushKit is what wakes your app when a conversation arrives, and the app isn't already running.\n\nWhen your app's server sends a Voice over IP push, PushKit launches your app and delivers the payload to the delegate method immediately.\n\nYour app must report the conversation before the method returns or the system will terminate the app.\n\nFor more on Voice over IP push handling, check out the PushKit documentation.\n\nHere's that PushKit delegate method in my app.\n\nThis is the entry point every incoming conversation goes through. My app first extracts the handle and conversation UUID from the payload.\n\nThen it builds a Conversation.Update with the decoded handle and the conversation's capabilities and reports it.\n\nAdam sees the incoming conversation and slides to answer.\n\nThe system updates the UI to show the conversation is connecting then sends my app a JoinConversationAction through the delegate.\n\nEvery time someone answers, pauses or merges a conversation, the system delivers it to my app as an action. My app handles them in one place, the perform action delegate callback. The ConversationManager calls this every time an action comes in. Inside, my app uses a switch statement to route each action type to its appropriate handler. I'll trace the join action.\n\nTo handle the join action, my app first verifies that the ConversationManager is tracking a conversation matching the action's unique identifier. If no matching conversation is found, my app fails the action.\n\nThen it reports the conversation has started connecting, which will set the state to joining.\n\nOnce my app reports the connecting event, the system updates the conversation UI on Adam's device.\n\nNow my app does its own setup, connecting to its server and configuring the media stream. That work is async, so my app wraps it in a Task to keep the delegate responsive.\n\nAfter setup finishes, my app reports the connection and fulfills the action. If setup fails for any reason, my app marks the action accordingly so the system can clean up the conversation on its side. The conversation is now in the joined state, and the UI updates accordingly .\n\nAfter weighing a few options, they settle on Iceland for this year's destination, and Adam taps the End button.\n\nAs soon as he does, the conversation transitions into the leaving state, and the UI updates to match.\n\nThe system then sends my app an EndConversationAction through the delegate, and my app tears down the media stream and fulfills the action. And on Adam's device, the conversation disappears from the system UI .\n\nNext, I'll talk about how to place outgoing conversations. Now that David and Adam have settled on Iceland, Adam starts a conversation with Ryan to figure out where the friends will stay. This time, Adam starts the conversation from inside my app.\n\nWhen someone starts a conversation from inside your app, you should report it to the system so people can keep the conversation going while they're using other apps. To do this, your app creates a start action to ring the recipient's device. Then it calls perform on the ConversationManager and handles the action in its delegate the same way as the join action from earlier.\n\nOnce the action is handled, the recipient either answers or your app reports that the conversation went unanswered or failed.\n\nHere's how my app represents Adam's conversation with Ryan.\n\nIt builds a StartConversationAction with a fresh unique identifier and Ryan's handle. Then it sends the action to the ConversationManager.\n\nThe manager first updates the system UI, then it forwards the action to the delegate.\n\nFrom here, my app handles it the same way as the join action from earlier.\n\nOnce the conversation connects, Adam and Ryan look through a few options before finally settling on a cabin near Reykjavík. With lodging decided, they catch up for a few more minutes, then they say their goodbyes and hang up. After a conversation ends, people can redial them from Spotlight or Recents.\n\nYour app handles this by having support for the start call intent.\n\nThis intent will be delivered to your app's scene to continue as an NSUserActivity.\n\nWhen your app's conversations are saved to recents, Apple Intelligence already knows about them but, to surface your app's own representation of the conversation, donate your own intent at the end of each conversation as well.\n\nTo learn more about integrating intents in your app, check out the session \"Get to know App Intents\".\n\nEverything so far has been one-to-one. Group conversations bring in multiple participants. Having settled on a destination and lodging, Adam starts a group conversation with David and Ryan to plan their itinerary.\n\nGroup conversations track two types of members. Members include everyone who's been invited to the conversation whereas activeRemoteMembers includes only those with media actively flowing.\n\nThe system needs both; members tells it how many participants the conversation has, and activeRemoteMembers tells it which ones are actively sending media.\n\nWhen reporting a group conversation, my app creates a handle for each participant then creates a startAction with all invited members and reports it.\n\nAfter the conversation starts, David and Ryan both join. To report this change, my app builds a conversation update. It names Adam as the localMember, declares the new activeRemoteMembership, and lists the capabilities the conversation supports, including merging and unmerging, which I'll come back to in a moment.\n\nMy app then reports the conversation update through the manager, and the system updates the conversation to reflect the new membership.\n\nWhile Adam, David, and Ryan are working through the itinerary, Ryan realizes Andre still needs to confirm that the trip dates work for him...\n\nso, Ryan starts a separate conversation with Andre to loop him in.\n\nNow two conversations are running in parallel, the original group with Adam, David, and Ryan, and Ryan's side conversation with Andre. Ryan is in both conversations but only active on the one with Andre. Once Andre and Ryan have agreed on the trip dates, they want to bring everyone back together to review the full plan. Rather than the group having to hang up and start a new conversation, my app can merge the two together.\n\nMy app declares the merging capability, and the system enables its merge control UI.\n\nWhen Ryan taps it, my app merges the two conversations, and the whole group, now with Andre, can finish planning their itinerary. I'll walk through the conversation merging code next. Unmerging follows the same delegation pattern, so once you've seen the merge handler, the unmerge one will seem familiar.\n\nWhen two conversations merge, the ConversationManager delivers a MergeConversationAction to my app's delegate.\n\nThe merge action carries two unique identifiers, one for each conversation being merged. The handler uses these to look up my app's local representation of both conversations. If either one is missing (maybe it already ended), the handler fails the action immediately.\n\nOnce my app has both conversations, it combines the media streams on its server with combineStreams, then it reports the updated membership and fulfills the action. If anything throws, the catch block fails the action instead.\n\nOnce my app has merged the two conversations, the system updates the conversation UI to reflect the new merged conversation. With everyone in the same conversation, the group finalizes their itinerary: a soak in the Blue Lagoon, a day on the Golden Circle, and a few glacier hikes. With the itinerary set, Andre and Ryan want to split off and book their flights together so they can sit next to each other on the plane.\n\nSince the conversation supports the unmerging capability, they can go back to their own conversation while Adam and David wrap up the last few details. That's LiveCommunicationKit. From a single incoming conversation on the Lock Screen all the way to merging group conversations, the framework gives your app system-level conversation UI everywhere people expect it: on the Lock Screen, in Recents, and Siri.\n\nHere's what to do next. Adopt ConversationManager and report your first incoming conversation on the Lock Screen. Donate intents so Siri knows how to start conversations. Replace any transient tokens with stable handles to support redialing from Recents, and make sure to keep conversation membership updated. Thanks for watching.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "6:41",
+ "title": "Set up a conversation manager",
+ "language": "swift",
+ "code": "// Set up a conversation manager\n\nimport LiveCommunicationKit\n\nlet configuration = ConversationManager.Configuration(\n ringtoneName: \"SampleRingtone.caf\",\n iconTemplateImageData: UIImage(named: \"SampleIcon\")?.pngData(),\n maximumConversationGroups: 1,\n maximumConversationsPerConversationGroup: 2,\n includesConversationInRecents: true,\n supportsVideo: true,\n supportedHandleTypes: [.phoneNumber, .emailAddress]\n)\n\nlet manager = ConversationManager(configuration: configuration)\nmanager.delegate = self"
+ },
+ {
+ "timestamp": "9:22",
+ "title": "Report the incoming conversation to the system",
+ "language": "swift",
+ "code": "// Report the incoming conversation to the system\n\nimport LiveCommunicationKit\nimport PushKit\n\nfinal class SamplePushHandler: NSObject, PKPushRegistryDelegate {\n func pushRegistry(\n _ registry: PKPushRegistry,\n didReceiveIncomingVoIPPushWith payload: PKPushPayload,\n metadata: PKVoIPPushMetadata) async {\n\n guard let (handle, uuid) = parseConversationPayload(from: payload) else { return }\n\n let capabilities = [.video, .pausing, .merging]\n let update = Conversation.Update(members: [handle], capabilities: capabilities)\n try? await manager.reportNewIncomingConversation(uuid: uuid, update: update)\n }\n}"
+ },
+ {
+ "timestamp": "9:57",
+ "title": "Implement the delegate",
+ "language": "swift",
+ "code": "// Implement the delegate\n\nimport LiveCommunicationKit\n\nfinal class SampleDelegate: ConversationManagerDelegate {\n func conversationManager(\n _ manager: ConversationManager,\n perform action: ConversationAction\n ) {\n switch action {\n case let action as JoinConversationAction:\n handleJoinAction(action)\n default:\n action.fail()\n }\n }\n}"
+ },
+ {
+ "timestamp": "10:13",
+ "title": "Fulfill the join action",
+ "language": "swift",
+ "code": "// Handle a failed connection\n\nextension SampleDelegate {\n func handleJoinAction(_ action: JoinConversationAction) {\n guard let conversation = manager.conversations.first(where: {$0.uuid == uuid })else {\n return action.fail()\n }\n\n manager.reportConversationEvent(.conversationStartedConnecting(.now), for: conversation)\n\n Task {\n do {\n try await setupMediaStream(with: action.conversationUUID)\n manager.reportConversationEvent(.conversationConnected(.now), for: conversation)\n action.fulfill(dateConnected: .now)\n } catch {\n action.fail()\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "11:17",
+ "title": "Route end actions",
+ "language": "swift",
+ "code": "// Route end actions\n\nfinal class SampleDelegate: ConversationManagerDelegate {\n // …\n func conversationManager(\n _ manager: ConversationManager,\n perform action: ConversationAction\n ) {\n switch action {\n case let action as JoinConversationAction:\n handleJoinAction(action)\n case let action as EndConversationAction:\n handleEndAction(action)\n default:\n action.fail()\n }\n }\n}"
+ },
+ {
+ "timestamp": "12:14",
+ "title": "Create a start action",
+ "language": "swift",
+ "code": "let startAction = StartConversationAction(\n conversationUUID: UUID(),\n handles: [Handle(type: .phoneNumber, value: \"+1-650-555-0199\", displayName: \"Ryan Notch\")],\n isVideo: false\n)"
+ },
+ {
+ "timestamp": "12:23",
+ "title": "Perform the action",
+ "language": "swift",
+ "code": "try await manager.perform([startAction])"
+ },
+ {
+ "timestamp": "12:29",
+ "title": "Route start actions",
+ "language": "swift",
+ "code": "// Route start actions\n\nfinal class SampleDelegate: ConversationManagerDelegate {\n // …\n func conversationManager(\n _ manager: ConversationManager,\n perform action: ConversationAction\n ) {\n switch action {\n case let action as JoinConversationAction:\n handleJoinAction(action)\n case let action as EndConversationAction:\n handleEndAction(action)\n case let action as StartConversationAction:\n handleStartAction(action)\n default:\n action.fail()\n }\n }\n}"
+ },
+ {
+ "timestamp": "13:51",
+ "title": "Start group conversations",
+ "language": "swift",
+ "code": "// Start group conversations\n\nlet adam = Handle(type: .emailAddress,\n value: \"adam.halwani@icloud.com\",\n displayName: \"Adam Halwani\")\nlet david = Handle(type: .emailAddress,\n value: \"david@example.com\",\n displayName: \"David Evans\")\nlet ryan = Handle(type: .phoneNumber,\n value: \"+16505550199\",\n displayName: \"Ryan Notch\")\n\nlet startAction = StartConversationAction(\n conversationUUID: UUID(),\n handles: [david, ryan],\n isVideo: false\n)\ntry await manager.perform([startAction])"
+ },
+ {
+ "timestamp": "14:01",
+ "title": "Report group membership updates",
+ "language": "swift",
+ "code": "// Report group membership updates\n\nlet update = Conversation.Update(\n localMember: adam,\n members: [david, ryan],\n activeRemoteMembers: [david, ryan],\n capabilities: [.merging, .pausing, .unmerging]\n)\n\nmanager.reportConversationEvent(\n .conversationUpdated(update),\n for: conversation\n)"
+ },
+ {
+ "timestamp": "15:26",
+ "title": "Route merge actions",
+ "language": "swift",
+ "code": "// Route merge actions\n\nfinal class SampleDelegate: ConversationManagerDelegate {\n func conversationManager(\n _ manager: ConversationManager,\n perform action: ConversationAction\n ) {\n switch action {\n case let action as JoinConversationAction:\n handleJoinAction(action)\n case let action as EndConversationAction:\n handleEndAction(action)\n case let action as StartConversationAction:\n handleStartAction(action)\n case let action as MergeConversationAction:\n handleMergeAction(action)\n default:\n action.fail()\n }\n }\n}"
+ },
+ {
+ "timestamp": "15:33",
+ "title": "Handle the merge action",
+ "language": "swift",
+ "code": "// Handle the merge action\n\nextension SampleDelegate {\n func handleMergeAction(_ action: MergeConversationAction) {\n let sourceUUID = action.conversationUUID\n let targetUUID = action.conversationUUIDToMergeWith\n guard manager.conversations.contains(where: { $0.uuid == sourceUUID }),\n manager.conversations.contains(where: { $0.uuid == targetUUID }) else {\n return action.fail()\n }\n\n Task {\n do {\n let update = try await combineStreams(from: sourceUUID, into: targetUUID)\n manager.reportConversationEvent(.conversationUpdated(update), for: target)\n action.fulfill()\n } catch {\n action.fail()\n }\n }\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Initiating VoIP conversations with LiveCommunicationKit",
+ "url": "https://developer.apple.com/documentation/LiveCommunicationKit/initiating-voip-conversations-with-livecommunicationkit"
+ },
+ {
+ "title": "Responding to VoIP Notifications from PushKit",
+ "url": "https://developer.apple.com/documentation/PushKit/responding-to-voip-notifications-from-pushkit"
+ },
+ {
+ "title": "LiveCommunicationKit",
+ "url": "https://developer.apple.com/documentation/LiveCommunicationKit"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/226/4/f8343d5b-0c78-4396-be05-956666fb4ae0/downloads/wwdc2026-226_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/226/4/f8343d5b-0c78-4396-be05-956666fb4ae0/downloads/wwdc2026-226_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "244",
+ "year": "2025",
+ "title": "Get to know App Intents",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/244"
+ },
+ {
+ "id": "10117",
+ "year": "2022",
+ "title": "Enhance voice communication with Push to Talk",
+ "url": "https://developer.apple.com/videos/play/wwdc2022/10117"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:14.038Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-227.json b/data/wwdc/videos/2026-227.json
new file mode 100644
index 0000000..2a65e7c
--- /dev/null
+++ b/data/wwdc/videos/2026-227.json
@@ -0,0 +1,33 @@
+{
+ "id": "227",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/227/",
+ "title": "Create UI prototypes using agents in Xcode",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Design",
+ "Developer Tools",
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hi, I'm Sam, and I'm a prototyper on the Apple Design Team. Many of the interactions and delightful moments people love across the ecosystem are the results of relentless iteration, trial and error, and careful tuning. Because it's never been easier to produce an app, designing with intention is critical to standing out. That's where prototyping comes in. Prototyping is a process that lets you quickly try out lots of different design ideas, and it's more important than ever.\n\nToday, I'll explain how you can use some awesome new tools in Xcode to deal with design challenges you might run into early in the development of your app...\n\nso that you can go from an unrefined, first-pass generated interface like this, to a design that's purposeful, tailored, and distinct, like this.\n\nI'll show you how to use agents in Xcode to find creative starting points in your development process. Then, how to bring in real content and make your app feel lived in. And lastly, I'll go through some techniques for tuning key moments and interactions in your app. Before I begin, it's important you're familiarized with two powerful features within Xcode. Let's go over them.\n\nThe first is coding agents. Coding agents enable you to bring ideas to life just by describing what you'd like to build. First, click the new conversation button, then simply express what kind of code change or what feature you would like to implement, and the agents take care of the rest.\n\nThe second is Xcode previews. Xcode previews lets you visualize and interact with your UI without having to rebuild and run it every time you make a change. To access previews, you'll first need to make sure your Swift file has a preview view specified If you don't know what that is, don't worry.\n\nGet to Swift previews by clicking the show canvas button.\n\nYou should get your preview instantly. Combined, coding agents and Xcode previews let you supercharge the prototyping of key screens and moments that make your app feel thoughtful and delightful. Even better, because agents produce real native code, you will be in a position to carry that code forward as you go on to develop your app.\n\nDo not delegate critical thinking to these tools. Ultimately, your task is to use your judgment to craft what you believe is the best possible experience for people who use your app. Think of coding agents as collaborators in your prototyping process to help you discover what the best experience is. Remember, you always have final say. Now, let's start with exploring UI possibilities. Suppose I'm interested in building an app to manage a book club. If you're like me, you just want to get started. With agents right in Xcode, you might be tempted to ask, \"create a UI for managing a book club that meets regularly.\" This could generate an interesting result and quickly but the prompt is vague and it brings a few problems.\n\nThe agent generated an arbitrary layout. While it might still work, it's one of many ways of organizing an app like this.\n\nAnd it also took a guess at the app features because it wasn't clearly defined in the prompt. Suppose we aren't interested in polling or a photo gallery, making it easy to get stuck or anchored on a flawed starting point. For example, we're now stuck with this arbitrary navigation element that might have made more sense with a different feature set.\n\nBy the time we've gotten the interface to display the features we do want, it could present feature creep, looking clunky and inelegant.\n\nTo avoid this, your prompts need to be much more specific. Take some time to think through the features and high-level points you want the app to have from the get-go. When it comes to your app and what problems you want it to solve, you likely have a better idea where to start than the agent does. Give stylistic cues. If you have an intuition about the mood or feeling you want your app to evoke, express those in the prompt. For example, for a book club, do you want to capture the warm atmosphere of a coffee shop and its color palette? Or do you want to highlight the feeling of paper and beautiful typography? Lastly, and most importantly, ask for multiple options. Early on is your best opportunity to evaluate and explore multiple and divergent directions. Here's an example of a better prompt. Feel free to pause here if you'd like to read it in detail.\n\nI first ask for multiple variations I'm specific about the features I want, and I make sure that each variation gets its very own named Swift preview.\n\nAnd voila! The coding agent in Xcode generated 10 different solutions following the prompt. I can click between them like so. And so on.\n\nThe coding agent generated this version with a tab structure named Club Hub. This variation, named Cozy, uses the system New York typeface and has a nice clear section for the current book title and meeting location. It also created an interesting version with a racetrack metaphor to track progress.\n\nThis one, named Editorial, has really pleasing typography too and a very clean layout.\n\nBlueprint atelier navigates from a grid down to a detail page.\n\nAnd this one... well, it was worth a shot. You're likely to find that you like different elements of the generated proposals. In those cases, follow up with a prompt that states which variations you find promising and which specific elements you would like to remix with others. It'll help you arrive at something that feels right for you. For the book club app, I'm going to start the prompt by listing my favorite features from all the generated variations. And again, I'm asking for each iteration to get its own Swift preview and its own unique name.\n\nThe agent creates some new hybrids using only the elements I expressed interest in, including the idea of a standings board and an image of the current book.\n\nHave fun with this process. In a nutshell, go wide, remix, repeat.\n\nSo, make lots of variations and see which components or ideas inspire you. One of the greatest powers of coding agents in Xcode is their tireless ability to produce new and interesting concepts. For my book club app, I went down a path of refinement, going from this design inspired by the Cozy Club variation, to this one that integrated the racetrack visual...\n\nto this one that took a neutral appearance... to this one that simplified and reduced redundancy. I think this is a good direction for iteration.\n\nNow that I have a better sense of how I want the app to be structured, I can move into its interactivity and further refinements. To start this, I'll go on to making your app feel lived in by bringing real content. Early on in the development of your app, it's a great idea to let different people try it and give you feedback to guide your process of refinement. For some kinds of apps and interfaces though you still might be a ways off from having it in a state where people can try it with their own content.\n\nFortunately, the agent can help play the role of somebody using your app so you can get an impression of what your app feels like when it's lived in Combined with images that you can bring in, an agent can get you from this kind of blank template, only loosely filled with content, to something rich, lived in, and closer to real use.\n\nHere are some prompting tips when working through how to make your app feel lived in. First, and this is a recurring theme, ask for not just one preview, but many previews. Second, stop and think through edge cases yourself. For example, in my app, how does the detail area look if no meeting has been scheduled yet? Try to be specific about what parts of the interface state should be iterated, minimizing the risk that the agent overlooks something. Make sure that there's enough context that the sample content is plausible for your app's audience. In this case, that means I need to make sure that the sample app content, such as the discussions, center around books.\n\nPay attention to interface elements that can grow unbounded: the number of members in the club, the length of the message conversations, the number of previous books, and so on... It might also include things like long input. Is it appropriate for the text to truncate? Or to take multiple lines? If applicable, ask the coding agent to make sure that the example content it is authoring is reusable and readable in its own file so that you can go in and make changes if you need to. Here's an example prompt I might use. I ensure that I specify concretely the edge cases I can imagine.\n\nMake sure that these sample models are easy to go in and modify, as well as can be reused in future prototypes..\n\nand lastly, that each variation gets its own Swift preview and has a descriptive name that I can refer to when I revise.\n\nHere's what Xcode presents. Each variation gets its own tab, so I can quickly switch between them.\n\nIn this example, I notice I don't have any real blank slate UI, no way of specifying a new book or even to manage my account. I address that by adding account management and call-to-action controls. In this variation, I noticed that really long next meeting descriptions might overrun the book cover. I address that by allowing truncation. In fact, I realize the title of the book is redundant with its cover, so I simplify it.\n\nHere I notice that the leaderboard, when there are too many participants, gets way too long, and it takes a long time to scroll to see the discussion. Instead, I can simply ensure that somebody can always see what their relative rank is, and with an expand control, see the full list.\n\nAfter seeing the UI respond to all of these wonderful book covers, I'm inspired to try having the view adapt to their colors in the detail page. These are just some examples of how bringing in real content to your interface can help you better understand how people might experience your app. Remember, nothing beats real-world use in getting feedback from people who actually use and try your app. But this is an excellent way to get a head start on the feedback stage of your prototyping process. Now I'll focus on tuning those key moments of animation and interaction. So far, I've explained how agents in Xcode can help you iterate over the static elements of an interface, like navigation, controls. type, and color. However, Swift UI gives you the power to go beyond the static and into the world of interaction, animation, and transitions. These can be a little harder to get right, so I'll show you a couple of common animation styles and techniques that can help you tune those key moments in your app perfectly. The first animation style is ease. In this animation style, an object either gently accelerates, decelerates or does both. You get to choose how long the animation takes.\n\nSpring, as the name suggests, mimics the characteristic motion of an object attracted by a spring force. These have three parameters you can tune: stiffness, damping, and mass.\n\nOutside of animation, there are other dynamic elements you might run into with your UI. How much perceived weight should a given element have if someone is dragging it around and interacting with it? That's friction and inertia. Let's take a look.\n\nIn this example for Music, I perceive a weight when I close this sheet.\n\nDevice motion. How does your app respond to device motion from sensors like the accelerometer and gyroscope? Such as in Wallet, where my Apple Cash card exhibits this iridescent parallax effect.\n\nOr haptics? How does your app employ haptics to communicate key moments or special modes, such as the use of haptics in Find My to indicate that I'm getting closer to the object I'm trying to locate? In the app, the key moments have to do with animation, so I'm focusing on these. When I tune an animation, each value brings a slightly different feel. How can I quickly decide the values that are right for this interface? One way is to use Xcode previews to modify the relevant constants in code. This can work really well, but sometimes the constants I'm interested in live in different parts of the code, and the context switching can feel clunky. You might find it easier to create a custom UI whose job it is to help you tune those specific parameters that are relevant to your interface. Here's a simple interface to demonstrate this principle.\n\nHere I'm just tossing an interface element at a goal to understand the different spring properties at play for this kind of interaction. A menu button brings up a tuning panel that allows me to try different parameters.\n\nHere, Xcode can help you immensely. Instead of building the UI directly, enlist the agent to build a UI to display tunable parameters. When you ask the agent in Xcode for help with building a tuning panel for your UI, here are some best practices. Be as detailed as possible about what you want to iterate on. Are you exploring different animation styles or the specifics of spring curves? Does your animation feature multiple elements or does it feature a transition in which elements enter and leave your view hierarchy? To keep your tuning panel easy to understand, ask for the animation to be broken into phases. This also gives you and the agent a shared vocabulary about the specific parts of the animation.\n\nFor example, this animation comes in two phases. In phase one, the view transitions to the detail page. In phase two, every subsequent row of content animates in with staggered timings.\n\nRecognize that tuning panels can be useful for a number of purposes - not just tuning animation parameters, but for swapping between app states, colors, font styles or visual offsets.\n\nLastly, try specifying a tuning panel layout that lays out your UI side by side on a wider window size. This will allow you to toggle your UI settings and try its effects without context switching.\n\nFor example, this layout, which creates a tuning panel that obstructs the content, looks like this when displayed in a larger window.\n\nNow I sense there's something wrong with this transition. I suspect it has something to do with the delays and staggered entrance timings. Let's tune it perfectly. Here's an example prompt to use. In the overview, I specify I want a tuning panel to manage an animation. I specify the animation's phases.\n\nI specify what parameters and options I'm interested in, and I make sure the preview can be displayed side by side against the UI instead of in a clunky modal.\n\nThe agent gives me a view that I can toggle, as in the spring example, but that's clunky, and my view is obstructed. However, by clicking this resize control, I can tune my transition on a larger canvas.\n\nNow I can comfortably dial in the animation without having to jump back and forth between a tuning panel and the UI. I have created individual controls that allow me to inspect a given phase of the animation in isolation.\n\nI can quickly get an understanding of how these different parameters affect my animation.\n\nI think I need to decrease the values related to the delay and staggered entrances.\n\nLet's also try using the bouncy preset animation style.\n\nOkay, this is feeling really great. The sequence of animations that trigger after the book cover transitions feels smooth and delightful. Let's look at it again.\n\nThis is one of the most powerful flows in prototyping with coding agents in Xcode. Anytime you're trying to manage multiple configurations of a view or deciding between different animation or interaction parameters, make a tuning panel, shorten the feedback loop, and get to what feels optimal for your app.\n\nI've covered a lot today. The big theme is not to think about agents as designers, but as collaborators to help you arrive at the best possible experience for your app.\n\nI'm sure you'll think of even more creative ways to bring coding agents to your process. To learn more, watch the video \"Xcode, agents, and you.\" Remember, these tools are ultimately here to help you find the best experiences for people who use your app. The key piece of the puzzle is your judgment. Thank you.",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/227/4/f96c1da6-a49b-4d9a-8612-340d198d201b/downloads/wwdc2026-227_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/227/4/f96c1da6-a49b-4d9a-8612-340d198d201b/downloads/wwdc2026-227_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "259",
+ "year": "2026",
+ "title": "Xcode, agents, and you",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/259"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:13.881Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-230.json b/data/wwdc/videos/2026-230.json
new file mode 100644
index 0000000..3d3936f
--- /dev/null
+++ b/data/wwdc/videos/2026-230.json
@@ -0,0 +1,81 @@
+{
+ "id": "230",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/230/",
+ "title": "What’s new in assessment on macOS",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "Business & Education"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Chris, an engineer on the Education Technologies team. And I'm excited to take you through the enhancements available in the Automatic Assessment Configuration framework in macOS 27. This framework helps to create a secure, locked down environment for testing organizations delivering education assessments or certifications on Apple devices. To use this framework, your app needs the restricted Automatic Assessment Configuration entitlement. If you haven't already requested it, you can do so through the Apple Developer portal.\n\nIn this session, I'll cover five areas. First, system preconditions: checks your app can require before allowing an assessment to continue.\n\nThen, accessibility restrictions that ensure these features are only available for the students who are approved to use them.\n\nAfter that, system experience customization for tailoring how users interact with the Mac during an assessment.\n\nFrom there, application launch restrictions your app can set to ensure that only the processes you deem trustworthy are running during an assessment.\n\nAnd lastly, best practices that will help you make the most of what assessment mode has to offer.\n\nI'll be using this sample assessment app to demonstrate the behavior of the API capabilities covered in this video.\n\nFirst up, I'll review the precondition checks that can be done before an assessment starts. Performing system precondition checks before allowing a student to begin an exam is critical for ensuring the Mac is in a hardened state, and helps ensure the security requirements of the assessment from the very start.\n\nYour app can require that System Integrity Protection is enabled, that the Mac is MDM enrolled, that only a single account is signed in, and that the account is a specific type, such as a standard account; four checks that together help ensure the device is in a hardened, tamper-resistant state.\n\nAdditionally, your app can require that Lockdown Mode and iCloud Private Relay are disabled; two checks that help ensure that Apple privacy and security features don't interfere with your assessment infrastructure requirements.\n\nI'll now add the precondition checks to my sample app, by setting properties on my AEAssessmentConfiguration object, which is the object that defines an assessment session's parameters. This code snippet requires the signed-in user account to be a standard account. With these precondition checks configured, if a student's device does not meet one or more of these requirements then an alert is displayed informing the student of the issues that must be addressed before they may continue.\n\nNext up: managing the availability of accessibility features. MacOS includes a comprehensive suite of built-in accessibility features. These features are essential for providing equitable access to exams, enabling individuals with visual, auditory, motor, or cognitive needs to fully participate without requiring third-party assistive software.\n\nBy default, the Menu Bar and the Dock are hidden, but any currently enabled accessibility features will continue to work during the assessment session.\n\nAs demonstrated here, Switch Control continues to run after the assessment session begins.\n\nHowever, it's important to note that certain accessibility features can be customized with user-generated content. Therefore, disabling access to those features for students who do not require them as part of an approved accommodation is an important security measure. In this configuration, every accessibility feature is allowed except one. I'm restricting the use of Switch Control. Note that setting the value to true for a configuration property does not enable the corresponding accessibility feature. It simply allows it to be used during an assessment when enabled by the user.\n\nWhen the assessment session starts, Switch Control is automatically quit and cannot be relaunched during the assessment session.\n\nOne of the most powerful areas of the framework is system experience customization, tailoring how students interact with macOS during an assessment.\n\nThe macOS system experience is designed to provide a seamless, intuitive environment through familiar elements like the Menu Bar, Menu Bar items such as Wi-Fi and Volume, the Dock, various input technologies like Dictation and AutoFill, and filesystem interaction using the Finder and the Open and Save file dialogs. Your app can make the Menu Bar available to allow students to access essential application functions during an assessment. Additionally, your app can customize which Menu Bar items are available, like volume or Wi-Fi, while removing items that could serve as a vector for information or content leakage.\n\nI've already demonstrated how the assessment session looks without a Menu Bar. In this example, I'm setting additional properties on my AEAssessmentConfiguration object to enable the Menu Bar and to define a set of allowlisted menu extras.\n\nNote that these menu extras are not forced on but rather continue to be available during an assessment session if they were already present in the Menu Bar.\n\nI've also customized the Apple menu by setting a property on my configuration object to show only the sleep menu item. Optionally, I could set an empty array to hide all items except for \"About This Mac\".\n\nAs the assessment session begins, the app's menu items are displayed and the set of Menu Bar items is filtered down to the allowlisted set I just specified. Additionally, selecting the Apple menu reveals that its contents have been filtered.\n\nNow I'll review some input methods you may wish to disable that can inadvertently provide students with hints or correct answers.\n\nDictation can produce correct spelling automatically. The emoji picker exposes a searchable symbol library. And structural input reveals character composition clues, any of which could bypass assessments designed to test unaided recall.\n\nAutoFill can provide pre-loaded answers, notes, or reference material into response fields from sources such as Contacts. To restrict these input methods, I set each of these properties to false in my AEAssessmentConfiguration object. This hides them in menus and prevents their use within UI controls that normally support them.\n\nWith the input restrictions in place, the AutoFill, Dictation, and Emoji & Symbols menu items are no longer available in the Edit menu.\n\nThe Dock is another valuable system experience you may wish to enable during an assessment session because it provides students with a clear, focused workspace where they can easily find and switch between applications. In this code snippet, I'm enabling a filtered Dock experience by setting allowsDock to true.\n\nDuring the assessment, the Dock displays only the allowed apps, giving students a focused workspace to find and switch between them.\n\nUpon entering the assessment session, note that in addition to the always present anchor elements of the Finder and the Trash, only the allowed apps for the session are present. However, even though the Finder is present, it is not accessible unless it is explicitly added as a participant.\n\nIf your assessment app requires interaction with designated files, then you may wish to allow access to the Finder and make use of the standard Open and Save dialogs. They provide a consistent, intuitive way to browse, organize, and access files across macOS. Next, I'm adding the Finder as a participant in the assessment session. With the allowedDirectoriesAndFiles property on the AEAssessmentConfiguration object, I can allowlist the Documents directory into which a student can save their scratch paper work in the Sample Assessment app. This same setting also filters the directories and files available in the standard Open and Save panels.\n\nWhen the student chooses to save their scratch paper work, they are presented with a standard Save panel, but only the allowlisted directory is available for them to save their work into.\n\nBringing up a Finder window shows the same filtered access and the allowlisted directory contains the scratch paper file the student just saved.\n\nThere's one more layer of control to cover: restricting which processes are allowed to run during an assessment. Processes that are not essential to the execution of your assessment app can pose a threat to the integrity of the assessment session. They can be used to capture screen content, log keystrokes, transmit data to external parties, or otherwise interact with the system in ways that undermine the secure testing environment. Consequently, you may wish to restrict the allowed processes to just your assessment app and participant apps you allowlist. Additionally, shortcuts and automator actions can also be shut down and their execution blocked. Setting allowOnlyParticipantsToRun to true tells the system to shut down non-essential processes when the assessment begins. With this enabled, only the main assessment app, its explicitly allowlisted participants, and essential system processes are permitted to run. I'm also setting allowsUserScriptExecution to false to prevent Shortcut and Automator scripts from executing.\n\nBefore starting the assessment, both Safari and Notes are running as well as a long running Shortcut. However, after starting the assessment, only the sample assessment app and its allowlisted participant, Finder, are still running. Any user-initiated background processes, including the Shortcut, have also been stopped and cannot be accessed during a secure exam.\n\nWhether adopting Assessment Mode for the first time or hardening an existing integration, the following best practices are ones that every developer should consider.\n\nThese practices will help with how you adopt the framework, how you shape the test-taker's experience, and how you keep your app functioning well across macOS releases.\n\nWith its deep system integration, let the Assessment framework do the work to secure the system environment for you. Resist the urge to roll your own equivalents by adopting the framework's APIs directly and deleting the redundant code you've been maintaining.\n\nOnly restrict the minimum that your assessment actually requires. Restrictions you add can detract from the test-taker's experience, so start permissive and tighten deliberately.\n\nAccessibility should be treated as a requirement in an assessment context. Every student deserves a fair testing experience, so design your app from day one to accommodate assistive technologies rather than treating them as exceptions to be carved out later.\n\nIf you're new to AAC, take note that session transitions don't occur the moment you call the begin and end session API. Register for the framework's transition callbacks and drive your app's state off of those events so you always know when a session truly started, ended, or was terminated unexpectedly.\n\nIt's critical to re-validate your app on every macOS beta the day it drops. Run your full assessment test matrix against it, and file Feedback reports immediately. Waiting until the release to discover a regression means your customers discover it too.\n\nNow that you've seen what's possible, consider these next steps to take your assessment app to the next level. Use system pre-checks to validate device integrity before starting an assessment.\n\nEnable accessibility features to provide an equitable assessment experience for all users.\n\nProvide a familiar and intuitive experience by customizing access to Menu Bar items, the Dock, and the file system.\n\nSecure your assessment's runtime environment by blocking non-essential processes.\n\nAnd lastly, test your assessment solution with real exam workflows on macOS.\n\nWhether you're building a classroom quiz app or a nationwide standardized testing platform, the AutomaticAssessmentConfiguration framework on macOS gives you the tools to verify system security prerequisites, tailor accessibility accommodations, customize the user experience, and lock down the runtime environment, all through a single, unified API, tailorable to your needs. I can't wait to see how you use these new capabilities to deliver safer, more inclusive, and more polished assessment experiences for students everywhere. Thank you for joining me.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "2:30",
+ "title": "Set up precondition checks",
+ "language": "swift",
+ "code": "import AutomaticAssessmentConfiguration\n\nfunc makeAssessmentConfiguration() -> AEAssessmentConfiguration {\n let configuration = AEAssessmentConfiguration()\n\n configuration.allowLockdownMode = false\n configuration.allowPrivateRelay = false\n configuration.requiresSIP = true\n configuration.requiresManagedDevice = true\n configuration.requiresSingleUser = true\n configuration.requiresUserAccountType = .standard\n\n return configuration\n}"
+ },
+ {
+ "timestamp": "4:01",
+ "title": "Restrict accessibility features",
+ "language": "swift",
+ "code": "import AutomaticAssessmentConfiguration\n\nfunc makeAssessmentConfiguration() -> AEAssessmentConfiguration {\n let configuration = AEAssessmentConfiguration()\n\n configuration.allowsAccessibilityVoiceOver = true\n configuration.allowsAccessibilitySwitchControl = false\n configuration.allowsAccessibilityAlternativeInputMethods = true\n configuration.allowsAccessibilityBackgroundSounds = true\n configuration.allowsAccessibilityHoverText = true\n configuration.allowsAccessibilityLiveSpeech = true\n configuration.allowsAccessibilitySpokenContent = true\n configuration.allowsAccessibilityVoiceControl = true\n configuration.allowsAccessibilityZoom = true\n\n return configuration\n}"
+ },
+ {
+ "timestamp": "5:32",
+ "title": "Customize the Menu Bar items",
+ "language": "swift",
+ "code": "import AutomaticAssessmentConfiguration\n\nfunc makeAssessmentConfiguration() -> AEAssessmentConfiguration {\n let configuration = AEAssessmentConfiguration()\n\n configuration.allowsMenuBar = true\n configuration.allowedMenuBarItems = [\n .battery,\n .clock,\n .volume\n ]\n configuration.allowedAppleMenuItems = [\n .sleep\n ]\n\n return configuration\n}"
+ },
+ {
+ "timestamp": "7:01",
+ "title": "Define input restrictions",
+ "language": "swift",
+ "code": "import AutomaticAssessmentConfiguration\n\nfunc makeAssessmentConfiguration() -> AEAssessmentConfiguration {\n let configuration = AEAssessmentConfiguration()\n\n configuration.allowsDictation = false\n configuration.allowsAutoFill = false\n configuration.allowsStructuralInput = false\n configuration.allowsEmojiKeyboard = false\n\n return configuration\n}"
+ },
+ {
+ "timestamp": "7:38",
+ "title": "Enable dock appearance",
+ "language": "swift",
+ "code": "import AutomaticAssessmentConfiguration\n\nfunc makeAssessmentConfiguration() -> AEAssessmentConfiguration {\n let configuration = AEAssessmentConfiguration()\n\n configuration.allowsDock = true\n\n return configuration\n}"
+ },
+ {
+ "timestamp": "8:35",
+ "title": "Set allowed directories and files",
+ "language": "swift",
+ "code": "import AutomaticAssessmentConfiguration\n\nfunc makeAssessmentConfiguration() -> AEAssessmentConfiguration {\n let configuration = AEAssessmentConfiguration()\n\n configuration.allowedDirectoriesAndFiles = [\n URL(fileURLWithPath: \"~/Documents/\")\n ]\n\n return configuration\n}"
+ },
+ {
+ "timestamp": "9:58",
+ "title": "Set application launch restrictions",
+ "language": "swift",
+ "code": "import AutomaticAssessmentConfiguration\n\nfunc makeAssessmentConfiguration() -> AEAssessmentConfiguration {\n let configuration = AEAssessmentConfiguration()\n\n configuration.allowOnlyParticipantsToRun = true\n configuration.allowsUserScriptExecution = false\n\n return configuration\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Automatic Assessment Configuration",
+ "url": "https://developer.apple.com/documentation/AutomaticAssessmentConfiguration"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/230/4/03914f48-0bbe-4f2d-bb09-3ae676579cf2/downloads/wwdc2026-230_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/230/4/03914f48-0bbe-4f2d-bb09-3ae676579cf2/downloads/wwdc2026-230_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "201",
+ "year": "2026",
+ "title": "Secure your apps with App Attest",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/201"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:14.109Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-232.json b/data/wwdc/videos/2026-232.json
new file mode 100644
index 0000000..905b7fa
--- /dev/null
+++ b/data/wwdc/videos/2026-232.json
@@ -0,0 +1,102 @@
+{
+ "id": "232",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/232/",
+ "title": "Run local agentic AI on the Mac using MLX",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Angelos, an engineer on the MLX team. Today I'm going to show you how to build and run agentic AI workflows entirely on your Mac using MLX. No cloud, no API keys, just your hardware doing the work. Over the past year, AI agents have gone from research prototypes to everyday productivity tools. But before we talk about agents, let's look at what we had before.\n\nHere's the chat experience you're familiar with. You send a prompt to the language model. The model sends a response back. If you need to act on that response, run a command, check a file, or fix an error, that's on you. But now you're talking to an agent. The agent talks to the model to decide what to do. Then it calls tools to actually do it: running commands, reading files, hitting APIs — It observes the results and goes back to the model to figure out the next step. User to agent. Agent to model. Agent to tools. This is the agentic loop. And it keeps cycling until your task is done. What makes this particularly exciting on Apple silicon is that the entire loop can run locally. Your data stays on your machine; AI is available anywhere at any time and there are no usage costs. Let me now show you what this looks like in practice. Here I have an agent running locally on my Mac. On my screen you can see the setup: on the left, MLX running the model, and on the right the OpenCode agent I am interacting with. I asked it to fetch the recent pull requests from our MLX repository, summarize the changes, and identify anything that needs my attention. The model reasons about the request, calls the GitHub CLI to fetch PR data, reads through the diffs, and produces a concise summary. All of this is happening locally, the model runs on my hardware and only the git commands reach the network. Well it seems like I have a lot of work to do after finishing this video. Now that you've seen what's possible, let me walk you through how we'll get there today. We'll start by introducing the local agentic AI stack, the four layers that make all of this work, from MLX at the foundation all the way up to the agent. Then I'll show you step-by-step how to set up your own local agent. After that, we'll look at how MLX gets the most out of your hardware to make agents fast. And finally, we'll go through more live demos, including building a SwiftUI app from scratch and fixing a bug in Xcode. Let's start with the stack.\n\nThe stack that powers local agentic AI on the Mac has four layers. Let me walk you through each one, starting from the bottom. At the bottom is MLX, our open-source array framework purpose-built for Apple silicon. It handles all the low-level computation, Metal acceleration, and memory management. This is the foundation everything else is built on. One level up, we have the language model layer. MLX-LM provides everything you need to load, run, quantize, and fine-tune large language models. It supports thousands of models from HuggingFace and gives you both CLI tools and a Python API. If you saw our sessions last year, this is what we covered in depth. But to serve an agent, we need something more: a persistent server with a standard API. That's where MLX-LM Server comes in. This is an OpenAI-compatible HTTP server that exposes your local model through a standard API. It supports structured tool calling so the model can invoke functions reliably, and reasoning models that can analyze complex problems step-by-step before responding. It's a drop-in replacement for any cloud LLM API. And at the top of the stack, we have the agent itself. This can be any framework or tool that speaks the OpenAI chat completions protocol: Xcode, OpenCode, Pi agent, a custom script, or anything else. Because MLX-LM Server provides a standard interface, any agent framework works out of the box. And it's not just us building on this stack. Several popular apps and tools build on MLX and MLX-LM. Ollama, LM Studio, and vLLM are just a few of the most popular ones. The ecosystem is broad and growing, and if you're using one of these tools, chances are you're already running on MLX. So that's the stack. Let me now show you how to set everything up yourself. It only takes three steps to go from zero to a fully local agentic workflow. Step one: install MLX-LM. A single pip install gets you everything you need. Step two: start the server. Run mlx_lm.server with a model that supports tool calling. Starting with a small model to test your set-up is always a good idea. The server starts up, loads the model, and is ready to accept requests on local host. Step three: point your agent at the local server. In most agent frameworks, you just set the base URL to your local server's address and you're done. The agent doesn't know or care that the model is running on your Mac rather than in the cloud.\n\nLet me show you a concrete example. Here's the configuration for OpenCode. We define a local provider. In particular, we set the URL to local host and set the model name the server expects. We also tell OpenCode to use this local model for everything. That's it. Now every interaction runs through your local model. Now that we have an agent talking to MLX, let's look at how MLX gets the most out of your hardware and addresses the key challenges of running agents locally.\n\nThe first challenge is prompt processing. In an agentic workflow, every time the model receives tool output, it has to process all that new context before it can reason about the next step. This happens over and over throughout the agentic loop, and it adds up fast. Agentic sessions usually comprise hundreds of thousands of tokens and most of those are not generated.\n\nThe M5 chip introduces dedicated Neural Accelerators, and MLX can target them for exactly this kind of work. Specifically, Neural Accelerators make matrix multiplication four times faster on M5 compared to M4. And with the specialized multiplication and attention kernels in MLX this translates almost exactly to prompt processing speedup.\n\nReducing prompt processing time means your agents can read your codebase or process tool results almost four times faster. And the best part? Taking advantage of Neural Accelerators requires no special arguments or code changes on your part, MLX selects the best kernel for the available hardware and it just works.\n\nLet's now talk about the second challenge, concurrency. In practice, agents rarely work alone. A common pattern is for an agent to spawn several subagents, each tackling a different part of the problem in parallel. One might be reading documentation, another searching code, and a third writing tests; all at the same time. That means multiple requests hitting your local model simultaneously. MLX-LM Server handles this with continuous batching.\n\nInstead of processing requests one at a time, it dynamically groups incoming requests into batches and processes them together on the GPU. New requests can join a batch in progress without waiting for the current one to finish. The result is that your subagents don't stall waiting in a queue. They all get served concurrently, which keeps the entire agentic workflow moving. Finally, the third challenge is model size. Sometimes a single machine, even one with 512GB of RAM, just isn't enough because the model is too large to fit in memory. The most recent DeepSeek model for instance has a whopping 1.6 trillion parameters and requires more than 800GB of memory just for the weights. MLX's distributed support lets you spread a model across multiple Macs connected over Thunderbolt or Ethernet. For agents, this is powerful in two ways. First, it lets you run much larger, more capable models that wouldn't fit on a single machine. Second, it parallelizes prompt processing across devices, which directly speeds up the agentic loop since the model can process tool results faster.\n\nSetting up distributed inference with MLX-LM Server is fairly straightforward. You launch the server using mlx.launch and a hostfile that contains information about the nodes and the type of connection. The model is automatically sharded across all available devices and everything else just works. Starting with macOS 26.2, we have support for Thunderbolt RDMA, which provides low-latency, high-bandwidth communication over Thunderbolt. As a result, distributed inference with MLX has seen significant speed-ups: up to three times with four nodes. To learn how to set up your Macs for distributed inference with MLX, check out our session \"Explore distributed inference and training with MLX\". Remember our PR summary demo from earlier? That was a simple read-and-report task. Let's now push things further and see what happens when we ask an agent to write an entire project from scratch and then fix a bug in an existing one.\n\nIn this demo, I'm going to ask the agent to build a small SwiftUI application from scratch.\n\nI have started with a blank Xcode project and I am asking the agent to build a drawing app for the iPad.\n\nAnd off it goes. The agent first looks at the current directory to find out the existing project structure, makes a plan to guide its implementation, and gets on to writing the code. Using an agent means we don't need to copy anything or even build the project. The agent writes the file then builds the app, fixing any errors it encounters along the way.\n\nAnd here we are: the model is done, it only took a couple of minutes to create the first version of the app. At the same time, I have the project open in Xcode and I am launching the app in the simulator.\n\nLet's have a look at what the agent created.\n\nIt seems that we have a fully functional drawing app. That's really nice for something that was built in 2 minutes. With agentic coding, however, we can keep iterating until we are happy with the result. For instance, I prefer rounded end caps. I think they look much better. Let's ask the agent to add them.\n\nThe agent will edit the code and recompile the app until it compiles without errors.\n\nLet's test the new version.\n\nWe now have rounded end caps. This is cool indeed. It is even more cool that all of this happened locally, the model ran through MLX-LM server on this Mac and the agent used standard development tools like xcodebuild to verify and build its work.\n\nFor our final demo, let's look at something that integrates directly with your development environment.\n\nHere I have the same drawing app project open in Xcode. Let's connect Xcode to our already running MLX server. We open the settings and navigate to the Intelligence tab. We click on Add Chat Provider... and select a Locally Hosted provider. We set the Port to 8080 or whichever port we selected when launching our MLX server and we're done. Now Xcode can talk to our local model.\n\nI have introduced a bug to our previously working app and now we can ask the model to fix it.\n\nWithin seconds, it identifies the bug and inspects the code around it. Finally, it writes a fix and we can now build and run our app.\n\nThis shows how a locally running agent can integrate with your existing development workflow in Xcode, reading project files, understanding build errors, and making targeted fixes. Local AI means your code never leaves your Mac. Today, we showed you the full stack for running agentic AI locally on your Mac, from MLX all the way up to the agent, and how Neural Accelerators, continuous batching, and distributed inference make it fast. To get started, install MLX-LM, launch the server, and point your favorite agent at it. Everything we showed today is open-source and available right now. Thank you for watching and I'm excited to see what you build with local agentic AI on the Mac.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "4:40",
+ "title": "Set up MLX-LM and start the local server",
+ "language": "swift",
+ "code": "# Step 1: Install MLX-LM\npip install mlx-lm\n\n# Step 2: Start the server\nmlx_lm.server --model mlx-community/Qwen-3.5-4B-8bit\n\n# Step 3: Point your agent to the server\ncurl -X POST \\\n http://127.0.0.1:8080/v1/chat/completions \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"default_model\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello!\"}]}'"
+ },
+ {
+ "timestamp": "5:18",
+ "title": "Configure an agent to use your local MLX server",
+ "language": "swift",
+ "code": "{\n \"$schema\": \"https://opencode.ai/config.json\",\n \"model\": \"mlx/default_model\",\n \"small_model\": \"mlx/default_model\",\n \"provider\": {\n \"mlx\": {\n \"npm\": \"@ai-sdk/openai-compatible\",\n \"name\": \"MLX (local)\",\n \"options\": {\n \"baseURL\": \"http://127.0.0.1:8080/v1\"\n },\n \"models\": {\n \"default_model\": {\n \"name\": \"Default MLX Model\"\n }\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "8:33",
+ "title": "Launch distributed inference with MLX",
+ "language": "swift",
+ "code": "mlx.launch --hostfile hosts.json \\\n --backend jaccl \\\n /remote/path/to/mlx_lm.server \\\n --model mlx-community/Qwen-3.5-122B-A3B-8bit"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "MLX Swift LM on GitHub",
+ "url": "https://github.com/ml-explore/mlx-swift-lm"
+ },
+ {
+ "title": "MLX Swift Examples",
+ "url": "https://github.com/ml-explore/mlx-swift-examples"
+ },
+ {
+ "title": "MLX Examples",
+ "url": "https://github.com/ml-explore/mlx-examples"
+ },
+ {
+ "title": "MLX Swift",
+ "url": "https://github.com/ml-explore/mlx-swift"
+ },
+ {
+ "title": "MLX LM - Python API",
+ "url": "https://github.com/ml-explore/mlx-lm"
+ },
+ {
+ "title": "MLX Explore - Python API",
+ "url": "https://github.com/ml-explore/mlx"
+ },
+ {
+ "title": "MLX Framework",
+ "url": "https://mlx-framework.org"
+ },
+ {
+ "title": "MLX",
+ "url": "https://ml-explore.github.io/mlx/"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/232/4/f309be4a-8e5b-4c0f-843a-fcbd84c5e2d1/downloads/wwdc2026-232_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/232/4/f309be4a-8e5b-4c0f-843a-fcbd84c5e2d1/downloads/wwdc2026-232_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "233",
+ "year": "2026",
+ "title": "Explore distributed inference and training with MLX",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/233"
+ },
+ {
+ "id": "328",
+ "year": "2026",
+ "title": "Explore numerical computing in Swift with MLX",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/328"
+ },
+ {
+ "id": "298",
+ "year": "2025",
+ "title": "Explore large language models on Apple silicon with MLX",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/298"
+ },
+ {
+ "id": "315",
+ "year": "2025",
+ "title": "Get started with MLX for Apple silicon",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/315"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:13.980Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-233.json b/data/wwdc/videos/2026-233.json
new file mode 100644
index 0000000..c3c64a3
--- /dev/null
+++ b/data/wwdc/videos/2026-233.json
@@ -0,0 +1,138 @@
+{
+ "id": "233",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/233/",
+ "title": "Explore distributed inference and training with MLX",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Tatiana, research scientist at MLX team. It's been a remarkable time for local LLMs: models keep getting larger and gaining new amazing capabilities -- becoming smarter and handling harder problems. And as they improve, we use them for more: longer contexts, harder tasks, more complex workflows. Eventually, memory, compute, or bandwidth on a single machine becomes a limitation. In our WWDC 26 video \"Run local agentic AI on the Mac using MLX\" it is shown how to run AI agents locally. But when you have multiple devices, you can take local AI even further, running larger LLMs or accelerating them through distributed inference and training. Today, we'll take a deep dive into scaling across multiple Macs with MLX, using the hardware right on your desk. We'll start with the command line interface to get models running on your machines, move to the Python API for experimentation, and finish with Swift for embedding these workflows directly into your apps.\n\nLet's start! First, we'll look at the full hardware and software stacks to make distributed workloads on Apple Silicon possible. Then we'll put everything together: turn four M3 Ultras into a cluster. We'll walk through every step: choosing the right topology to connect machines, enabling fast communication, and launching distributed jobs.\n\nOnce the cluster is ready, we'll get to the exciting part: fast and local distributed LLM inference and finetuning. We'll run it with MLX, compare it side by side against a single Mac, and look at how MLX distributes the model across the cluster.\n\nHaving most examples in command line interface, in the end we'll show how distributed communication is also exposed to you via Python, Swift and C++ APIs. Let's start by looking at distributed communication for Apple Silicon. To send and receive data fast, machines need to be connected with a physical link — an interconnect. On top of that, we also need a transport protocol — a mechanism that pushes bytes from one machine's memory to another's. Starting in macOS 26.2, Remote Direct Memory Access protocol, shortly RDMA, is supported over Thunderbolt 5. RDMA moves data directly from one machine's memory to another's, avoiding most CPU and operating system overhead.\n\nRDMA over Thunderbolt gives us the high-bandwidth — low-latency communication we need for distributed workloads. However, alone, it gives us raw data movement between two machines only. Thus, distributed programs need something higher-level — a communication backend which provides communication primitives for sending data between individual machines or coordinating across the entire group. These two operations are building blocks of distributed training and inference. And this is where JACCL comes in.\n\nJACCL is an open-source collective communication library built by Apple. It leverages RDMA over Thunderbolt and gives you collective communication primitives for sending data between machines and combining results across the group — without managing any of the low-level transport yourself. And it's not limited to machine learning — any distributed workload on Apple Silicon can be built on top of it.\n\nAnd the final piece of the stack is a machine learning framework that uses the communication backend for distributed inference and training — that's MLX.\n\nMLX is an open-source machine learning library built by Apple for Apple Silicon. It leverages JACCL for low-latency distributed communication and provides tools for orchestrating distributed jobs across the cluster. If you're new to MLX, check out our video \"Getting Started with MLX on Apple Silicon\" from WWDC25.\n\nSo now we understand the full stack. Let's put it all together and build a cluster — a group of machines that work together on the same task. We will use 4 M3 Ultras.\n\nTo setup the cluster, we need to connect the machines with Thunderbolt 5 cables. There are different ways to wire them together, and the topology directly affects the communication time.\n\nSo to begin with, we'll look at what defines that time. Next, we'll look at how to actually connect the machines — which topologies JACCL supports, and the trade-offs between them.\n\nAfter that, we'll show how to enable RDMA on the machines for fast communication. And finally, we'll launch distributed jobs on the cluster using MLX.\n\nSo, communication time has two components: latency and transfer time. Latency is the fixed cost paid for each communication operation, independent of the amount of data being sent.\n\nTransfer time is the cost of moving the data though the link; it grows with message size and depends on the bandwidth of the link.\n\nFor small messages, the data movement cost is tiny, so latency dominates.\n\nFor large messages, the trade off is opposite. Depending on whether communication is latency-bound or bandwidth-bound, we may prefer different topologies.\n\nJACCL supports two of them: a mesh and a ring.\n\nIn a full mesh, every machine connects directly to every other, thus any group communication has the lowest possible latency. In a ring, each node connects only to its two neighbors. Communication between nonadjacent nodes must travel through intermediate machines which increases latency. However, the ring requires fewer cables and ports per machine, making it easier to scale to more nodes. And because each node has only two connections, we can use the extra Thunderbolt ports to run two or tree cables per neighbor (depending on the Mac) — thus increasing the bandwidth per link and reducing transfer time.\n\nWhen machines are connected into a mesh, we have the flexibility to route each communication through either a mesh topology or a ring topology.\n\nWhat's nice about JACCL, it automatically picks the best topology depending on the message size and communication operation — mesh when latency matters, ring when bandwidth matters. For this flexibility, let's connect all M3 Ultras into a mesh.\n\nAs we connected all M3 Ultras together, now we need to enable RDMA on all machines. Open settings on the machine, search for \"RDMA\", click on \"Enable RDMA over Thunderbolt\", enable RDMA, and reboot.\n\nGreat! Macs are connected with Thunderbolt 5 cables, and RDMA is enabled. Now we need a way to launch distributed programs.\n\nOne way to do it, is over the local network, for example, through wifi or ethernet. From any machine with SSH access to the cluster, for example MacBook in my case, we connect to each Mac, start the program, and from that point on, all machines communicate directly over the Thunderbolt links.\n\nMLX provides a launch helper, which exactly does all of this for you! You run mlx.launch on your MacBook and it orchestrates the cluster. You give it the executable you want to run and a JSON hostfile describing your cluster. From there, it SSHes into each node using hostnames from provided hostfile and starts the executable on every machine.\n\nLet's see how the hostfile that describes the cluster should look like.\n\nIt is a JSON array — one entry per node. \"ssh\" is the hostname used by mlx.launch to reach the machine. \"ips\" is the machine's IP on your local network used by JACCL for initial coordination between nodes. And \"rdma\" is a list of the RDMA device names for each Thunderbolt peer connection.\n\nYou can write it manually, but MLX also provides a helper script `mlx.distributed_config` that generates it for you.\n\nYou pass the list of hostnames, and an output path. You can also embed environment variables in the config. They will be set automatically on every node at launch time. Here we set MLX_METAL_FAST_SYNCH=1, which enables faster GPU-to-CPU synchronization. It is critical for distributed tasks because computation runs on the GPU while communication runs on the CPU. You can also pass the --auto-setup flag to configure the Thunderbolt network automatically. Communication --backend argument defines whether it is a mesh or ring: for a mesh, --backend is set to jaccl, as in this example; for a ring, we would change it to jaccl-ring.\n\nLet's run this command and generate the hostfile for our cluster.\n\nFirst, it checks that all hosts are reachable over SSH. Then it probes each machine's Thunderbolt ports to discover which machines are physically connected to which — building a map of the topology. Since we passed --auto-setup, it disables the Thunderbolt Bridge on all machines and configures each Thunderbolt link for RDMA. Finally, it writes a JSON hostfile with everything mlx.launch needs. Note, that without --auto-setup flag, script prints the configuration commands, so you can review them and run yourself.\n\nNow, the cluster is ready. Let's move to the exciting part — distributed language model inference and finetuning. And the easiest way to start is via command line interface and MLX LM. MLX LM is an open-source Python package built on top of MLX that provides command-line tools and a Python API for running language models locally on Apple Silicon. Check out our video, \"Explore large language models on Apple Silicon with MLX\" from WWDC25 to get started on a single device.\n\nAs we showed last year, chatting with a model on a single Mac can be done via command line interface with mlx_lm.chat. We run it in the terminal, specifying the model we want to use, for example, Qwen 3.6, and the maximum number of tokens for the response. Under the hood, MLX LM loads and runs the model on a single machine.\n\nTo chat with the same model on the cluster via command line interface, we wrap the command with mlx.launch. On our MacBook, in the terminal we run mlx.launch with the --hostfile pointing to our cluster configuration. After the double dash, we pass the exact same mlx_lm.chat command — but using the remote path to the executable on each node. The command is almost identical, MLX LM shards the model and coordinates the distributed inference for you. Keep in mind that all necessary libraries like MLX must be installed on each Mac and the executable must be accessible on all machines.\n\nOne line via command line interface, and we're running a model spread across the entire cluster! Let's try both side by side and chat with Qwen 3.6 — a 27-billion-parameter model — on a single M3 Ultra and on 4 of them.\n\nI've already started mlx_lm.chat on both sides — on the left, the model is loaded on a single M3 Ultra; on the right, it's sharded across four machines.\n\nLet's prompt both with \"Implement a transformer model in MLX.\" It is a quite impressive speed up! The cluster generates tokens at nearly three times the rate of a single machine for Qwen 3.6 model.\n\nAs we see, running a model across multiple Macs can significantly boost inference speed. The exact speedup depends on the model size and architecture. But time improvement is not the only reason to go distributed, sometimes a model is simply too large for one machine. Kimi 2.6, for example, has 1 trillion total parameters. Even with 8-bit quantization, the weights alone require about one terabyte of memory. That does not fit on a single M3 Ultra, but it can fit across four. So how do we actually split the weights and computation across machines? MLX and MLX LM support two approaches: pipeline and tensor parallelism.\n\nPipeline parallelism splits the model by depth. In this case, each machine holds a group of layers, and data moves through the machines sequentially. It does not speed up the inference, because each token still has to pass through the layer groups one after another. But the benefit is simple communication: machines only exchange activations at the boundaries between layer groups.\n\nTensor parallelism splits the model by width. In this case, each machine holds part of every layer, so all machines process the same token at the same time. It improves inference speed due to parallelized per-layer computation. However the trade-off is much more frequent communication, that happens at every layer and for every token. This makes low latency important, and that is why the mesh topology is crucial for this case — every machine can reach every other machine in a single hop.\n\nTensor paralelism is the default sharding strategy in MLX LM. To shard the model with pipeline parallelism, we can simply append a flag --pipeline to the command. Note, that not all models support pipeline parallelism.\n\nNow, let's chat with a one-trillion-parameter Kimi 2.6 on our cluster.\n\nFor this we use mlx.launch from our MacBook as before, pointing to the hostfile. I'm not passing the --pipeline flag, so we're using tensor parallelism. We need to wait a moment — mlx.launch is connecting to every machine, MLX LM loads and shards the model, and starts the chat.\n\nGreat, the model is loaded! Let's prompt model with: \"Implement machine learning architecture for GPT in Python with MLX\".\n\nAnd there we go — with one command, a massive trillion-parameter model is running locally across your Macs, answering your questions.\n\nWith MLX and MLX LM, you can not only run language model inference, you can also fine-tune models on your hardware. Fast, efficient, and fully private — your data never leaves your machines. Let's start with a single Mac, and then scale to our cluster.\n\nWhen fine-tuning or training on a single machine, we split the training data into batches — a set of multiple examples.\n\nFor each batch, the Mac computes gradients and updates the model weights. We repeat this process for one or more passes over the training dataset, until the model reaches the desired quality.\n\nThe faster we process the training data, the sooner fine-tuning finishes. So how can we use multiple machines to speed this up? The idea is straightforward. We replicate the model on every Mac.\n\nEach machine receives a different batch of data and computes gradients locally. Then we average the gradients, so the model's update uses information from all batches. This is called data-parallel training because the model is replicated, while the data is processed in parallel across machines — this is what gives us the speedup.\n\nSo with N machines we can process data up to N times faster. Sounds amazing! Lets see how we can use data parallelism with MLX LM.\n\nAs before, the only difference from a single device is launching the job with mlx.launch from your MacBook, specifying a path to mlx_lm.lora on remote machines. Data sharding is handled by MLX LM and the command is almost identical — we scale --batch-size by the number of devices so each machine still processes the same number of samples per step as before.\n\nLet's fine-tune Qwen 3.5 with 9 billion parameters on a single machine and on the cluster, and compare the number of tokens the model processes per second.\n\nWe are launching fine-tuning on a single device on the left and on the cluster on the right using mlx.launch and hostfile, specifying path to mlx_lm.lora on the remote machine.\n\nFirst, it loads data and model; and then training starts. Single M3 Ultra is processing around 180 tokens per second, while on the cluster we process around 600 tokens per second, which gives us more than 3 times speed up for fine-tuning. Now, with MLX, you can turn your devices into a local training cluster for efficient fine-tuning without moving to a cloud.\n\nSo far, we used command line interface for distributed inference and fine-tuning within MLX LM. However, MLX provides a fine-grained control over sharding and distributed operations, via flexible Python, Swift, and C++ APIs.\n\nThis allows you to experiment with models in Python and C++ or embed models into your App with Swift. Let's look at the examples.\n\nTo run distributed inference with Python API and MLX LM, we first initialize the distributed group for communication. Then, define the type of parallelism we want, for example, tensor parallelism. Finally, we shard the model using the sharded_load function. After that, we use the model exactly as we would on a single device — MLX LM handles all distributed communications under the hood.\n\nTo have more control over the model and its sharding, we can use low-level primitives from MLX itself. For example, after defining a simple Linear layer, we can shard it with tensor parallelism using shard_linear function.\n\nYou can even control basic distributed operations like all reduce. In Python, Swift or C++ after initializing the distributed group via JACCL, we perform a collective distributed sum across all Macs for our tensor using corresponding MLX primitives.\n\nAs we pointed out at the beginning of the session, JACCL is available on its own and you can leverage it for any applications requiring distributed communication, even non-ML applications. JACCL can be built without MLX and it provides a C++ API with communication primitives: after initializing a JACCL group, we again perform a collective distributed sum across all Macs for our tensor but via JACCL directly, not MLX.\n\nNow you know both high-level and low-level APIs, for distributed inference and training with MLX and JACCL, and you are ready to build advanced distributed workflows with MLX.\n\nThroughout this session, we looked at the full stack that makes distributed training and inference possible on Apple Silicon — from RDMA over Thunderbolt, all the way up to MLX and MLX LM. We showed you how easy it is to scale from a single device to multiple devices, and the benefits it brings: faster inference, the ability to run trillion-parameter models, and faster fine-tuning; all with minimal changes to your single device code, supporting command line interface, Python, Swift and C++ APIs.\n\nWith distributed cluster, now you can run local AI agents powered entirely by MLX — fast, private, and on the hardware you own. To know more, check out our WWDC 2026 video \"Run local agentic AI on the Mac using MLX\".\n\nTo further dive into advanced distributed features — including custom parallelism strategies and training loops, check out our documentation. You can also use MLX LM to serve models distributedly with the built-in server.\n\nWe can't wait to see what you build with MLX on Apple Silicon!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "8:31",
+ "title": "Hostfile format for a 4-node MLX cluster",
+ "language": "swift",
+ "code": "[\n {\n \"ssh\": \"m3-ultra-0\",\n \"ips\": [\"192.168.1.10\"],\n \"rdma\": [null, \"rdma_en5\", \"rdma_en4\", \"rdma_en3\"]\n },\n {\n \"ssh\": \"m3-ultra-1\",\n \"ips\": [\"192.168.1.11\"],\n \"rdma\": [\"rdma_en5\", null, \"rdma_en4\", \"rdma_en3\"]\n },\n {\n \"ssh\": \"m3-ultra-2\",\n \"ips\": [\"192.168.1.12\"],\n \"rdma\": [\"rdma_en5\", \"rdma_en4\", null, \"rdma_en3\"]\n },\n {\n \"ssh\": \"m3-ultra-3\",\n \"ips\": [\"192.168.1.13\"],\n \"rdma\": [\"rdma_en5\", \"rdma_en4\", \"rdma_en3\", null]\n }\n]"
+ },
+ {
+ "timestamp": "8:56",
+ "title": "Generate the cluster hostfile with mlx.distributed_config",
+ "language": "swift",
+ "code": "mlx.distributed_config \\\n --hosts m3-ultra-0,m3-ultra-1,m3-ultra-2,m3-ultra-3 \\\n --output \"m3-ultra-jaccl.json\" \\\n --env MLX_METAL_FAST_SYNCH=1 \\\n --auto-setup \\\n --backend jaccl"
+ },
+ {
+ "timestamp": "11:04",
+ "title": "Run distributed LLM inference with mlx_lm.chat",
+ "language": "swift",
+ "code": "# Single-device LLM inference\nmlx_lm.chat --model \"Qwen/Qwen3.6-27B\" --max-tokens 2048\n\n# Distributed LLM inference across the cluster\nmlx.launch --hostfile \"m3-ultra-jaccl.json\" -- \\\n /remote/path/to/mlx_lm.chat --model \"Qwen/Qwen3.6-27B\" --max-tokens 2048"
+ },
+ {
+ "timestamp": "15:03",
+ "title": "Run distributed inference with pipeline parallelism",
+ "language": "swift",
+ "code": "# Tensor parallelism (default)\nmlx.launch --hostfile \"m3-ultra-jaccl.json\" -- \\\n /remote/path/to/mlx_lm.chat --model \"moonshotai/Kimi-K2.6\" \\\n --max-tokens 2048\n\n# Pipeline parallelism — append --pipeline flag\nmlx.launch --hostfile \"m3-ultra-jaccl.json\" -- \\\n /remote/path/to/mlx_lm.chat --model \"moonshotai/Kimi-K2.6\" \\\n --max-tokens 2048 \\\n --pipeline"
+ },
+ {
+ "timestamp": "17:18",
+ "title": "Run distributed fine-tuning with mlx_lm.lora",
+ "language": "swift",
+ "code": "# Single-device fine-tuning\nmlx_lm.lora --model \"Qwen/Qwen3.5-9B\" \\\n --data \"mlx-community/wikisql\" \\\n --train --batch-size 4\n\n# Distributed fine-tuning (scale --batch-size by number of devices)\nmlx.launch --hostfile \"hostfile.json\" -- \\\n /remote/path/to/mlx_lm.lora --model \"Qwen/Qwen3.5-9B\" \\\n --data \"mlx-community/wikisql\" \\\n --train --batch-size 16"
+ },
+ {
+ "timestamp": "19:01",
+ "title": "Distributed inference with the MLX LM Python API",
+ "language": "swift",
+ "code": "import mlx.core as mx\nfrom mlx_lm import stream_generate\nfrom mlx_lm.utils import sharded_load\n\n# Initialise distributed backend\ngroup = mx.distributed.init(strict=True, backend=\"jaccl\")\n# Define parallelism\ntensor_group, pipeline_group = group, None\n\n# Shard the model\nmodel, tokenizer = sharded_load(\"moonshotai/Kimi-K2.6\", pipeline_group, tensor_group)\nfor response in stream_generate(model, tokenizer, prompt, max_tokens=1024):\n if group.rank() == 0:\n print(response.text, end=\"\", flush=True)"
+ },
+ {
+ "timestamp": "19:31",
+ "title": "Shard a layer with the MLX Python API",
+ "language": "swift",
+ "code": "import mlx.core as mx\nimport mlx.nn as nn\n\n# Initialise distributed backend\ngroup = mx.distributed.init(strict=True, backend=\"jaccl\")\n\n# Define layer and shard it column-wise\nlayer = nn.Linear(1024, 1024)\nsharded_layer = nn.layers.distributed.shard_linear(\n layer, strategy=\"all-to-sharded\", group=group\n)\ndata = mx.random.normal((1, 1, 1024))\noutput = sharded_layer(data)\nmx.eval(output)"
+ },
+ {
+ "timestamp": "19:47",
+ "title": "All-reduce across devices in Python, Swift, and C++",
+ "language": "swift",
+ "code": "# Python\nimport mlx.core as mx\nworld = mx.distributed.init(strict=True, backend=\"jaccl\")\ndata = mx.full((4,), float(world.rank()), dtype=mx.float32)\nresult = mx.distributed.all_sum(data, group=world)\nmx.eval(result)\n\n# Swift\nlet group = try DistributedGroup(strict: .ring)\nlet data = rank == 0\n ? MLXArray(converting: [1.0, 2.0, 3.0])\n : MLXArray(converting: [5.0, 6.0, 7.0])\nlet result = try group.allSum(data)\n\n// C++\nnamespace mx = mlx::core;\nauto world = mx::distributed::init(/* strict */ true, \"jaccl\");\nmx::array data = mx::full({4}, static_cast(world.rank()), mx::float32);\nmx::array result = mx::distributed::all_sum(data, world);\nmx::eval(result);"
+ },
+ {
+ "timestamp": "20:06",
+ "title": "Standalone distributed sum with the JACCL C++ API",
+ "language": "swift",
+ "code": "#include \n#include \n\nint main() {\n // Initialize JACCL group\n auto group = jaccl::init();\n std::cout << \"Rank \" << group->rank() << \" of \" << group->size() << std::endl;\n // Perform all-reduce sum\n float data[10] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f};\n float output[10];\n group->all_sum(data, output, sizeof(data), jaccl::Float32);\n std::cout << \"Result: \" << output[0] << std::endl;\n return 0;\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "MLX Swift LM on GitHub",
+ "url": "https://github.com/ml-explore/mlx-swift-lm"
+ },
+ {
+ "title": "MLX Swift Examples",
+ "url": "https://github.com/ml-explore/mlx-swift-examples"
+ },
+ {
+ "title": "MLX Examples",
+ "url": "https://github.com/ml-explore/mlx-examples"
+ },
+ {
+ "title": "MLX Swift",
+ "url": "https://github.com/ml-explore/mlx-swift"
+ },
+ {
+ "title": "MLX LM - Python API",
+ "url": "https://github.com/ml-explore/mlx-lm"
+ },
+ {
+ "title": "MLX Explore - Python API",
+ "url": "https://github.com/ml-explore/mlx"
+ },
+ {
+ "title": "MLX Framework",
+ "url": "https://mlx-framework.org"
+ },
+ {
+ "title": "MLX",
+ "url": "https://ml-explore.github.io/mlx/"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/233/4/379c319a-5718-4fd2-aac6-2f97180c5892/downloads/wwdc2026-233_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/233/4/379c319a-5718-4fd2-aac6-2f97180c5892/downloads/wwdc2026-233_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "328",
+ "year": "2026",
+ "title": "Explore numerical computing in Swift with MLX",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/328"
+ },
+ {
+ "id": "232",
+ "year": "2026",
+ "title": "Run local agentic AI on the Mac using MLX",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/232"
+ },
+ {
+ "id": "298",
+ "year": "2025",
+ "title": "Explore large language models on Apple silicon with MLX",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/298"
+ },
+ {
+ "id": "315",
+ "year": "2025",
+ "title": "Get started with MLX for Apple silicon",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/315"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:14.385Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-234.json b/data/wwdc/videos/2026-234.json
new file mode 100644
index 0000000..f26cf91
--- /dev/null
+++ b/data/wwdc/videos/2026-234.json
@@ -0,0 +1,25 @@
+{
+ "id": "234",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/234/",
+ "title": "Design immersive environments for visionOS apps and the spatial web",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Design",
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hello, my name is Michael Breymann and I'm a human interface designer at Apple, working on system environments for visionOS.\n\nIn visionOS, system environments are more than simple backdrops. They are photo-realistic natural landscapes, designed for spatial computing, that transport you to a different time and place. Here in Haleakala, you get a stunning panoramic view above the clouds.\n\nBut unlike flat media or a panoramic image, which does not respond to changes in perspective, this environment offers true depth and parallax, giving someone the same experience they would see in the real world. When you combine this with motion and audio, the scene feels alive, and it immerses the viewer in the space.\n\nIn this talk, I'll take you through each part of the design process, for making your own immersive environments, pre-production, production, and post-production.\n\nI'll share the principles and concepts at each stage that you'll need to create a compelling immersive experience, with examples from six of the visionOS system environments: Mount Hood, the Moon, Jupiter, Yosemite, Thorsmork, and Bora Bora.\n\nIt's time to get started with pre-production. When our team kicks off a new environment, we first spend time identifying some questions about our intent. Why are we building this particular environment? What are the qualities we want to bring to life? And how is the viewer going to use this space? These are all important questions to ask yourself about your project.\n\nFor example, when our team started thinking about use cases for system environments, watching media in large format, was especially exciting to us.\n\nWe wanted the media experience to feel cinematic, so we sculpted the terrains to fit a large screen, and researched the optimal center line for viewing.\n\nThe Keynote environment was built for a different purpose: to give people a space to practice public speaking, as though they are live on stage in the real world. Here, sound is intentionally absent, to keep them focused, and the lighting is concentrated on the stage, just as it would be if they were presenting in front of an audience. You might want your environment to be a backdrop for your app's content, or to showcase an experience from the web. Whatever you're designing, this ideation phase is such a key part of building environments, and answering these questions early on, helps you save time and avoid costly mistakes later in production. If you're designing an environment that exists in the real world, like Mount Hood, it's incredibly helpful to scout your location in advance. Consider where you want to place the viewer in the scene for their primary viewpoint, as well as what will be visible to them if they decide to turn around.\n\nIn visionOS, viewers will see approximately 81° of the scene in their field of view, when fully immersed.\n\nScouting is also helpful, for knowing what features you won't want to showcase in your environment. The panorama in the top view shows our Mount Hood location.\n\nNotice the road, and the dense vegetation as the camera pans around to the back side. Our team flagged these for replacement when we got into production.\n\nThe panorama in the bottom view shows the result after our team made these changes.\n\nOf course, sometimes the environment you want to create, won't be easily accessible in the real world. When we worked on the Moon environment, for example, we relied on photography from the Apollo missions to help us visualize our scene.\n\nReference materials, like photography and videography, are essential if you're building an environment like this.\n\nYou can confirm any assumptions you already had, and reconcile any discrepancies. You can also use reference material, to help you better understand a location here on Earth.\n\nFor example, our team created a lighting study using Digital Elevation Models of the Yosemite valley, and the Earth's orbit, to determine the exact days and times we wanted to capture.\n\nWhether you're building your environment from scratch or a real location, here are a few things to keep in mind, when planning for the next stages of production. Try to visualize your environment in terms of layers, building from the background to the foreground. Identify which scene elements will need to have motion, and start thinking about what spatial audio you can attach to them. This will help you get ahead of any complications, that might disrupt your asset creation. One last thing! Often, during pre-production, you will discover things that will change your plan for the better.\n\nFor the Jupiter environment, for example, we built a scale model of the solar system so that we could understand how the Sun would light Jupiter's moons.\n\nDuring this process, we realized that we wanted a system that allows for the passage of time, and this became a key design requirement for the final environment.\n\nTo recap, when you begin pre-production for your environment, make sure you're clear on your intent.\n\nScout your scene by visiting real-world locations and collecting reference material. Consider the composition and layering of your environment, and what you might want to remove or change. And use all the resources you get from pre-production, to refine your ideas through iteration. Because having a solid plan for how you will approach the build of your environment, prepares you for the next phase of development: production.\n\nIt takes time to build complex 3D scenes. But you can make it easier on yourself, and anyone you're working with, by capturing high-quality photography at the source. The Yosemite environment is a great example. During our team's advance scouting, we identified the exact viewpoint we wanted. Here, the viewer is in Yosemite Valley, with a clear shot into the mid-distance. This sort of framing is ideal for environments. You can always add detail into the foreground with CG elements, but it's much harder to remove things that obstruct the view.\n\nWhen we were ready to enter production, we returned to this spot in the valley with our equipment and a very detailed shoot schedule. Seasons, weather, and even the time of day can dramatically change what you're able to capture, and that was certainly true for the Yosemite environment. Lighting can change quickly, especially around sunrise and sunset.\n\nPlan to spend more time in your location, and shoot more photography, than you think you will need.\n\nEvery image you take, can be used to create your environment's 3D assets later on, so it's important to capture source imagery that is directly usable. Here are a few tips: use a tripod with your camera leveled 1 meter off the ground, and ensure a deep depth of field.\n\nIf possible, set up another camera 2 meters off the ground to trigger at the same time. This alternate view is especially helpful during Post-Production, to fill in parts of the scene that are not visible from the primary view. And remember, you are creating a 360° environment, so you will need to use a rig and lens that covers all views, and ultimately produces a stitched panorama.\n\nThe dynamic range of an environment can be quite large, from the Sun to the shadows, so shoot bracketed exposures to ensure you capture all the details. Know your target output display, and capture more resolution than you think you will need. For visionOS, an environment will be sharp at 40 pixels per degree, so 360° panorama of 14,400 by 7,200 pixels is an ideal target.\n\nIn addition to your primary photography, it's also a good idea to capture secondary photography on location for reference and measurements.\n\nThe point clouds produced by photogrammetry and LiDAR, can be meshed and used as starting points for 3D assets, but the data is also useful for distance measurements. You might discover, for example, that your seemingly flat terrain actually has a slope. And with that knowledge, you might make different choices about how you construct your 3D assets. Lighting reference is especially helpful when making CG assets integrate seamlessly into your environment. Macbeth charts, as well as chrome and gray spheres, should be shot at the same time as your primary photography.\n\nVideo of scene elements in motion will also provide invaluable reference when creating believable shader effects. Take note of what sounds, if any, are associated with motion, so that you can source the appropriate audio.\n\nAnd if it's not possible to capture a panorama through photography, you will need to create a rendered panorama, using your favorite digital content creation tool.\n\nThe advantage of creating a rendered panorama, is that you have ultimate control over the entire scene at any given time. Production helps set you, and your environment, up for success. Careful preparation, will help you capture the best photography for the next stage of the process.\n\nHigh-quality imagery gives you, or your technical artists, valuable data to work with.\n\nSecondary photography and reference material can provide you with extra information about the scene to make your 3D work easier.\n\nLastly, this is not a one-size-fits-all process. If you're a small team with limited resources, you can adapt your production with these best practices in mind.\n\nAfter the production phase, you should have enough material to create a high-resolution source panorama. With this and your reference content, you're now ready to begin designing your 3D asset.\n\nThe first step, however? Cleanup.\n\nLet's take a look at the principal photography from the Thorsmork environment, and how our team approached cleaning up this panorama.\n\nThere are a couple of obvious things here that you wouldn't want in the final environment.\n\nLike the camera rig.\n\nAs well as footprints and people, which would detract from the landscape.\n\nBut the cleanup process is also where those notes taken in pre-production come in handy. Let's take a look at a few scene elements, that are interfering with our ideal composition. Like here, the vegetation in the mid ground is too busy.\n\nAnd these bushes, are too dominant in frame.\n\nOnce we identified the key elements we wanted removed, our team went to work and refined the imagery through iterations of digital matte painting and rendering CG assets. It's very important to pay attention to color balance and lighting consistency, throughout this process.\n\nPost-production involves making many creative decisions about the look of your environment, but there's another critical aspect of this stage. Maintaining visual fidelity as you prepare the final asset. Because a 3D mesh is needed to achieve a sense of parallax and depth, you'll move your refined panorama, to textures in UV space. The parts of the scene that are not visible in the panorama need to be filled in. This is where secondary photography and CG renders supplement the primary view. During this process, it's helpful to always do an A/B comparison between your 3D asset and your panorama, to make sure your texture transfer is retaining the highest quality. Make sure that all scene elements have consistent sharpness, especially when compared to those nearby, so that they convey the proper scale.\n\nTry flopping your scene to look at it in a new way. At this point in post-production, your eyes may be too accustomed to seeing the same thing, so you can use this trick to make sure your composition is still working. You can also subject your environment to extreme gamma and gain values. This technique can reveal any color and values inconsistencies, along with any data loss during texture transfer, which may become more obvious when viewed on different displays. Many aspects of post-production involve creative choices, but there's also a lot of technical artistry that supports this phase.\n\nTo learn more about what is required to render your environments in real-time, check out \"Optimize your custom environments for visionOS\".\n\nNow that you have a textured mesh, the visual base of your environment is complete, but you'll need a couple more elements to complete the sense of immersion. Sound and motion.\n\nSound can be incredibly impactful for creating immersion, and you can place spatial audio sources to help amplify that effect. For example, in this environment, an emitter with a rippling water sound attached, is located in a part of the river where the water flows around large rocks.\n\nThe design and implementation of spatial audio, are big topics in themselves. So check out these two excellent talks, to learn more about how sounds can enhance the sense of immersion, in your environments. And speaking of the river, it also needs to move! For any aspect of your environment that needs motion, custom shaders can help you create the best visuals, while still being power efficient. Now, Thorsmork is a subarctic landscape, and there aren't a lot of motion features to design here. So let's travel south, to Bora Bora in French Polynesia, an environment rich with movement. Landscapes like Bora Bora have lots of complex motion and light interactions. I'll speed up the passage of time, so you can see them more clearly. The clouds, which at first seem static, are actually evolving, casting shadows throughout the environment. The palm trees sway in the wind, and the waves break a little differently each time as they hit the shore. All of these details add realism to the environment. But motion and light can be some of the most expensive things to render. For a scene like Bora Bora, our team had to think creatively about how we could achieve the visual intent, without breaking our real-time rendering budget. I'm going to share a few of the techniques we used, and explain how you can take advantage of them in your environment.\n\nLet's start with the sky, a huge part of the scene.\n\nWe wanted it to feel like the clouds are continuously evolving with a sense of direction. UV flow maps made this possible, at little cost. And by weighting the flow speed in different parts of the sky, it feels massive, with depth and scale.\n\nCloud shadows can be achieved not through rendered lights, as you might expect, but rather a scrolling mask that darkens the terrain textures. The direction and speed of the clouds above are matched, so that they feel connected, even though they're not.\n\nLet's go back to real-time speed, and examine another part of the environment.\n\nHere under the palm trees, the more dense, clearly defined shadows, are pre-rendered into flip book textures that darken the terrain textures.\n\nIn this way, you can trade the expense of rendering soft shadows, for a believable approximation.\n\nCounterintuitively, it's often the case that less is more. Offloading complex shading work to precomputed data textures, allows you to produce motion that would otherwise be too expensive to render real-time. These palm fronds, for example, have their mesh complexity greatly reduced. And the wind motion comes from a UV flow map, to produce the final effect. Notice also how the trunks and fronds sway more gently than the leaflets. This is achieved through hierarchical vertex animation and layered sine waves. By stacking low frequency and high frequency motion, you can create variety that is never repetitive. In fact, lots of expensive rendering can be reduced through artful layering of textures and compositing operations. Water, for example, can have its hue saturation and brightness modulated over time, to simulate subsurface scattering of sunlight.\n\nThe waves can move through layers of normal maps and scrolling textures. The sum of these effects, is greater than its parts.\n\nMaking all of these effects work together and feel like they exist in the same world, requires artistry and creative direction. To make sure you're developing the right features and controls for your shaders, remember your visual reference can be a great resource to articulate what you need to build. So let's recap. In post-production, you take all your planning, reference, and photography, and bring your environment to life.\n\nYou'll clean up your panorama, refine your scene in 3D and make sure your elements feel right, and add supporting details like sound and motion to complete the sense of immersion.\n\nDesigning for immersive environments is really about producing convincing results and presenting an illusion that feels authentic. You're in control of this, so at each stage of production, you will want to check your creative decisions for their merits.\n\nTo close, keep these things in mind as you start designing your environment. Be intentional with everything you do. You should be able to explain the reasoning behind everything you add or remove.\n\nCreate a composition that works. Every scene element should feel like it belongs, wherever it lives.\n\nAnd all aspects of the scene should be tied together with sound and motion.\n\nGood design makes your craft and execution stronger, and applies whether you're an individual, a small team, or a large studio.\n\nI'll leave you with a final thought.\n\nOften the best way to know your designs are working, is to try to break them. Don't get too attached to any one idea. Do something unexpected, and be receptive to the outcome. Some of your best work will be the result of the surprises that you embrace along the way.",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/234/4/88f2dbdd-e1b1-4b50-9fa0-69a32ac768b2/downloads/wwdc2026-234_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/234/4/88f2dbdd-e1b1-4b50-9fa0-69a32ac768b2/downloads/wwdc2026-234_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:14.467Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-237.json b/data/wwdc/videos/2026-237.json
new file mode 100644
index 0000000..13599da
--- /dev/null
+++ b/data/wwdc/videos/2026-237.json
@@ -0,0 +1,84 @@
+{
+ "id": "237",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/237/",
+ "title": "What’s new in image understanding",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Megan Williams, from the Vision framework team.\n\nThis year there are some powerful advancements in image understanding, which you can now use to create incredible experiences in your apps. I'll talk about a few of them, starting with what's new - Oh, that's weird, my agenda is missing. Let me see if I can create one quickly.\n\nI made notes of the topics I want to cover. I bet AI can help me make an agenda. I'll use this picture of my notes. And ask a large language model to generate an agenda. This is pretty easy to do with the Foundation Models framework. Thankfully, this year Foundation Models is supporting image inputs. Great! The model made me an agenda. Now I can get back to the presentation. This year, there's more ways than ever to bring image understanding to your apps. Starting with what's new in Vision, the tap-to-segment API allows you to isolate any object in an image just by tapping on it. There's also a new and powerful way to analyze images, using large language models. I'll talk about how to do this with the Foundation Models framework. Then I'll show how you can create image-based tools for LLMs that unlock even more possibilities in image understanding. Finally, Vision is also now available on watchOS. I'll show how to use Vision to enhance your watch apps. But first, I'll show you some of the awesome things you can do with the tap-to-segment API.\n\nVision already has several image segmentation capabilities. For example, there's person segmentation, which will isolate all of the people in an image. But, what if I want to segment something else in the image? Like this flower vase? Now with Vision's tap-to-segment API, I can choose any object in the image to segment. Like this board game, a piece of clothing, or even the floor. There are multiple ways that I can select an object to segment. I'll demonstrate a few of them.\n\nIn my app, I have a photo from a cafe and I want to segment the coffee cup on the table. First, I can choose a point on the cup. And now the cup is isolated. This works well for simple objects, but if my subject is complex, then selecting just one point may not be enough. For example, maybe I want to include the plate as well. I can instead draw a bounding box around all of the objects I want to segment.\n\nAnd now I'm able to get both the cup and the plate.\n\nI can also draw a lasso around an object. I'll use a lasso to segment this croissant.\n\nAnother great option is to draw a scribble.\n\nI can scribble over multiple objects to easily segment all of them at once.\n\nOnce I have a mask, I can refine the mask by adding or subtracting more points.\n\nI've already segmented this cup with an initial point, and now I want to include the plate. I'll just tap the plate, and now it's included too.\n\nI can also remove sections from my mask. Maybe I only want the coffee, not the cup. I can choose a point on the cup to exclude from my mask, and now I get just the coffee.\n\nTo use the API, you'll start with an image. You can use an ImageRequestHandler to hold your image. In Vision, images are processed using requests. To segment an object, you'll use GenerateIterativeSegmentationRequest.\n\nUse the ImageRequestHandler to perform the request.\n\nThis generates a mask of the segmented object.\n\nThe mask is a PixelBuffer that shows which pixels belong to the segmented object. Here's the code. I'll start with an image and create an ImageRequestHandler. Now I'll create the request, using a point inside the object I want to segment as a starting seed. Then I'll use the ImageRequestHandler to perform the request on the image. This produces a segmentation mask of the object.\n\nI can now refine this mask with a new point if I want.\n\nTo do this, I'll just include the point in the request. And then I'll perform the request again. There are a few other things to keep in mind.\n\nVision uses a normalized coordinates system with the coordinate origin in the lower left hand corner. Points should be normalized to the image width and height, with coordinate values between 0 and 1.\n\nIt's also important when you're drawing a lasso, that your stroke width is wide enough. Thin strokes may not produce the best result. The line width should be at least 1% of the total image width.\n\nLastly, I want to mention that before you perform a segmentation request for the first time on a device, you'll have to download the model.\n\nYou can use the downloadAssets API to begin a download.\n\nAnd if you're not sure whether the model is downloaded or not, you can check assetStatus to see if the model is ready to use.\n\nWith tap-to-segment, you can now interactively segment any part of an image you'd like.\n\nNow, I want to talk about a new and exciting way to analyze images using the Foundation Models framework. I showed an example earlier of using a large language model to put together an agenda, using a photo of my sticky notes. But large language models can do a lot more. I can also ask a model to help generate captions for the images in my app. Models tend to do well with descriptive tasks.\n\nIt can even help me with my interior decorating, and provide some helpful suggestions for my living room.\n\nAnd my personal favorite, I can use a large language model to create a recipe from a picture of my fridge.\n\nThe possibilities are endless. And the API to do this is really simple.\n\nHere's the code to generate a caption for an image.\n\nI'm using the prompt builder syntax from Foundation Models. I have a text prompt with instructions for the model about how to process the image. Then I'll include the image as an attachment in my prompt.\n\nNow I can ask the model to respond to the prompt, and it will generate a caption for me.\n\nNow you can try this with your own prompts. There are now multiple ways to analyze images, each with their own benefits. The Foundation Models framework leverages large language models, which can do almost anything you ask them. By comparison, traditional image processing frameworks, like Vision, use a fixed set of computer vision APIs. Vision APIs are fined tuned for specific tasks, which they do really well. And Vision is fast. Often fast enough to analyze video frames in real time.\n\nBut you don't always have to choose between Vision and Foundation Models to analyze your images. There's a way to leverage Vision's expertise with Foundation Model's versatility using tool calling. I'll demonstrate how you can give large language models access to tools that run traditional image processing APIs, like those in Vision, to take image understanding to new levels. But first, I'll give a quick refresher on tool calling.\n\nEarlier I showed how you can give a prompt to a model and have it generate a response.\n\nWith tool calling, your model can now invoke a tool to run external code and get back a result. The model can use this result in its response.\n\nFor example, I can have a weather tool, which fetches a weather forecast for a specific day. My prompt is a question about the weather. The model can't answer the question by itself, so it makes a tool call to the weather tool.\n\nWhen a model makes a tool call, it will generate arguments needed by the tool. In this case, the argument will be the date that the model wants to fetch the weather for.\n\nThe tool will then fetch the weather for the requested date and report back to the model.\n\nNow the model can respond to the question about the weather.\n\nFor more information on tool calling, check out \"Deep dive into the Foundation Models framework\".\n\nThis year, tool calling supports image arguments. For example, now you can give a photo of a plant and ask a question about it.\n\nIf the model can't identify the plant by itself, I could create my own plant identification tool and give the model access.\n\nThe model would call the tool on the image to identify the plant. Rather than passing the whole image as an argument, the model would instead pass a reference to the image.\n\nThe tool will analyze the image and return the name of the plant. Now the model can respond with the correct information. Here's the code. My tool conforms to the tool protocol from the Foundation Models framework. Tools must define input arguments.\n\nFor my plant identifier tool, I want the argument to be an ImageReference. This signals to the model that the argument needs to be a reference to an existing image from the current chat session.\n\nTools also need to define a call method, which is invoked when the model calls the tool.\n\nInside the call method, I can access the imageReference from the tool arguments.\n\nBut now I need to resolve this reference into an actual image. Each imageReference is only valid in the context of the transcript from which it was generated. To access this transcript, use the history session property.\n\nUsing the transcript, the imageReference is resolved back into an imageAttachment.\n\nNow the attachment is converted into a pixelBuffer, so it can be analyzed.\n\nTools can provide a lot of utility for models, particularly in tasks that models don't do well. While you can write own tools, for common tasks, Vision is providing some tools for you.\n\nSome models struggle to read barcodes and QR codes.\n\nI have an event flyer here, and I'm asking the model to extract information like the date, location, and website registration. Without tools enabled, the model can find the location and the date, but it can't read the QR code. Vision provides a barcode reader tool which will help the model.\n\nNow the model can make a tool call to the barcode reader. The tool will analyze the image and return the website from the QR code. Now the model can read all of the information correctly Vision gives you two tools. You've already seen the barcode reader tool for scanning barcodes and QR codes. There's also an OCR tool which is good for helping models read really fine or dense text. It can read text in over 30 languages.\n\nTo use the tools, all you need to do is import Vision, and then configure your language model session with the tools you want to use.\n\nNow the model will be able to call the tool to help answer your prompts. It's also important that you give attached images a label when you want the model to make an image-based tool call.\n\nThis label is how the model will identify which image to pass to the tool.\n\nYou can also make your own tools using Vision. Vision supports over 30 different types of image analysis. I've mentioned image segmentation, but I'll highlight a few more.\n\nVision can also do facial analysis, pose estimation, detection and image classification, and even trajectory analysis and object tracking. Check out \"Discover Swift enhancements in the Vision framework\" to see the full list.\n\nThis year, Vision is available in more places than ever. You can even use Vision to enhance your watchOS apps.\n\nI have a watch app that displays information about local wildlife I can look at when I'm on a hike. It has a bunch of different animals I might encounter, and I can select an animal to learn more about it.\n\nThe app displays a photo of the animal, but because the watch screen is so small, it's hard to see. Vision can help. I can use Vision's saliency analysis to identify subjects of interest in the photo. Then I can crop the image to feature the main subject more prominently.\n\nHere's the code to generate a crop using Vision. First I'll the create the request. I'm using GenerateObjectnessBasedSaliencyImageRequest. Now I'll perform the request on the image. This produces a saliency observation.\n\nFrom this observation I can access the bounding boxes of the salient objects detected in the image.\n\nI'll take most prominent object, and use this for my crop.\n\nI've updated my app to display only the salient portion of the image.\n\nNow when I select an animal, I can get a zoomed in view.\n\nThat looks much better.\n\nI've covered a lot in this video. Here's a quick recap. Vision's new tap-to-segment API lets you interactively segment objects in an image.\n\nFoundation Models now supports image inputs for large language models, which lets you analyze images in new ways that weren't possible before. And you can create tools that use frameworks like Vision to make your image analysis even better. You can also use Vision to enhance your apps on all platforms, including watchOS.\n\nYou can download the watchOS and tap-to-segment sample apps I showed earlier from the developer website. And don't forget to watch \"Discover Swift enhancements in the Vision framework\" to learn about more Vision APIs. You can also check out \"What's new in the Foundation Models framework\" to learn about the other ways large language models can enhance your apps. Thanks for watching.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "4:15",
+ "title": "Segment images (tap-to-segment)",
+ "language": "swift",
+ "code": "// Generate a segmentation mask of an object with a seed point\nlet handler = ImageRequestHandler(image)\nlet request = GenerateIterativeSegmentationRequest(seed: point)\nlet observation = try await handler.perform(request)\nlet mask = observation?.pixelBuffer\n\n// Refine the mask with a new point\nrequest.addIncludedPoint(newPoint)\nlet refinedObservation = try await handler.perform(request)"
+ },
+ {
+ "timestamp": "6:41",
+ "title": "Generate an image caption with Foundation Models",
+ "language": "swift",
+ "code": "// Generate an image caption with Foundation Models\nimport FoundationModels\n\nlet prompt = Prompt {\n \"Generate a caption for this image\"\n Attachment(image)\n}\nlet response = try await session.respond(to: prompt)\nlet caption = response.content"
+ },
+ {
+ "timestamp": "9:55",
+ "title": "Create an image-based tool",
+ "language": "swift",
+ "code": "// Create an image-based tool\nstruct PlantIdentifierTool: Tool {\n @SessionProperty(\\.history) var history\n\n @Generable\n struct Arguments {\n var image: ImageReference\n }\n\n func call(arguments: Arguments) async throws -> String {\n let imageReference = arguments.image\n let transcript = Transcript(history)\n guard let imageAttachment = imageReference.resolve(in: transcript) else {\n throw AppError.imageNotFound\n }\n let image = try imageAttachment.pixelBuffer()\n return classifyPlant(image)\n }\n}"
+ },
+ {
+ "timestamp": "12:09",
+ "title": "Use Vision tools",
+ "language": "swift",
+ "code": "// Use Vision tools\nimport FoundationModels\nimport Vision\n\nlet session = LanguageModelSession(model: model, tools: [BarcodeReaderTool()])\nlet response = try await session.respond(generating: EventInfo.self) {\n \"Get the date, location, and website from this flyer\"\n Attachment(image)\n .label(\"flyer\")\n}"
+ },
+ {
+ "timestamp": "13:54",
+ "title": "Create a crop that highlights a prominent subject (watchOS / saliency)",
+ "language": "swift",
+ "code": "// Create a crop that highlights a prominent subject\nfunc generateImageCrop(in image: CGImage) async throws -> NormalizedRect? {\n let request = GenerateObjectnessBasedSaliencyImageRequest()\n let observation = try await request.perform(on: image)\n let prominentObjects = observation.salientObjects\n return prominentObjects.first\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Segmenting objects using taps, scribbles or rectangles",
+ "url": "https://developer.apple.com/documentation/Vision/segmenting-objects-using-taps-scribbles-or-rectangles"
+ },
+ {
+ "title": "Implementing saliency-based image cropping in iOS and watchOS",
+ "url": "https://developer.apple.com/documentation/Vision/implementing-saliency-based-image-cropping-in-iOS-and-watchOS"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/237/6/a3bdea1e-5c1d-44bc-8c21-9e1958774bd3/downloads/wwdc2026-237_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/237/6/a3bdea1e-5c1d-44bc-8c21-9e1958774bd3/downloads/wwdc2026-237_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "241",
+ "year": "2026",
+ "title": "What’s new in the Foundation Models framework",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/241"
+ },
+ {
+ "id": "301",
+ "year": "2025",
+ "title": "Deep dive into the Foundation Models framework",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/301"
+ },
+ {
+ "id": "10163",
+ "year": "2024",
+ "title": "Discover Swift enhancements in the Vision framework",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10163"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:14.894Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-240.json b/data/wwdc/videos/2026-240.json
new file mode 100644
index 0000000..cfc88b3
--- /dev/null
+++ b/data/wwdc/videos/2026-240.json
@@ -0,0 +1,109 @@
+{
+ "id": "240",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/240/",
+ "title": "Build intelligent Siri experiences with App Schemas",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Dan Niemeyer, a software engineer on the Swift Intelligence Frameworks team. Today, I'll show you how to bring your app to Siri using new capabilities powered by Apple Intelligence. In the 27 releases, Siri is more capable, more contextual, and more personal. And App Intents are the foundation that make that possible.\n\nHere's what I'll cover today. Starting with what's new with Siri and App Intents. How Siri understands your app's content, and when Siri understands your content, enabling it to take action.\n\nAt the end of the talk, I'll cover use cases for working across apps, such as asking Siri to text my wife her plane ticket, or apply a cinematic filter to this photo. And share some best practices for how to bring your app to Siri by extending the system's understanding of what your app can do.\n\nAlright, there's a lot here, so let's get started.\n\nIn the 27 releases, Siri takes a big step forward, powered by Apple Intelligence, our personal intelligence platform that helps people be more creative and productive in the work they do. As a developer, the way you participate in Apple Intelligence is through the App Intents framework.\n\nApp Intents is the foundation for integrating your Apple Siri and Apple Intelligence, and provides a structured way to describe what your app can do and the content it manages. Everything I talk about today builds on top of App Intents.\n\nIf App Intents is new to you, I highly recommend starting with these videos, which cover the fundamentals in more depth.\n\nThis year, Siri becomes more powerful in three key ways. Siri can now access your app's entities, the real meaningful content inside your app. This means people can ask questions like: \"When and where is my next meeting?\" And Siri can answer directly by understanding what a meeting is in your app, which meeting is relevant, and which properties to return, like time and location.\n\nSiri can take action using your app's intents. For example, someone could say: \"Send my latest report to Mary\". So, people can send emails just by asking. Intents describe the actions your app supports, the parameters they require, and when they're safe to run. Siri handles the language understanding. Your app focuses on the action.\n\nSiri can also understand on-screen context. For example, people can say things like: \"Explain this text\", or: \"Get me reviews for this product\". When you annotate views with entities that describe what's on screen, Siri can then understand what content is meaningful, which entities it represents, and what actions apply. This gives people a more contextual conversational experience that feels natural and incredibly powerful.\n\nSo that's a preview of what's new with Siri, how we can find content, take actions, and understand what's on screen. Now it's time to ground all of that in a real app.\n\nFor the rest of this video, I'll work in a sample app called UnicornChat.\n\nUnicornChat is a messaging app where people can chat with fictional unicorn characters like Bubbles, Flare, and Glow. It has just enough functionality to demonstrate the concepts I'm covering today.\n\nAnd while it's a messaging app, the same ideas apply to apps across many other domains. You can download the sample from the Apple Developer website and follow along.\n\nHaving explored what's new and the app we'll use to demonstrate it, let's talk about how you can bring these capabilities to your app. At the center of all of these App Intents-powered experiences is a single concept - app entities. An AppEntity is a structured representation of the content inside your app. To make this concrete, think about the data your app already works with every day.\n\nIf you have a calendar app, each event is an entity. If you have a mail app, each message is an entity. And if you have a photos app, each photo and each album is an entity.\n\nApp entities describe three important things. What the thing is, how it's identified, and which properties matter, like a title, a date, or some text.\n\nThey're not a new data model. They're a way of describing your existing content so the system can understand it.\n\nNow, modeling an entity is the first step, but on its own, that's not enough for Siri to be able to find it or talk about it. For Siri to understand what an entity is, what category of thing it represents, your entity needs to conform to an AppSchema.\n\nApp schemas give Siri a predefined understanding of common concepts, like messages, contacts, or documents. When your entities conform to a schema, Siri already knows how to reason about them.\n\nInstead of treating your app like a black box, Siri can reason about what the user is talking about. In UnicornChat, the nouns in the app are Contact, Conversation, and Message. All three are modeled as app entities that conform to app schemas.\n\nThat's what allows Siri to understand questions like: \"Show my last message from Flare\", one of our contacts, or \"Open UnicornChat with Glow\", another unicorn friend.\n\nSo once you've modeled your content as app entities, the next question is, how does Siri find the right one? Entity resolution is how Siri resolves what the user says into real app entities inside your app.\n\nFor example, when a user says: \"Open UnicornChat with Glow\", Siri resolves that Glow refers to a specific unicorn, finds the matching contact, and fills in the entity along with the values of its properties, like its name and identifier, so it can be used by the system.\n\nBut people don't always ask for things using exact names. They speak in concepts and descriptions. When someone says something like: \"The best windsurfing in Carmel\", they're not looking for an exact text match. They're expressing meaning. To support that experience, Siri needs more than string matching. It needs semantic search. And that's exactly what IndexedEntity enables.\n\nThe primary way to power entity resolution is by adopting IndexedEntity. When you do this, your app's entities are indexed into the system semantic index. This allows Siri to match based on meaning, not just text, understand relationships between entities, and even answer questions over your content. For example: \"Show the messages with Flare about movies\" - that's not a string match. Siri can find messages that reference movie titles because it's performing a semantic query over UnicornChat's indexed messages.\n\nAnd the way you get that behavior is by conforming your schematized entity to the IndexedEntity protocol.\n\nThe indexingKey tells Spotlight which properties, like the message body, should be searchable.\n\nOnce indexed, Siri can search your content, reason over it, and use it to answer questions, not just retrieve items.\n\nIndexedEntity enables the best Siri experience with semantic matching, fewer follow-up questions, and more natural language understanding.\n\nBut not everything can be indexed. Your dataset might be large, lives on a server, or changes too frequently to index ahead of time. In those cases, you can use EntityStringQuery.\n\nWith a string query, Siri hands you the person's input. Your app is responsible for finding matching entities and returning them.\n\nYou don't get semantic understanding, but you do get full control over how you search for and match your app's entities.\n\nSo to bring this all together, start by modeling your app's content as app entities.\n\nConform each entity to an AppSchema so Siri can understand what kind of thing it is. When your data can be indexed, adopt IndexedEntity. Or use EntityStringQuery when indexing isn't feasible. This unlocks powerful experiences like answering questions and finding content instantly. But entities on their own are just information. Where things really get interesting is when you combine entities with actions.\n\nApp Intents are how your app exposes actions to the system. But it's important to understand that not all actions are treated the same way. So we're going to break this into two parts.\n\nWhen you define an app intent, that action can show up across the system in places like Shortcuts, Spotlight, Widgets, and more. This means people can discover and trigger your app's actions in many places, even without Siri.\n\nYou describe what the action does, you define its parameters, and you implement the behavior. The system takes care of surfacing it, suggesting it, and wiring it into system experiences. This is incredibly powerful and provides great benefits for your app and the people who use it. But you can provide even more value by taking a few extra steps to bring your actions to Siri.\n\nJust like entities use app schemas to be understood, actions use schemas to become executable by Siri. Think of schemas as a specialization of App Intents. They're still App Intents, but shaped in a way that Siri knows how to process.\n\nSchemas define the kinds of actions Siri understands, the structure it expects, and how those actions map to natural language. This is what allows Siri to confidently handle commands like: \"Send a message to Mary\", or: \"Play my focus Playlist\". Individual schemas define individual actions, but apps usually need a complete set. That's why schemas are grouped into AppSchema domains.\n\nEach domain represents a category of tasks, such as mail, photos, messages, and more. When you integrate with a domain, you implement a set of predefined app schemas. You map them to your app's functionality, and Siri immediately knows how to talk about your app in that domain. Think of domains as categories of contracts between your app and Siri. They tell Siri what your app can do, what actions are available, and how the system should respond.\n\nFor more details on how app schemas work, check out my video from WWDC24.\n\nNow that we understand the difference between general App Intents and series-specific app schemas, let's bring this to life by adopting one of these domains in UnicornChat and wiring it up end-to-end.\n\nJust like before, everything starts with entities. In UnicornChat, we already modeled the two things we need to send a message.\n\nContacts which represents the recipient and exposes properties like a name and identifier.\n\nAnd the message, which captures things like the message body and author.\n\nThese are app entities, which tell Siri who the message is going to and what is being sent. Now, let me show you how we connect those entities to an action by adopting an app schema.\n\nIn Xcode, we start by typing the schema name. Xcode already knows about all the available app schemas grouped by domain, and Autocomplete lets us pick exactly the one we want.\n\nSince we're building messaging functionality, we select the sendMessage schema from the messages domain.\n\nInstead of inventing a custom intent structure, we're adopting a schema Siri already understands and mapping it to UnicornChat's existing logic. This tells Siri what the action does, which parameters it expects, like the recipient and the message content, and how to guide the customer if something is missing.\n\nOur job is to map those schema parameters onto UnicornChat's existing messaging flow. First, we process the parameters.\n\nThen we pass them into UnicornChat's interface so the message is actually sent.\n\nFinally, we return the newly sent message back to the system as an app entity.\n\nAnd that's it.\n\nBecause this action is implemented as an app intent, it's available throughout the system. And because it conforms to an app schema from the messages domain, Siri can execute it directly without you having to handle natural language yourself. Let's see it in action.\n\nHere I say: \"Send a message to Glow in UnicornChat, saying 'What movies do you recommend?'\" Siri resolves Glow using our AppEntity query, invokes our intent, and sends it.\n\nAll without opening the app.\n\nThis is the power of app schemas. Once your entities are in place, you connect them to well-defined actions and Siri handles the rest.\n\nSo to recap, App Intents expose actions to the system. App schemas make those actions understandable by Siri. App schema domains like messages package schemas into powerful end-to-end experiences. Once you adopt a domain, Siri can speak your app's language fluently.\n\nSo far we focus on how Siri can understand your content and take actions inside your app. But many real-world requests span multiple apps.\n\nThey start in one app, continue in another, and finish somewhere else. For example: \"Hey Siri, email my wife this reply from Bubbles.\" These experiences combine two capabilities, understanding what people are looking at and moving that content to another app.\n\nLet's break that down into two parts. First, Siri needs to understand that this reply from Bubbles refers to, that's on-screen awareness. Then, Siri needs to pass that content into another app so an action can be performed. That's content transfer.\n\nWe'll look at these one at a time. To enable on-screen awareness, your app connects what's visible on-screen to structured information the system understands. At the core of this is app entities. When views are associated with entities, Siri can resolve references like \"this message\" or \"that conversation\" without people naming them explicitly. There are two APIs for annotating on-screen content, and they serve different purposes. Use UserActivity when there's one primary thing on-screen, like viewing a document or composing a message.\n\nUse View annotations when multiple meaningful items are visible at once, like messages in a conversation or items in a list.\n\nHere's how you can achieve this. Each row in this list represents real data in your app. In UnicornChat, that's a message. Each message row is annotated with its corresponding message entity. This explicitly connects what's on screen to the same entities we already use for intents. It allows customers to say things like: \"Edit this message\", or: \"Forward the last one\". Siri resolves entities directly from the view, enabling powerful in-app experiences. But the real power comes when you combine annotations with content transfer.\n\nContent transfer is what allows other apps to act on your entities. You enable this by exporting your entities using Transferable. This tells the system how to represent your content in a form other apps can understand.\n\nFor example: \"Text my wife this conversation\", or: \"Summarize this message\".\n\nYou do this by adopting Transferable and providing an IntentValueRepresentation. In UnicornChat, we export a ContactEntity as an IntentPerson. Once exported, the system can pass that content into actions from other apps. It enables use cases such as: \"Call this contact\". Your app doesn't need to know what happens next. It just needs to describe its content accurately.\n\nWhen content comes into your app, there are usually two possibilities. Either that content refers to something that already exists, or it represents something entirely new.\n\nYou get to decide which path your app takes. If you're matching existing content, use IntentValueQuery. If you're creating something new, use importing on the transferRepresentation.\n\nUse IntentValueQuery when incoming content should resolve to an existing app entity in your app. Conceptually, this is very similar to entity query but scope to intent parameters instead of standalone entity resolution.\n\nIn this example, UnicornChat receives an IntentPerson from another app and tries to match it into an existing ContactEntity. This is ideal when the content already exists in your app or you want to select from existing data. You're essentially saying: giving this incoming value, which of my entities does it refer to? Use IntentValueRepresentation importing when incoming content should create something new in your app. Instead of resolving to an existing entity, you convert the incoming value into a brand new app entity. In UnicornChat, this allows us to create a new unicorn from an IntentPerson when needed. From the user's perspective, the content just works. But your app stays in control of how that content is stored and managed.\n\nSo when importing content, if the content already exists in your app, resolve it. If it doesn't, import it. Many apps use both, depending on the intent and workflow.\n\nSo to recap, on-screen awareness lets Siri understand what the user sees. Content transfer lets that content move across apps. Together, they unlock powerful multi-step experiences, all built on app entities, app intents, and schemas. Now that we've covered entities, actions, and working across apps, let's talk about best practices. Beyond getting things working, there are a few practices that make Siri work better with your app and more resilient over time. Because while you can adopt individual schemas and APIs independently, great Siri experiences often depend on how these pieces work together. And the good news is you don't have to figure this out alone. The tools are designed to help you along the way. Let me show you what I mean with a demo.\n\nLet's jump back into Xcode and pick up where we left off. In our last demo, we adopted the sendMessage schema in UnicornChat. That works great.\n\nPeople can send messages with Siri. But now, let's try to build.\n\nAnd we get a build error. Xcode is telling us that while we adopted sendMessage, we haven't adopted the related draftMessage schema. This is important because some Siri scenarios require more than one schema to deliver a complete experience. This isn't just a compiler error, it's a design hint. Xcode knows that if your app can send messages with Siri, it also needs a way to draft messages, especially when confirmation is required. So instead of failing silently at runtime, the build system surfaces this early.\n\nIf we click into the error, Xcode offers a fix-it.\n\nXcode generates a sample adoption of the draftMessage schema. This gives us an intent definition, the required parameters and a stub implementation. All wired correctly. From here, we just need to fill in the app specific pieces. First, we connect the intent to our entities.\n\nThen, we inject our dependencies.\n\nNext, we process the input.\n\nAnd finally, we open the message creation view.\n\nSince this action mutates UI state, it needs to run on the main actor.\n\nNow, when we build again, the build succeeds.\n\nWe've completed the schema adoption, and Siri now has everything it needs to guide people through the full messaging flow.\n\nThe important thing to remember is, if a Siri experience depends on multiple schemas, Xcode will tell you, show you what's missing, and help you generate the right remaining steps. This makes it much easier to build complete high-quality integrations.\n\nOnce you've adopted your schemas, testing becomes critical. And the best place to start is AppIntentsTesting. This is a new testing framework that lets you exercise your intents entirely in isolation. No Siri involved. You can invoke your intent, pass in parameters, and validate the result, just like any other integration test. This is the fastest and most reliable way to validate your business logic early in development.\n\nCheck out the video \"Validate your App Intents adoption with AppIntentsTesting\" to learn more.\n\nOnce your logic is solid, the next step is the Shortcuts app. Shortcuts gives you a structured UI for your intents. Letting you inspect parameters, tweak inputs, and understand how your action is presented to people. This is where you validate the shape of your intent. Not just what it does, but how it's configured and exposed.\n\nNext, move to Spotlight. Spotlight is where you validate your content integration, ensuring your entities are indexed correctly, discoverable, and linkable. This helps you confirm that Siri can find the right data before it ever tries to act on it.\n\nFinally, test a complete experience with Siri. This is where everything comes together. Natural language, entity resolution, on-screen context, and cross-app workflows. Testing end-to-end ensures that everything works together the way your customers expect.\n\nAt this point, you've seen how to model your content with app entities. Expose actions using App Intents and app schemas. Enable cross-app workflows. And use tooling to build and test confidently. So let's wrap up with a few concrete next steps.\n\nHere's how to get started. Model and index your entities to Spotlight so Siri can find your content. Adopt app schema domains that match your app's core experiences. Adopt Transferable to enable content import and export. And test early and often using AppIntentsTesting, then Shortcuts, Spotlight, and Siri. All of these APIs are available today, and they're designed to scale with your app as Siri continues to evolve.\n\nSiri is becoming more powerful, more contextual, and more capable. And App Intents are the foundation that make that possible. By bringing your app to Siri, you're not just adding voice support. You're making your app faster, more accessible, and easier to use across the system. Thanks for watching, and we can't wait to see what you build.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "7:59",
+ "title": "Contributing message content to Apple Intelligence",
+ "language": "swift",
+ "code": "// Contributing message content to Apple Intelligence\n \n @AppEntity(schema: .messages.message)\n struct MessageEntity: IndexedEntity {\n\n // The text content of the message\n @Property(indexingKey: \\.textContent)\n var body: AttributedString?\n }"
+ },
+ {
+ "timestamp": "8:36",
+ "title": "An interface that locates entities using arbitrary string input",
+ "language": "swift",
+ "code": "// An interface that locates entities using arbitrary string input\n\n struct ContactQuery: EntityStringQuery {\n func entities(matching string: String) async throws -> [ContactEntity] {\n let predicate = #Predicate { person in\n person.name.localizedStandardContains(string)\n }\n let descriptor = FetchDescriptor(predicate: predicate)\n let matches = try modelContext.fetch(descriptor)\n return matches.map(\\.entity)\n }\n }"
+ },
+ {
+ "timestamp": "17:19",
+ "title": "Working across apps - View annotations",
+ "language": "swift",
+ "code": "// Working across apps - View annotations\n \n List {\n ForEach(messages) { message in\n MessageRow(message: message)\n .appEntityIdentifier(\n EntityIdentifier(\n for: MessageEntity.self,\n identifier: message.id\n )\n )\n }\n }"
+ },
+ {
+ "timestamp": "18:18",
+ "title": "Working across apps - Exporting content to another app",
+ "language": "swift",
+ "code": "// Working across apps - Exporting content to another app\n \n extension ContactEntity: Transferable {\n\n static var transferRepresentation: some TransferRepresentation {\n IntentValueRepresentation(\n exporting: \\.person\n )\n }\n }"
+ },
+ {
+ "timestamp": "19:21",
+ "title": "Working across apps - IntentValueQuery",
+ "language": "swift",
+ "code": "// Working across apps - IntentValueQuery\n\n struct ContactEntityQuery: IntentValueQuery {\n\n func values(for input: [IntentPerson]) async throws -> [ContactEntity] {\n let names = input.map(\\.displayName)\n let descriptor = FetchDescriptor()\n let contacts = try model.mainContext.fetch(descriptor)\n let matches = contacts.filter { contact in\n names.contains(where: { name in\n contact.name.localizedStandardContains(name)\n })\n }\n return matches.map(\\.entity)\n }\n }"
+ },
+ {
+ "timestamp": "20:00",
+ "title": "Working across apps - IntentValueRepresentation",
+ "language": "swift",
+ "code": "// Working across apps - IntentValueRepresentation\n\n extension ContactEntity: Transferable {\n\n static var transferRepresentation: some TransferRepresentation {\n IntentValueRepresentation(exporting: \\.person, importing: { intentPerson in \n let contact = Contact(importing: intentPerson)\n ContactManager.shared.contacts.append(contact)\n return contact.entity\n })\n }\n }"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Integrating your messaging app with Apple Intelligence",
+ "url": "https://developer.apple.com/documentation/AppIntents/integrating-your-messaging-app-with-apple-intelligence"
+ },
+ {
+ "title": "Donating your app’s data and actions to the system",
+ "url": "https://developer.apple.com/documentation/AppIntents/donating-your-apps-data-and-actions-to-the-system"
+ },
+ {
+ "title": "Making app entities available in Spotlight",
+ "url": "https://developer.apple.com/documentation/AppIntents/making-app-entities-available-in-spotlight"
+ },
+ {
+ "title": "Making actions and content discoverable by Apple Intelligence",
+ "url": "https://developer.apple.com/documentation/AppIntents/making-actions-and-content-discoverable-by-apple-intelligence"
+ },
+ {
+ "title": "Providing contextual cues to Apple Intelligence and Siri",
+ "url": "https://developer.apple.com/documentation/AppIntents/providing-contextual-cues-to-apple-intelligence-and-siri"
+ },
+ {
+ "title": "Apple Intelligence and Siri AI",
+ "url": "https://developer.apple.com/documentation/AppIntents/apple-intelligence-and-siri-ai"
+ },
+ {
+ "title": "Messages",
+ "url": "https://developer.apple.com/documentation/AppIntents/app-schema-domain-messages"
+ },
+ {
+ "title": "App schema domains",
+ "url": "https://developer.apple.com/documentation/AppIntents/app-schema-domains"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/240/4/d46aac11-3990-42cd-bb33-4ce5e958b902/downloads/wwdc2026-240_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/240/4/d46aac11-3990-42cd-bb33-4ce5e958b902/downloads/wwdc2026-240_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "344",
+ "year": "2026",
+ "title": "Code-along: Make your app available to Siri",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/344"
+ },
+ {
+ "id": "343",
+ "year": "2026",
+ "title": "Explore advanced App Intents features for Siri and Apple Intelligence",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/343"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:14.707Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-241.json b/data/wwdc/videos/2026-241.json
new file mode 100644
index 0000000..a9e96d1
--- /dev/null
+++ b/data/wwdc/videos/2026-241.json
@@ -0,0 +1,84 @@
+{
+ "id": "241",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/241/",
+ "title": "What’s new in the Foundation Models framework",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hello everyone, and welcome! My name is Erik! And my name is Zhen! Last year we introduced the Foundation Models framework with features like guided generation, snapshot streaming, and the powerful tool protocol. We were blown away by your excitement for the Foundation Models framework in year one, and we think you're going to love what we've lined up for you this year, even more! We're going to walk through everything that's new in the framework this year. And let me tell you, this release is packed! Our 2027 release is all about integrations into and beyond the OS, a wider variety of models, and new primitives for building agentic experiences. Let's kick it off with one of our most exciting updates. The Foundation Models framework, including many of the brand new APIs that we're announcing today, is going open source! And doing it in style! In addition to the core framework, we're also releasing a new package, Foundation Models framework utilities, that will be updated between OS releases to give you access to emerging and experimental building blocks. And during the course of this session, you'll be hearing about multiple other packages all joining the Foundation Models framework ecosystem. So, are you ready to jump in with us? I'm going to cover everything related to the new models, new modalities, and new tools we're adding to the framework. And I'm going to show you the brand new APIs we've made to help you get the most out of them! We've got a full schedule! First up, we've got a whole pile of model updates for you, including updates to the on-device model and access to server models.\n\nWe've also added new system tools that supercharge your session with help from Spotlight and the Vision framework. Afterwards, Zhen will rejoin us to tell you about a powerful new API called dynamic profiles, our new primitive for creating agentic experiences. Zhen will also dig into the brand new Evaluations framework and its tight integration with Foundation Models.\n\nAnd to wrap up, we have some exciting news to share about Mac-specific productivity tools, so make sure to stick around! Let's jump right in with our new model!\n\nThis release comes with a new on-device model, rebuilt from the ground up, and better across the board. It's more intelligent; better at logic and tool calling. In iOS 26.4, we released new APIs for inspecting the model's context size and counting the tokens in instructions, prompts, and transcripts. You'll want to use these going forward to adapt your app to the hardware it's running on. We've also been hard at work on refining our guardrails. You may have noticed adjustments in iOS 26.4 to reduce the number of false positives, and we're continuing to make even more improvements in iOS 27.\n\nIn addition the on-device model is also gaining Vision capabilities, which unlocks entire new categories of applications.\n\nThe API is simple, a natural extension of the existing prompt builders.\n\nHere we've created a session, and we want to ask about the photo of the origami on the right.\n\nSimply insert an image attachment into your prompt, together with text.\n\nNow, the model can answer questions about the image.\n\nImage attachments can be created from a variety of types including; UIImage, NSImage, CGImage Core Image types, CoreVideo Pixel Buffers, and file URLs.\n\nThe model supports images in any size and aspect ratio, so you don't need to crop or pad to any particular shape.\n\nArbitrary image sizes are allowed, but bear in mind that larger images will consume more tokens and incur more latency. Thanks to all these upgrades, the on-device system language model is more capable than ever. But if you need even more horsepower, we're giving you access to a brand new PrivateCloudComputeLanguageModel. The Private Cloud Compute model is the very same one that powers many of the Apple Intelligence features you know and love. It is a much bigger model than the on-device models, and has a 32,000 token context window, and it comes with a powerful new capability, reasoning. Reasoning models are trained to spend time carefully thinking through their answers before providing a response, which results in significantly better outcomes. Using Private Cloud Compute couldn't be easier. Just create an instance of the model and use it to initialize your language model session. When prompting the session, I can now specify a reasoning level on the new contextOptions argument. reasoningLevel controls how much the model is allowed think before responding. Deep reasoning produces better responses in exchange for additional compute.\n\nOne of the best things about Private Cloud Compute is that you don't have to worry about account setup, you don't have to deal with authentication, and you don't have to store API keys, it's all completely seamless! And of course, Private Cloud Compute is above all else, private. No prompts are ever stored, and we make it possible for independent researchers to verify these claims. And to top it all off, Private Cloud Compute makes it possible for us to bring the Foundation Models framework to watchOS. Starting in watchOS 27, you can wear your most powerful intelligence features right on your wrist.\n\nPCC is available with no cloud API costs to developers who have less than 2 million first time downloads. Your users will have access to PCC every day and if they are subscribed to iCloud+, their limit will be even higher! To learn more about the ins and outs of PrivateCloudComputeLanguageModel, including the entitlement you'll need to use it, make sure to tune in to our video about building with Private Cloud Compute.\n\nIn addition to an overhauled on-device model and the new Private Cloud Compute model, we're opening up our model abstraction layer to make it possible for nearly any language model to be used with the Foundation Model's framework. The abstraction layer is built around a new LanguageModel protocol that allows both local and servers models to back a LanguageModelSession. Existing models like SystemLanguageModel and PrivateCloudComputeLanguageModel already conform to this protocol. And, we're open sourcing two additional implementations: CoreAILanguageModel and MLXLanguageModel, for running a myriad of local models on the Apple Neural Engine in your Mac's GPU.\n\nWe're also excited to share that we've been hard at work behind the scenes to ensure you have access to a variety of frontier server models! Anthropic, and Google are both publishing Swift packages to provide you with access to their latest and greatest models! The model abstraction layer makes using third party models simple. I'll just import a language model package using Swift Package Manager, initialize the model that I want to use, and pass it when creating my session.\n\nEverything downstream stays the same.\n\nBear in mind that if you use third party server models, you'll probably have to deal with both authentication and billing. Remember, never store private keys in your app binary. Always fetch access tokens with a secure mechanism like OAuth, and store them securely using KeyChain. As a developer, you'll typically be billed per-token when using 3rd party models, so we've made it easy to keep track of your usage. Sessions and responses now have a usage property that tells you precisely how many tokens were used. You can also check how many of the input tokens were read from cache, and how many of the response tokens were used for reasoning.\n\nIf you'd like to learn more about using LanguageModels, or how you can author your own LanguageModel package, check out \"Bring an LLM provider to the Foundation Models framework\". We have finally made it through all our model updates! Next up is system tools! In this release, we're introducing several built-in tools that supercharge your LanguageModelSessions with system provided functionality. FoundationModels now contains two native tools backed by the Vision framework's powerful capabilities.\n\nThe BarcodeReaderTool allows the model read information from barcodes, and the OCRTool allows the model to extract structured text from images. Both enhance a model's ability to reason about visual information in ways it can't natively. The \"What's new in image understanding\" video has more detail about how to leverage these tools, so queue that one up for more info. Similarly, we're also introducing a search tool powered by Spotlight for implementing fully local Retrieval-Augmented Generation. This has been one of your most most requested features. Retrieval-Augmented Generation, or RAG, is a technique that gives the model access to up-to-date personal or domain knowledge by leveraging a Spotlight index and specially processed queries. If this sounds like just what you've been waiting for, \"LLM search using Core Spotlight\" should be at the top of your watch list. Now that we've looked at all the new models in this release, and the new system tools we're adding to the SDK, I'm going to hand off to Zhen to tell you all about our new APIs for building agentic app experiences. Get excited! Take it away, Zhen! Thanks Erik!\n\nI'm going to introduce you to dynamic profiles, our new primitive for building agentic experiences.\n\nLet's begin by walking through the crafts app and looking at the kinds of experiences dynamic profiles make possible. Inside the app, I can create a journal entry with some origami photos. The app creates a session that starts in craft analysis mode. The instructions tell the model to analyze the images, and record what it finds. It identifies the craft type, colors, and materials, then saves them back to the journal, through a tool call.\n\nNext, the app switches to brainstorming mode using Private Cloud Compute's reasoning capability, it takes everything it just learned and suggests a list of creative origami projects. Pretty cool, right? To implement this feature, I'd start by creating a LanguageModelSession. Then I'll add more sessions, each with its own models, instructions, and tools.\n\nBut what if I want the model to autonomously switch modes? Things start to get hairy.\n\nManaging context and orchestrating an agentic system like this can involve a lot of boilerplates. That's why Foundation Models is introducing a new declarative API, dynamic profiles, so you can focus on what matters in the context, and worry less about imperative controls, all within a single language model session. To create a simple dynamic profile, I can declare a struct, and conform it to the DynamicProfile protocol with a body property that contains a Profile.\n\nLanguage model sessions now can be initialized with a DynamicProfile.\n\nI can specify the instructions and tools, that should be present in the context, at that very moment. This is the simplest form of a DynamicProfile, a data structure made up of instructions, and tools. I want to implement two different modes, one for craft analysis, and one for brainstorm. My app has an observable object that stores a mode variable, so I can switch on it.\n\nIn the different branches, the LanguageModelSession should have different instructions and tools.\n\nI can even give the model a tool, to intelligently switch to the context for brainstorm mode.\n\nSometimes it's not enough to just manage the context, you may also want different models and configurations for different tasks, while still maintaining the conversation history.\n\nIn my crafts app, there are 2 scenarios: craft analysis and brainstorm. Each of them already has a different set of instructions and tools.\n\nFor quick tasks like analyzing a craft, SystemLanguageModel is probably enough. Now, if I want to switch to brainstorming, I can also specify Private Cloud Compute, configured with deep reasoning.\n\nTo describe those configurations, I can use modifiers.\n\nA model modifier to specify PCC, And a reasoningLevel modifier to ask the model think thoroughly. Now we have a LanguageModelSession with dynamically configured model, tools, and instructions. When the app needs to handle different context with different model capabilities, dynamic profiles are a great fit. The important thing to understand is that a DynamicProfile resolves to a single active Profile at any given time. You use conditionals to pick which Profile is active, and the framework handles the transition for you. Now let's try it out in the crafts app. As I select the idea, the model switches to Private Cloud Compute.\n\nIt still has the full context from the analysis, but generating creative project ideas benefits from the larger model's capabilities, like better tool calling, and broader world knowledge. Profiles make it so much easier to manage context and dynamically configure sessions. When using this API, consider privacy boundaries, model capabilities, and cost. To explore more, check out the deep dive session: \"Build agentic app experiences with Foundation Models framework\".\n\nAs powerful as these capabilities are, language models are inherently non-deterministic, which makes their behaviors hard to predict.\n\nThe Evaluations framework is a new Swift framework that measures the quality of your intelligence features.\n\nWith the Evaluations framework, you can quantify accuracy as you tweak your prompts.\n\nEvaluations is built to help app developers like you, understand the statistical impact of changes, and deliver your app with confidence.\n\nTo learn more about Evaluations, check out these sessions.\n\nNow, I want to shift gears, and talk about our tooling and open source efforts.\n\nIn macOS 27, the models are coming to the command line. The fm CLI is a brand new way to use Apple Foundation Models for everyday productivity.\n\nYou can access the on-device model and PCC from the terminal, just by using the fm command.\n\nfm has a nice helper and it lists all the features it supports.\n\nI've been using fm chat to experiment with models, for my app features. Let me show you. I want to know, what does valley fold mean in the context origami? Easy. Just like that. I can even plug fm into shell scripts to summarize documents, extract information, or generate content.\n\nFor example, I have some pictures with random names like this one, IMG_1234.\n\nLet me just ask fm to generate a file name based on the content inside the image.\n\nLook at that! It just came up with a nice, descriptive name! And if you're a data scientist or researcher working in the Python ecosystem, the FoundationModels SDK for Python has you covered, too.\n\nThe Python SDK gives you direct access to the very same on-device model that powers the Swift Foundation Models framework. You can check model availability, or generate a response with just a few lines of Python. The SDK has the core feature of the Swift framework so you can go from a prompt to a structured response in seconds. To learn more, check out the session: \"Build AI-powered scripts with the fm CLI and Python SDK\". Now that we have looked at productivity on Mac, let's talk about open source.\n\nStarting with Foundation Models framework utilities.\n\nUtilities contains a collection of building blocks to help you explore emerging practices in working with LLMs.\n\nIt provides profile modifiers for transcript management, a skill API for procedural knowledge loading, and a language model that can interface with servers using the Chat Completions standard. These are just the starting points. Tools and trends evolve, and the Foundation Models framework utilities is there to grow with you.\n\nIn addition to the utilities package, the core of the FoundationModels framework will also be open source.\n\nOpen sourcing the Foundation Models framework makes it a great solution for interacting with LLMs everywhere Swift runs, including Linux servers. Together with other model providers like Anthropic and Google, alongside CoreAI and MLX integrations, you'll be able to run any model, anywhere. Welcome back Erik! Are you ready to wrap up? Way to bring it home Zhen! We hope you're as jazzed as we are about all these new capabilities, models, and APIs. We've only just scratched the surface. Yeah, to get the full scoop, make sure to check out other videos, for deep dives on all the topics we've introduced here, from the Evaluations framework to Private Cloud Compute, the enhanced Xcode instrument, and the nitty gritty on dynamic profiles.\n\nSome great next steps would be to explore our sample app to learn more about dynamic profiles, and to start getting familiar with the Evaluations framework.\n\nOn behalf of the whole team, thanks for joining us! Thank you!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "2:46",
+ "title": "Context size and token counting",
+ "language": "swift",
+ "code": "// Context size and token counting\n \n let model = SystemLanguageModel()\n print(model.contextSize)\n // 8192\n \n let count = try await model.tokenCount(for: \"What are the Japanese characters for origami?\")\n print(count)"
+ },
+ {
+ "timestamp": "3:52",
+ "title": "Attachable image types",
+ "language": "swift",
+ "code": "// Insert c// Attachable image types\n\n let response = try await session.respond {\n \"What animal is this?\"\n Attachment(UIImage(...))\n }ode snippet."
+ },
+ {
+ "timestamp": "8:45",
+ "title": "Inspecting usage",
+ "language": "swift",
+ "code": "// Inspecting usage\n \n let response = try await session.respond(\n to: \"Recommend a craft that doesn't require scissors.\",\n contextOptions: ContextOptions(reasoningLevel: .light)\n )\n\n print(response.usage.input.totalTokenCount)\n print(response.usage.input.cachedTokenCount)\n\n print(response.usage.output.totalTokenCount)\n print(response.usage.output.reasoningTokenCount)"
+ },
+ {
+ "timestamp": "11:55",
+ "title": "Routing between craft analysis and brainstorm",
+ "language": "swift",
+ "code": "// Routing between craft analysis and brainstorm\n \n @Observable\n final class AppStates {\n var mode: Mode\n }\n\n let appStates: AppStates\n var session: LanguageModelSession?\n\n func updateSession() {\n let originalTranscript = session?.transcript.dropFirstInstructions() ?? Transcript()\n\n // Create a new session with new instructions and tools\n switch appStates.mode {\n case .craftAnalysis:\n session = LanguageModelSession(\n tools: [\n RecordImageAnalysisTool(),\n SwitchModeTool(states: appStates)\n ],\n instructions: \"Analyze the user's craft project...\",\n transcript: originalTranscript\n )\n case .brainstorm:\n session = LanguageModelSession(\n tools: [\n RecordBrainstormTool(),\n ],\n instructions: \"Brainstorm some ideas...\",\n transcript: originalTranscript\n )\n }\n }\n \n struct SwitchModeTool: Tool {\n let description = \"Switch to a different mode.\"\n let states: AppStates\n\n @Generable\n struct Arguments {\n let mode: Mode\n }\n\n func call(arguments: Arguments) async throws -> some PromptRepresentable {\n appStates.mode = arguments.mode\n return \"Successfully switched to \\(arguments.mode).\"\n }\n }\n \n // If mode changes, update the session\n withObservationTracking {\n appStates.mode\n } onChange: {\n updateSession()\n }"
+ },
+ {
+ "timestamp": "12:42",
+ "title": "Describing the profile for craft app",
+ "language": "swift",
+ "code": "// Describing the profile for craft app\n\n struct CraftProfile: LanguageModelSession.DynamicProfile {\n var body: some DynamicProfile {\n Profile {\n Instructions {\n \"\"\"\n You are an expert crafting assistant. \\\n Record craft project image analyses \\\n using the recordImageAnalysis tool.\n \"\"\"\n }\n RecordImageAnalysisTool()\n }\n }\n }\n\n let session = LanguageModelSession(\n profile: CraftProfile()\n )"
+ },
+ {
+ "timestamp": "14:36",
+ "title": "Describing the profile for craft app",
+ "language": "swift",
+ "code": "// Describing the profile for craft app\n \n struct CraftProfile: LanguageModelSession.DynamicProfile {\n let states: CraftProjectStates\n\n var body: some DynamicProfile {\n switch states.mode {\n case .craftAnalysis:\n Profile {\n Instructions { /* ... */ }\n RecordImageAnalysisTool()\n SwitchModeTool(states: states)\n }\n case .brainstorm:\n Profile {\n Instructions { /* ... */ }\n BrainstormRecordTool()\n }\n .model(states.privateCloudCompute)\n .reasoningLevel(.deep)\n }\n }\n }"
+ },
+ {
+ "timestamp": "18:29",
+ "title": "Foundation Models SDK for Python",
+ "language": "swift",
+ "code": "# Foundation Models SDK for Python\n \n import apple_fm_sdk as fm\n\n model = fm.SystemLanguageModel()\n\n # Check the model's availability\n is_available, reason = model.is_available()\n\n if is_available:\n\n # Create a session\n session = fm.LanguageModelSession(model=model)\n\n # Generate a response\n response = await session.respond(prompt=\"Hello!\")\n print(response)"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Expanding generation with tool calling",
+ "url": "https://developer.apple.com/documentation/FoundationModels/expanding-generation-with-tool-calling"
+ },
+ {
+ "title": "Analyzing images with multimodal prompting",
+ "url": "https://developer.apple.com/documentation/FoundationModels/analyzing-images-with-multimodal-prompting"
+ },
+ {
+ "title": "Composing dynamic sessions with instructions and profiles",
+ "url": "https://developer.apple.com/documentation/FoundationModels/composing-dynamic-sessions-with-instructions-and-profiles"
+ },
+ {
+ "title": "Adding server-side intelligence with Private Cloud Compute",
+ "url": "https://developer.apple.com/documentation/FoundationModels/adding-server-side-intelligence-with-private-cloud-compute"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/241/6/900558cb-1997-490a-9aac-2461b209e578/downloads/wwdc2026-241_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/241/6/900558cb-1997-490a-9aac-2461b209e578/downloads/wwdc2026-241_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:14.516Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-242.json b/data/wwdc/videos/2026-242.json
new file mode 100644
index 0000000..fac7a8d
--- /dev/null
+++ b/data/wwdc/videos/2026-242.json
@@ -0,0 +1,132 @@
+{
+ "id": "242",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/242/",
+ "title": "Build agentic app experiences with the Foundation Models framework",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hello everyone! And thank you for joining us! My name is Erik. And I'm Oliver. Today, we're going to dig into a new set of APIs that open up whole new possibilities for your apps; Dynamic profiles! But before we dive into the code, we want to lay the groundwork by identifying the problems these APIs solve, and our philosophy behind their design. The first challenge these APIs solve is context management. In long running sessions, dynamic profiles let you trim or summarize the transcript to keep it within the model's context window. The second problem these APIs solve is establishing boundaries. When using multiple models, you should design around capability and cost considerations. Dynamic profiles give you that option. This field is changing week-to-week. The primitives that we're introducing are designed to be flexible, ensuring it's possible to build today's abstractions, and tomorrow's. Exactly! Dynamic profiles enable context engineering, defining model boundaries, and can be scaffolded into just about any architecture. And it's in that spirit today that we're announcing a new package; Foundation Models framework utilities. Utilities is an open source Swift package that houses components helpful for building agentic experiences. It will be updated in between OS releases and give you access to emerging or experimental patterns, all backed by dynamic profiles. So now that we've set the stage, let's jump into our agenda. In the first half of this video, Oliver is going to teach you about the mechanics of dynamic profiles. In the second half, I'll rejoin to cover some advanced topics related to orchestration patterns. Finally, we'll wrap up with a foray into performance and accuracy considerations. So with that, it's over to you Oliver! Thanks Erik.\n\nWith the introduction of the LanguageModel protocol and PrivateCloudComputeLanguageModel, you now have more models than ever to choose from. DynamicProfile is a new API that gives you the ability to switch models within your LanguageModelSession, providing you with the flexibility to select the best configuration for the task at hand.\n\nDynamicProfile is the foundation on which you can build many useful abstractions, such as agents or skills.\n\nToday, I'll give you a tour of the API starting with leveraging multiple models, before diving into transcript considerations and finishing with session lifecycle events. Let's start by looking at an example.\n\nI'm working on a craft app called Origami which can produce both origami and crochet tutorials.\n\nHere, the user will upload images and our app will help them brainstorm ideas using the image as inspiration.\n\nThe user can provide feedback on the shortlist of ideas before a tutorial is generated for the selected concept. While the user works through the tutorial, they can upload in-progress photos and get advice on their technique.\n\nEach stage in the app requires shared context but individually, they have a unique set of priorities. They may benefit from a diverse set of models, with different instructions and generation options.\n\nThese configurations are agents - they act on your app's behalf, and are configured with a particular goal and set of capabilities in mind. DynamicProfile allows you to declare individual Profiles, which represent a configuration state or agent in your LanguageModelSession.\n\nA Profile is made up of instructions, tools, and modifiers for configuring things like the model, temperature, samplingMode and more. So let's start by declaring a DynamicProfile for our craft experience.\n\nHere, we have an Observable class called CraftOrchestrator that will track the different phases of the app. We'll focus on the brainstorming phase first, which is used for presenting different craft project ideas to the user.\n\nHere, our new profile has some instructions explaining its goal and a tool for generating titles.\n\nBecause origami is a complicated craft, let's also include some additional instructions and tools but only when the user is working on an origami project.\n\nOrigamiExpert makes use of another new type called DynamicInstructions. DynamicInstructions enables grouping of relevant tools and instructions together into a single component that can be reused throughout your codebase.\n\nOrigamiExpert contains knowledge and tools that can be reused every time we're prompting a model about origami. DynamicInstructions are also composable so nesting OrigamiExpert inside another DynamicInstructions body will concatenate the instructions and tools together. Here, we've created BrainstormFacilitator to hold our profile's instructions. Now we can clean up our brainstorming profile using the new declaration. Since brainstorming requires both a broad knowledge of crafts and creative thinking, this profile will use PrivateCloudComputeLanguageModel, which is a new model available in Foundation Models.\n\nBe sure to check out the talk from Louis on PCC in Foundation Models to learn more about what this model has to offer. We'll also set the temperature to 1, to allow the model to produce more creative responses.\n\nWe've just defined our first agent using DynamicProfiles.\n\nLet's move on to the next mode in our profile: planning.\n\nThe \"planning\" profile is responsible for creating directions for an agreed upon craft project.\n\nAgain, we'll use PCCLanguageModel since this requires in-depth knowledge of crafts.\n\nWe'll also configure reasoningLevel, which is a capability available to most server models. This controls the model's capacity to think through the problem before responding. Since generating a tutorial is complex, we'll set it to deep.\n\nLastly, the \"reviewing\" phase provides advice and guidance as the user works through the tutorial.\n\nTo save on unnecessary server calls, this makes use of SystemLanguageModel.\n\nAnd just like that, we've finished defining our crafting DynamicProfile.\n\nTo make use of DynamicProfile in your session, it's as simple as using the new LanguageModelSession initializer.\n\nNote that the body of a DynamicProfile is re-evaluated each time the model is prompted, so as the app moves between each mode, the persona of the LanguageModelSession changes.\n\nYou can think of this as swapping hats, or switching agents. You can move from brainstorming to planning, to reviewing. All by changing the mode.\n\nYou've now seen how you can route between different models using DynamicProfile. But it's important to consider that each model may have different context size limits.\n\nOur craft example switches between PCCLanguageModel and SystemLanguageModel. When moving between models, you may need to trim unnecessary entries to stay within the context size. But that's not the only reason for adjusting the model's context. You can also improve the model's focus by removing irrelevant entries, or redact private information from existing entries when moving to a less private model.\n\nThe transcript is LanguageModelSession's representation of the model's context. DynamicInstructions offers one way to modify the transcript. More specifically, it allows modifying the instructions entry. For updating the remaining entries, we'll use a window into the transcript called \"history\".\n\nDropping tool calls is one easy way to trim history. Let's take a look at how you'd implement this.\n\nhistoryTransform can be applied to a profile to transform the history prior to prompting the model.\n\nThis is the opportune time to filter out entries that may not be necessary for the request.\n\nApplying a transformation on our \"reviewing\" profile helps keep the transcript within the on device model's context size. Transforms don't permanently mutate the session's transcript. Instead, they're local transformations applied prior to prompting the model.\n\nThis means you don't need to worry about losing context that may become relevant at a later point.\n\nOur historyTransform has a lot going on. Let me show you how we can use custom modifiers to hide the complexity of our transform.\n\nFirst, we'll declare a new type that conforms to DynamicProfileModifier and apply our historyTransform.\n\nWe can then make it available for reuse by implementing an extension on DynamicProfile. Any new Profiles that would benefit from reducing context can now utilize the new modifier.\n\nWe've made a number of useful modifiers available in the new Foundation Models framework utilities package. We encourage you to take a look. Custom modifiers are a great way to build reusable configuration for your declarations.\n\nBut transforms aren't the only way that you can influence the transcript. Let's take a look at another more stateful approach.\n\nAt certain points in the session, you may need to summarize earlier entries from the existing transcript to reclaim context.\n\nDoing this after each model's response provides a clear boundary in the session's lifecycle.\n\nLet's take a look at how we can perform our summarize operation after each response using a new set of modifiers.\n\nLifecycle modifiers provide access to your profile's progress by giving you the opportunity to run imperative code directly in your profile declaration. This can be useful for updating state external to your session, like reflecting progress in UI. But it's also useful for internal state updates, like changing the mode in our craft profile or modifying the session's history.\n\nLet's use the onResponse modifier to mutate the history at the response boundary that I mentioned earlier. You'll notice this is also making use of another new concept: session properties. Session properties allow you to define state that's accessible from any Tool or Profile. The history property that we just used is a built-in property provided by the framework. It captures the session's history and can be used as an alternative to historyTransform for updating the transcript. Keep in mind that the history property is lossy and its changes will be reflected across all profiles in the session. For lossless transformations targeted to specific profiles, you should prefer historyTransform. In addition to history, you can also create your own session properties. Let's create a new property to store our conversation summary when onResponse is called.\n\nYou can declare properties using the @SessionPropertyEntry macro within an extension on SessionPropertyValues. All session properties are mutable and must have an initial value. Here, we've declared our summary as an optional string. Each Profile can now read the value of the summary by accessing the session property that we just declared. We'll include the summary in our profile's instructions to ensure they have the context on the transcript entries that were dropped.\n\nAny profile can write to the property and changes will be visible across the session.\n\nNow let me produce a conversation summary for you.\n\nUse lifecycle modifiers to run code at specific points in the session. Use the history property to update the session's history for all profiles. And use custom session properties for storing state that's shared by all session components. And with that, I'll hand it back to Erik to teach you about agent orchestration. Thanks Oliver! Hopefully, you're starting to develop an intuition for how profiles can be used to build things like agents. Let's take a look at two common patterns for orchestrating agentic experiences.\n\nWe like to refer to these patterns as baton-pass and phone-a-friend.\n\nBaton-pass is a collaboration and phone-a-friend is a consultation. Let's look at baton-pass first.\n\nIn this pattern, there are two or more profiles, typically each leveraging different models.\n\nThere also needs to be a variable that controls which profile is active.\n\nFinally, we give each profile a tool that allows the model to set that variable.\n\nTogether, these pieces make up the baton-pass pattern.\n\nIf we're currently brainstorming and ask how to fold a crane, the brainstorm profile will call a tool to pass the baton to the tutorial profile. A tool output signals a successful handoff, and the tutorial profile produces the final answer.\n\nThe most important attributes of the baton-pass pattern are that the full transcript history is visible to both profiles, and that the profile that receives the baton can carry it across the finish line and provide the final response.\n\nBoth of those attributes will be in contrast to the next pattern we look at: phone-a-friend.\n\nIn the phone-a-friend pattern, you also rely on tool calling. The key difference is that instead of toggling a variable, the tool spawns a short-lived session.\n\nIf we ask for a fun project for kids, the model may reason that it needs a title for the project, and call its phone-a-friend tool to consult with the title profile.\n\nThe phone-a-friend tool spawns a new session with an independent transcript prompts it, and then delivers the response back as tool output.\n\nThe child session disappears, and the parent session produces the final response. The most important attributes of the phone-a-friend pattern are that the transcripts for each profile are isolated, and that the parent profile is always responsible for giving the final answer.\n\nBaton-pass and phone-a-friend are good tools to have in your belt, but there are other options as well.\n\nFor example, the Foundation Models framework utilities package houses a Skills type, which you may be familiar with as a popular pattern for procedural context loading.\n\nSo now that you've got a grasp on the many ways tools can be used for orchestration, we're going to look at a new knob you can use to exert control over when tool calls happen - Tool calling mode.\n\nTool calling mode has three options: allowed, disallowed, and required. The default value is \"allowed\", which is the existing behavior. The model may produce a tool call or it may respond directly. This is the option to use when you just don't know if tools will be necessary or not, which is the most common case.\n\n\"disallowed\" prevents the model from calling tools. This can be helpful if the user navigates into a part of your app where the session's tools are known to be irrelevant.\n\nFinally, \"required\" means that the model can only call tools. And this can be particularly useful in agentic systems that represent all actions as tool calls.\n\nIf you're using profiles, you can specify tool calling mode with a modifier.\n\nIf you're not using a profile, tool calling mode can be set via GenerationOptions when calling respond(to:).\n\nHere's the most important thing to remember. When tool calling is required, the model is essentially in a while loop - it is your job to ensure that there is an exit condition of some kind.\n\nOne good option is to conditionalize the tool call mode on a variable.\n\nHere, we're requiring tool calls until the model calls the database tool.\n\nA second, more forceful option is to equip your model with a final answer tool that throws an error. Throwing an error aborts the tool calling loop and immediately returns control flow to you.\n\nBy default, when you throw an error from a tool, or when you cancel a response, your session's transcript will roll back to its previous state.\n\nFor advanced use cases where you want to allow cancelling part way through a response and then resuming again, you need to keep your transcript in state after an error.\n\nWe've added new API to enable this. If you're using profiles, you can now set \"transcriptErrorHandlingPolicy\" using a modifier. If you're not using a profile, you can set it directly on your session.\n\nThe two options are \".revertTranscript\" and \".preserveTranscript\".\n\nWhen using \".preserveTranscript\", the onus is on you to put your transcript back into a good state if you intend to continue using your session.\n\nTo facilitate that, the \"transcript\" property on session is now mutable. Remember though, you can only modify the transcript when the session's \"isResponding\" property is false. Attempting to mutate the transcript during a response is a programmer error.\n\nNow that we've taken a look at our new APIs, we need to talk about the implications of mutating the transcript on performance and accuracy.\n\nKey-value, or KV caches are an important optimization mechanism in large language models and they can be invalidated by transcript mutations.\n\nGenerally, appending to the transcript preserves the KV cache, and minimizes the time-to-firsttoken.\n\nIf you rewrite history by removing entries, changing the attached tools, or updating the instructions, that will typically trigger a cache invalidation, and can increase latency.\n\nNow, we didn't talk about this last year because we intentionally shaped LanguageModelsSession APIs to be append only. By default, they ensured optimal use.\n\nBut this year, we're taking the training wheels off, so to say.\n\nIt's important to understand that different models have different caching behavior and the only way to be certain is by measuring.\n\nThe best way to do that is the upgraded Foundation Models Instrument in Xcode.\n\nFor more about detecting cache invalidations with Instruments, make sure to check out our video on debugging and profiling.\n\nIn addition to performance implications, the other thing you have to be careful about when rewriting history is accuracy, because it's possible to confuse the model.\n\nLet's say I have a session where I asked the model to think of fun origami project names.\n\nAnd then let's say I add a generate title tool to the session, and prompt it for more ideas. What do you expect will happen next? If we're lucky, the model will use the tool like we want.\n\nBut it's also possible that the model will notice it previously generated titles without the tool, and may think it's supposed to do that again. That's not what we want. Our history modification confused the model.\n\nWhen you start to get into nuanced transcript modifications like this, it becomes even more important to use the Evaluations framework to create eval sets and quantify the effect of context engineering strategies. Data driven optimization is the only way to be confident. I highly recommend watching all of our videos about the evaluations framework.\n\nAlright, that brings us to the end of our section on performance and accuracy.\n\nThat was a lot! Are you ready to bring it home Oliver? You know it. We've shown you how dynamic profiles allow you to steer model behavior and manage your session's transcript. We talked through patterns like phone-a-friend and baton-pass, tool calling mode, manual transcript management, and even KV caches. And we hope you're as enthusiastic about Foundation Models framework utilities as we are!\n\nNext, try playing around with the sample app. Or test out PCC together with the revamped Xcode instrument.\n\nUntil next time, thanks for watching. Thank you!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "5:04",
+ "title": "DynamicInstructions",
+ "language": "swift",
+ "code": "// DynamicInstructions\n \n struct BrainstormFacilitator: DynamicInstructions {\n var orchestrator: CraftOrchestrator\n var body: some DynamicInstructions {\n Instructions {\n \"You are a warm and friendly expert crafting brainstorm facilitator.\"\n }\n // Tools\n GenerateProjectTitle()\n // Conditionally include Origami knowledge\n if orchestrator.techniques.contains(.origami) {\n OrigamiExpert()\n }\n }\n }"
+ },
+ {
+ "timestamp": "6:41",
+ "title": "DynamicProfile",
+ "language": "swift",
+ "code": "// DynamicProfile\n\n struct CraftProfile: LanguageModelSession.DynamicProfile {\n var orchestrator: CraftOrchestrator\n var body: some DynamicProfile {\n switch orchestrator.mode {\n case .brainstorming:\n Profile { BrainstormFacilitator(orchestrator: orchestrator) }\n .model(orchestrator.pccLanguageModel)\n .temperature(1)\n case .planning:\n Profile { TutorialAuthor(orchestrator: orchestrator) }\n .model(orchestrator.pccLanguageModel)\n .reasoningLevel(.deep)\n case .reviewing:\n Profile { CraftCoach() }\n .model(orchestrator.systemLanguageModel)\n }\n }\n }"
+ },
+ {
+ "timestamp": "6:43",
+ "title": "Initialize your session with your dynamic profile",
+ "language": "swift",
+ "code": "// Initialize your session with your dynamic profile\n let session = LanguageModelSession(profile: CraftProfile(orchestrator: orchestrator))"
+ },
+ {
+ "timestamp": "8:33",
+ "title": "Transcript management",
+ "language": "swift",
+ "code": "// Transcript management\n \n struct CraftProfile: LanguageModelSession.DynamicProfile {\n var orchestrator: CraftOrchestrator\n var body: some DynamicProfile {\n switch orchestrator.mode {\n case .reviewing:\n Profile { CraftCoach() }\n .model(orchestrator.systemLanguageModel)\n .historyTransform { history in\n // Update the history for your profile\n guard let latestResponseIndex = lastResponseEntryIndex(history) else {\n return history\n }\n let filteredHistory = history[0.. some DynamicProfile {\n content\n .historyTransform { history in\n guard let latestResponseIndex = lastResponseEntryIndex(history) else {\n return history\n }\n let filteredHistory = history[0.. some DynamicProfile {\n self.modifier(DroppingToolCallsProfileModifier())\n }\n }"
+ },
+ {
+ "timestamp": "9:27",
+ "title": "History management modifiers",
+ "language": "swift",
+ "code": "// History management modifiers\n\n import FoundationModelsUtilities\n\n struct CraftProfile: LanguageModelSession.DynamicProfile {\n var orchestrator: CraftOrchestrator\n var body: some DynamicProfile {\n switch orchestrator.mode {\n case .reviewing:\n Profile { CraftCoach() }\n // Keep the most recent 10 entries\n // after dropping finished tool calls\n .rollingWindow(size: .entries(10))\n .droppingCompletedToolCalls()\n }\n }\n }"
+ },
+ {
+ "timestamp": "10:48",
+ "title": "Lifecycle modifiers",
+ "language": "swift",
+ "code": "// Lifecycle modifiers\n \n struct CraftProfile: LanguageModelSession.DynamicProfile {\n @SessionProperty(\\.history) var history\n var orchestrator: CraftOrchestrator\n var body: some DynamicProfile {\n switch orchestrator.mode {\n case .planning:\n Profile { TutorialAuthor(orchestrator: orchestrator) }\n .model(orchestrator.pccLanguageModel)\n .reasoningLevel(.deep)\n .onResponse {\n // Update history\n if history.count > 50, let responseIndex = lastResponseIndex(history) {\n history = history[responseIndex...]\n }\n }\n }\n }\n }"
+ },
+ {
+ "timestamp": "11:40",
+ "title": "Declare a custom session property",
+ "language": "swift",
+ "code": "// Session properties — declaration\n\n extension SessionPropertyValues {\n @SessionPropertyEntry var summary: String?\n }"
+ },
+ {
+ "timestamp": "12:24",
+ "title": "Read and write session properties in a profile",
+ "language": "swift",
+ "code": "// Session properties\n \n struct CraftProfile: LanguageModelSession.DynamicProfile {\n @SessionProperty(\\.history) var history\n @SessionProperty(\\.summary) var summary\n var orchestrator: CraftOrchestrator\n var body: some DynamicProfile {\n switch orchestrator.mode {\n case .planning:\n Profile {\n TutorialAuthor(orchestrator: orchestrator)\n if let summary {\n Instructions { \"Summary: \\(summary)\" }\n }\n }\n .onResponse {\n if history.count > 50, let responseIndex = lastResponse(history.prefix(40)) {\n summary = try await summarize(history[0..: Tool {\n func call(arguments: GeneratedContent) async throws -> String {\n let session = LanguageModelSession(profile: profile())\n let response = try await session.respond(to: arguments)\n return response.content\n }\n }"
+ },
+ {
+ "timestamp": "15:15",
+ "title": "The skills pattern",
+ "language": "swift",
+ "code": "// The skills pattern\n \n struct CraftingSkills: LanguageModelSession.DynamicInstructions {\n var activations: SkillActivations\n var body: some DynamicInstructions {\n Skills(activations: activations) {\n Skill(\n name: \"origami_folds\",\n description: \"Details about specific types of folds\",\n prompt: \"\"\"\n Valley Fold: Paper is folded toward you, creating a V-shaped crease\n Mountain Fold: Paper is folded away from you, creating an inverted V\n ...\n \"\"\"\n )\n Skill(...)\n Skill(...)\n }\n }\n }"
+ },
+ {
+ "timestamp": "15:31",
+ "title": "Tool calling mode",
+ "language": "swift",
+ "code": "// Tool calling mode\n \n public struct ToolCallingMode: Sendable {\n public static let allowed: ToolCallingMode\n public static let disallowed: ToolCallingMode\n public static let required: ToolCallingMode\n }\n \n // Pass tool calling mode as a profile modifier\n struct OrigamiExpert: LanguageModelSession.DynamicProfile {\n var body: some LanguageModelSession.DynamicProfile {\n Profile {\n Instructions(\"You are an origami expert\")\n QueryOrigamiDatabaseTool()\n ShowDirectionsTool()\n }\n .toolCallingMode(.required)\n }\n }\n\n // Or pass it as a generation option\n let response = try await session.respond(\n to: \"Write out the instructions for folding a paper crane.\",\n options: GenerationOptions(toolCallingMode: .required)\n )"
+ },
+ {
+ "timestamp": "16:47",
+ "title": "Escaping a tool call loop",
+ "language": "swift",
+ "code": "// Escaping a tool call loop\n\n struct OrigamiExpert: LanguageModelSession.DynamicProfile {\n let state: OrigamiAppState\n\n var body: some LanguageModelSession.DynamicProfile {\n Profile {\n Instructions(\"Answer questions about how to fold origami\")\n QueryOrigamiDatabaseTool()\n }\n .toolCallingMode(state.queriedDatabase ? .disallowed : .required)\n .onToolCall { state.queriedDatabase = true }\n }\n }"
+ },
+ {
+ "timestamp": "16:57",
+ "title": "Define a tool that throws an error",
+ "language": "swift",
+ "code": "// Define a tool that throws an error\n var output: String?\n\n @Generable struct Arguments {\n var answer: String\n }\n\n func call(arguments: Arguments) async throws -> Never {\n output = arguments.answer\n throw CancellationError()\n }\n }"
+ },
+ {
+ "timestamp": "17:28",
+ "title": "Set the transcript error handling policy",
+ "language": "swift",
+ "code": "// Specify transcript behavior on a profile\n struct OrigamiExpert: LanguageModelSession.DynamicProfile {\n let state: OrigamiAppState\n\n var body: some LanguageModelSession.DynamicProfile {\n Profile {\n Instructions(\"Answer questions about how to fold origami\")\n QueryOrigamiDatabaseTool()\n }\n .transcriptErrorHandlingPolicy(.preserveTranscript)\n }\n }\n\n // Or specify it on a session\n let session = LanguageModelSession()\n session.transcriptErrorHandlingPolicy = .preserveTranscript\n\n // Policy options\n extension LanguageModelSession {\n public struct TranscriptErrorHandlingPolicy: Sendable {\n // Roll the transcript back to its previous state\n public static let revertTranscript: TranscriptErrorHandlingPolicy\n // Keep the transcript in state following an error\n public static let preserveTranscript: TranscriptErrorHandlingPolicy\n }\n }"
+ },
+ {
+ "timestamp": "17:51",
+ "title": "Transcript mutation",
+ "language": "swift",
+ "code": "// Transcript mutation\n\n public final class LanguageModelSession: Sendable {\n public var transcriptErrorHandlingPolicy: TranscriptErrorHandlingPolicy { get set }\n\n // Transcript is now settable\n public var transcript: Transcript { get set }\n\n // But you must not modify it during a response!\n public var isResponding: Bool { get }\n }"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Composing dynamic sessions with instructions and profiles",
+ "url": "https://developer.apple.com/documentation/FoundationModels/composing-dynamic-sessions-with-instructions-and-profiles"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/242/4/7f05515d-be1a-43a0-9962-a1f77f115666/downloads/wwdc2026-242_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/242/4/7f05515d-be1a-43a0-9962-a1f77f115666/downloads/wwdc2026-242_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:15.123Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-243.json b/data/wwdc/videos/2026-243.json
new file mode 100644
index 0000000..4b5aed2
--- /dev/null
+++ b/data/wwdc/videos/2026-243.json
@@ -0,0 +1,60 @@
+{
+ "id": "243",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/243/",
+ "title": "Debug and profile agentic app experiences with Instruments",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Developer Tools"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hi, I'm Erik an AI Tools Engineer.\n\nIn this session, I'll show you how to use Instruments to debug and develop features built with the Foundation Models framework.\n\nThe Foundation Models APIs give your app direct access to on-device and server-based generative AI. With them, you can build features that understand natural language, generate content, and respond to what the person is doing.\n\nThe features that create the best experiences aren't static. They adapt based on context. That's what the Foundation Models APIs are designed for. DynamicInstructions lets you specify exactly which instructions and tools the model can access. It re-evaluates before every request, so the model always has the right context for the task at hand. That flexibility is what makes these features so responsive, and also what makes them harder to debug. Building with Large Language Models or LLMs is different from traditional development. Traditional code is predictable. LLMs are non-deterministic - the same input can produce different outputs. When a feature loses context or responds too slowly, tracking down the cause isn't straightforward. Good tooling makes the difference. By the end of this session, you'll know how to use Instruments to identify and fix those issues and ship fast, reliable experiences with confidence. First, we'll start by comparing and contrasting traditional versus LLM app development concepts to get us into the right mindset. Then, we'll use Instruments to inspect and debug an agentic experience I'm developing in my Craft app. Before getting started, we recommend you check out \"What's new in the Foundation Models framework\" and \"Build agentic app experiences with the Foundation Models framework\" to gain a better understanding of the latest additions.\n\nBuilding apps with LLMs introduces three challenges you won't find in traditional software development.\n\nThe first is probabilistic output. Give a traditional function the same input twice, and you get the same output. LLMs don't work that way. The same prompt can produce two completely different responses which means standard unit testing breaks down. You can't assert that an output matches a hardcoded string. You have to evaluate the quality and intent of the response instead.\n\nThe second is model-to-model communication. Powerful features often rely on multiple models working together. For example, in a recipe app, one model might identify ingredients in a photo, while a second generates a recipe from that result. Getting data to flow reliably between those models, and recovering gracefully when something goes wrong, is where real complexity lives.\n\nAnd the third is observability. When something breaks in a multi-model pipeline, it can be very hard to know where it went wrong. You need visibility into each step: what the model received, what it decided, and why. That's exactly what this session is about.\n\nAt its core, an LLM application does three things: a person sends a prompt, the model reasons about it, and the person gets a response. Simple, fast, and for many features (a summarization tool, a writing assistant, a Q&A interface), exactly what you need. Many useful features need more than text generation. Sometimes the model needs information it doesn't have: the current time, a database record, or a search result. That's where tool calls come in. The loop works like this: the person sends a prompt, the model reasons about it and calls a tool, that tool performs an action, the model takes the result and generates a final response, which can kick off the loop again. Each extra step adds latency. Each step is a new place for failure. Understanding this loop is the basis for everything the Foundation Models Instrument shows you. Now that I have covered the mindset required for LLM app development, I'll use Instruments to debug and inspect the brainstorming feature I'm developing for my Craft app.\n\nI'm working on a crafting companion app where you can keep a journal of your craft projects.\n\nThe app lets you record craft progress, ask questions about specific crafts, and generate tutorials. Recently, I had an idea for an interactive brainstorming feature that gives people suggestions on what to craft. The crafter can speak with the model to refine its ideas and when they're ready to commit, the app generates a detailed tutorial for that craft.\n\nThis feature uses two sets of instructions: one for brainstorming ideas, and a second for tutorial generation. The brainstorming instructions include two tools: a GenerateCraftIdeaTool and a SwitchToTutorialModeTool. Both sets of instructions use the server model on Private Cloud Compute, one for quick idea generation and the other to generate more detailed tutorials. Let's see this in action with Instruments.\n\nThe project is already open in Xcode. To begin profiling, I'll open the Product menu and select Profile. Xcode will build the app locally. From the template chooser, I'll select the Foundation Models template and click Record. This instrument captures prompt and response data from your device, which can include sensitive information. Logging is off in production but it's on for the duration of your trace so keep your trace files somewhere safe. Select \"Record Anyway\" to get started.\n\nNow that the app has launched, let's give it a try. As soon as we land here, the model suggests a few project ideas: Yarn PomPom, Fabric Pouch, and Paper Butterfly. Paper Butterfly sounds fun - let's go with that.\n\nHm. That's not right. The model was supposed to kick off a tutorial but instead it just offered more ideas. Something's off. Let's end the recording and dig into the trace to find out what happened.\n\nInstruments shows a lot at once, so let's walk through it together. The top section holds the tracks. Tracks show activity on the timeline, and each track can contain multiple lanes with charts that show levels or regions.\n\nBelow the timeline is the detail view. It shows summary information about the range you're currently inspecting.\n\nIf you click a bar in the timeline or a row in the detail view, the inspector opens up on the right giving you a closer look at what you've selected.\n\nThe Foundation Models Instrument has 6 lanes in the timeline. These give you a quick overview of session structure and latencies. Alongside the timeline, there's a tree detail view. That's where you can really dig into the model's chain of thought.\n\nThe Instructions lane shows how long a given set of instructions and tools was active. One set can cover multiple requests. Looking at this lane, it's clear only one set of instructions was active for the entire session but the feature was supposed to use two, so something went wrong during the handoff.\n\nThe Model Inference lane has two types of bars: yellow and orange.\n\nYellow bars represent how long the system spent processing the input prompt.\n\nOrange bars represent how long it took to generate the response.\n\nThe timeline gives you a quick overview but the real power is in the tree view. It takes everything logged during this recording and organizes it into a hierarchy: sessions, requests, model inferences, instructions, prompts, and responses. Let's use it to track down why the instruction set never changed.\n\nSession 1 had two requests. The first one was kicked off by the prompt starting with \"Please generate 3 craft ideas.\" That request was made up of two model inferences and a few tool calls. Every model inference should have instructions, a prompt, and either a response or an error. Click any node in the tree to pull it up in the inspector.\n\nThe model inference detail shows a summary of the instructions, prompt, and response that made up this call.\n\nScroll down and you'll find duration visualizations and token usage metrics. We'll come back to those later when we talk about optimizing for reliability and performance.\n\nGetting back to the failure, the timeline already told us the instruction set never changed, and here in the inspector for this model inference node, I can see the prompt tied to those instructions. Let's select the Instructions node to see how they're set up.\n\nThe inspector shows that this instruction only had one tool associated with it. The prompt references the switchToTutorialMode tool but that tool isn't actually configured with this instruction.\n\nWithout it, the app has no way to switch from brainstorm mode to tutorial mode, so the crafter gets stuck in a loop.\n\nLooking at the subsequent nodes in the tree, this was a silent failure. The model kept accepting input and making tool calls but never threw an error.\n\nThere was no clear signal that anything had gone wrong. That makes it a hard bug to catch. Now that the root cause is clear, I'll jump into Xcode to fix it. Based on what I found in Instruments, I'll look at the BrainstormDynamicInstructions definition. In the Instructions block, the SwitchToTutorialMode tool is mentioned in the prompt but only the GenerateCraftIdeasTool is listed in the toolset, so let's add it.\n\nNow, I'll recompile and re-run with Instruments to make sure the fix actually worked.\n\nBack in the app, I'll head to the Ideas tab, and just like before, the model suggests some new crafts. I'll go with... necklace.\n\nAnd there it is. The UI has switched to tutorial mode. The model made the transition and generated a full tutorial for this craft. Now let's jump back into Instruments and take a look at this new recording to make sure everything ran efficiently.\n\nThe Instructions lane now shows two distinct instructions active during this experience.\n\nThe first is a brainstorming instruction and the second is a tutorial generation instruction.\n\nThat lines up exactly with the brainstorm experience design we covered earlier. Let's dig into the tree view to see how that transition actually happened.\n\nThe first set of instructions now includes both the generateCraftIdea and switchToTutorialMode tools. That confirms the model had everything it needed to make the switch. The fix worked. The instruction change happened after the second model inference of Request 2.\n\nThat inference resulted in a tool call to switchToTutorialMode, passing the selected craft as an argument.\n\nAnd in the following request, the instructions correctly switched over to the tutorial generator, with the selected craft passed along as context.\n\nThe info column is a great way to quickly flag nodes worth a closer look: things like errors, long durations, and large token counts. Request 1's first model inference took a bit longer than I was expecting, so let's take a look.\n\nThe metrics and duration sections break down token usage for this inference. These numbers are your starting point for understanding and improving the efficiency of an experience.\n\nYou can measure performance using three key metrics. Time to First Token measures how long it takes for the model to begin generating a response after receiving a prompt. A high Time to First Token means people are staring at a blank screen. To reduce it, shorten your prompt.\n\nTokens per Second measures overall generation speed of the response. Use it to benchmark performance across different prompt configurations and catch regressions after changes.\n\nTotal Latency is the complete time from sending the request to receiving the final response. This is the number people feel most directly. To reduce perceived Total Latency, utilize streaming to surface partial results sooner.\n\nRunning a trace is where optimization starts. These metrics tell you exactly where time and resources are going and point you toward the right fix. Use the model inference node to get a clear picture of your token usage.\n\nIn this session, I showed you how to use Instruments to debug an agentic experience developed with the Foundation Models framework. Once you've ironed out the bugs, the next thing to explore is evaluation. Watch \"Meet the Evaluations framework\" to see how you can measure and improve the quality of your prompts by using structured evaluation.\n\nTo get started with the improved Foundation Models Instrument, install Xcode 27. Then, on the device you'd like to run and profile your app on, update to the latest OS releases. Its important to note that this Instrument supports using any model you use with the Foundation Models framework.\n\nThe Foundation Models APIs are your starting point. Experiment, build, and see what's possible. When something isn't working as expected, the Foundation Models Instrument is there to help you debug, giving you direct visibility into framework behavior right in context. Go further with related sessions on agentic app experiences and the Evaluations framework and explore the full documentation to unlock everything the framework can do. Thank you for joining us! We're excited to see you develop and debug your intelligent experiences using the improved Foundation Models Instrument.",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Analyzing the runtime performance of your Foundation Models app",
+ "url": "https://developer.apple.com/documentation/FoundationModels/analyzing-the-runtime-performance-of-your-foundation-models-app"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/243/4/127c397a-8124-4f3d-ad18-ac2a1d275803/downloads/wwdc2026-243_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/243/4/127c397a-8124-4f3d-ad18-ac2a1d275803/downloads/wwdc2026-243_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "339",
+ "year": "2026",
+ "title": "Bring an LLM provider to the Foundation Models framework",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/339"
+ },
+ {
+ "id": "242",
+ "year": "2026",
+ "title": "Build agentic app experiences with the Foundation Models framework",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/242"
+ },
+ {
+ "id": "334",
+ "year": "2026",
+ "title": "Build AI-powered scripts with the fm CLI and Python SDK",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/334"
+ },
+ {
+ "id": "319",
+ "year": "2026",
+ "title": "Build with the new Apple Foundation Model on Private Cloud Compute",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/319"
+ },
+ {
+ "id": "241",
+ "year": "2026",
+ "title": "What’s new in the Foundation Models framework",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/241"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:14.986Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-246.json b/data/wwdc/videos/2026-246.json
new file mode 100644
index 0000000..0d426b3
--- /dev/null
+++ b/data/wwdc/videos/2026-246.json
@@ -0,0 +1,106 @@
+{
+ "id": "246",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/246/",
+ "title": "LLM search using Core Spotlight",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Spatial Computing"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Jennifer, from the Spotlight engineering team. This year, we're taking search to a whole new level, with Foundation Models and Core Spotlight. You can build rich, conversational experiences in your app, simply by making your app content available to a large language model for reasoning and response generation.\n\nNow I'm from California, and there are so many beautiful hikes in the area. I've been slowly making my way through some of the nicest trails, so I thought I'd build an app to help me along.\n\nIn my hiking trails app, I can already browse through state parks and trails. And once I've completed a trail, I like to write my own notes on what I enjoyed most about the hike. But it would be really great to be able to ask a language model all about the hikes I've already gone on, or even about new hikes I should try. Well, the Foundation Models framework makes it easy to get started. By introducing a language model session into the app, I can ask broad questions, and the model will answer just by drawing on its own knowledge of the world.\n\nNow, I really only want answers about hikes that my app knows about.\n\nAnd this is where Spotlight can help. The hiking trails app has indexed all these great hikes into a Core Spotlight search index.\n\nSo to help the model answer questions about those particular hikes, we can use the app's Core Spotlight search index, through tool-calling from the Foundation Models framework.\n\nThe Tool protocol from Foundation Models is a powerful concept that can be used to extend a model's capabilities, both by taking actions for a request, or by looking up context that a model needs to generate a response.\n\nA tool works by declaring its arguments and output, along with some instructions on what the tool does.\n\nAnd then, when the model decides it needs to use a tool, it will simply generate the arguments to call up that tool, and use that output for response generation.\n\nIf you haven't already, there are some great sessions, such as the \"Deep dive into the Foundation Models framework\", to learn more about how tool-calling works.\n\nSo what if we had a tool that lets a model generate a search on an app's Core Spotlight index? Well, today, we're introducing SpotlightSearchTool. It's a tool that adopts the tool protocol, to let a language model directly search your app's content in Core Spotlight for contextual response generation.\n\nSpotlightSearchTool is available on iOS, iPadOS, macOS, and visionOS.\n\nBefore we get started, you'll want to make sure your app donates searchable content with Core Spotlight. Take a look at our past session on \"Supporting semantic search with Core Spotlight\", where we talk through how to donate searchable content to Spotlight, how to manage donations with a delegate and reindex extension, and how to perform structured search over item attributes, and search against the semantic index.\n\nOnce your app has donated searchable items to Core Spotlight, or indexed entities for Apple Intelligence, we're ready to begin.\n\nWe have a lot to cover in this video! We'll show you how to provide the new SpotlightSearchTool to your language model session.\n\nThen we'll explore how to customize SpotlightSearchTool with guidance, knowledge providers, and specialized capabilities . And finally, we'll look at ways to evaluate model responses, with the evaluations framework. Alright let's get started.\n\nFirst, let's see how SpotlightSearchTool can be used for contextual response generation.\n\nIn our hiking trails app, we've donated searchable items, to the Spotlight index that represent hiking trails. Each trail has metadata like the trail's name and location. And on some of the hikes, there's also some personal details such as the date when a hike was completed, and some notes I wrote about how those hikes went.\n\nIf I wanted to ask: What hikes have I gone on?, the model will need to search for items by attributes, like completion date and location, to be able to formulate its response. So let's build this functionality into our app. There's three things we'll look at when adopting SpotlightSearchTool. We'll need to configure the tool, for the kind of search we want the model to perform. Then we'll want to add additional context to the model, while the search is active, to get the best response. And finally, we'll explore different ways to display results in our app's user interface.\n\nConfiguring the tool is not too different from performing a Spotlight query directly. We'll start by importing both CoreSpotlight and FoundationModels. Then, in one line of code, the tool is ready to search your app's Core Spotlight index. You can also provide SpotlightSearchTool with a custom configuration. Here we're specifying a FileSource to perform a search against file paths in your app's sandbox.\n\nNext you'll want to choose the right model for your app, whether it's the SystemLanguageModel or a model of your choosing, which you can do using the new Model Provider APIs.\n\nOnce you've chosen the model, add the new SpotlightSearchTool instance to your LanguageModelSession to start getting a response.\n\nIt feels like magic, but the response follows a path of tool calling and generation. For a question like: What hikes have I gone on?, the trajectory might start with the model deciding it needs to use SpotlightSearchTool the model will invoke the tool with a generated query Spotlight will execute that query and return a description of the result set back, and the model will reason over that output and generate its final response.\n\nNow when I ask: What hikes have I gone on?, the model can generate an answer grounded in the app's content.\n\nYou might notice from some responses, that the model was not able to see all of the metadata, that was donated for the items.\n\nThat's because some metadata in the Spotlight index, like text content and HTML, is stored in a highly-compact representation that can be searched, but not recovered in a way that a language model can read it. For these cases, you'll want to consider providing additional metadata for an item, while SpotlightSearchTool is performing a search.\n\nIf your app donates searchable content to Core Spotlight, you'll already be familiar with the index delegate protocol.\n\nYour app would set an index delegate on your CSSearchableIndex to handle reindex requests, such as when Spotlight needs to perform migration or recovery. For SpotlightSearchTool, we've added a method to the delegate to recover the full CSSearchableItem by its unique identifier. This allows the model to efficiently manage responses over potentially millions of results.\n\nOn your index delegate, simply adopt the new searchableItems (forIdentifiers:) to return the complete CSSearchableItem.\n\nIf your app has metadata that doesn't make sense to donate for search, but might be useful for the model to reason about, this is the right time to set any additional attributes on an item for the model to see.\n\nNow that we've configured the tool to perform searches, we'll want to think about how to display results and responses in our user interface. The session response is a concise description over the result set. And in an assistant-style interface, this response is typically what an app would want to display.\n\nBut search results are also available directly on SpotlightSearchTool itself. For a list-style display, this is the best way to access searchable items, especially when the result set is large. Search replies pass back results in batches during the search, so query tokens can be used to manage the conversation stream, ensuring that user interface stays up-to-date with the model.\n\nTo access results from the SpotlightSearchTool, your app can wait for search replies and check for CSSearchableItem in the content of the reply. Search replies come as an async sequence of events, where each reply may include a batch of results, until the tool call completes.\n\nKeep in mind that for any given response, the model may call SpotlightSearchTool more than once, before generating its final response. For that reason, use the queryToken on each reply, to determine when the user interface should refresh.\n\nSpotlightSearchTool provides a host of search capabilities, from semantic search over text, to structured search over metadata, like dates, persons, locations and more. But depending on the language model you choose, you may want to customize SpotlightSearchTool both for the model, and your app content. There's a few ways to customize SpotlightSearchTool. Guidance profiles can be used to scope the tool's search capabilities. Providing the tool with world knowledge can help with reference resolution. And implementing custom pipeline stages, can improve model reasoning over your app's content.\n\nSpotlightSearchTool provides its entire set of search capabilities to a model for guided generation. But guidance profiles can help scope that guidance to only what an app needs.\n\nThe hiking trails app doesn't donate person relationships, so guiding the model on how to search for authors and recipients, could be skipped for limited-context models. To selectively enable guidance on search capabilities like people and dates, use a GuidanceProfile.\n\nYou can even specify the exact list of metadata attributes, that the model should consider during a search.\n\nThen set a dynamic guide level using the profile, when creating SpotlightSearchTool. On-device models have a more restricted model context size, so it's best to use focused guidance for simpler search capabilities.\n\nReference resolution is another way for your app to provide context that's not directly available in the search index. As an example, if the hiking trails app did donate person relationships, the person using the app might want to ask about other participants on the trail. In that case, the model needs to know who that person refers to in a prompt. If the app already knows who that person is, use a contact resolver to help the tool filter to the right set of results.\n\nA contactResolver should return any contact information related to the user's identity, that can be matched against metadata in the search index.\n\nAnd at last, your app can take advantage of custom pipeline stages, that take document reasoning even further. For really complex requests, the language model might forgo a simple search query, in favor of a pipeline search. A pipeline search brings together queries to the index, plus computation over a result set, for maximal efficiency. I could ask: how many trails have I hiked this year, and for each month, how many miles have I gone on average? Now, the model could perform a simple search and keep a tally in memory to answer the question. Or, if the result set is likely to be large, SpotlightSearchTool allows the model to request that Spotlight run a pipeline of search and computation stages.\n\nWith a pipeline search, the model can break down this complex query into a set of steps.\n\nThe model might generate a search for completed hikes, along with a counting stage that builds a table by month, then a stage that computes an average over all counts.\n\nPipeline stages allow the tool to perform efficient computation, or transformation, over a search result set on behalf of the model. And your app can participate by registering its own custom stages. Pipeline stages are Generable, so the model will generate a stage on-demand based on the user's prompt. And whenever a stage is generated, the model may choose to return data back to the app when it makes sense. The Foundation Models deep dive has a great segment on Guided Generation and Generable types that I highly recommend.\n\nLet's take a look at the hiking trails app again. Some trails includes personal notes on how each hike went, so I might want to ask: I remember being really happy on some of my hikes. Which ones were they? On its own, the model could make its best guess at my happiness level, just by reading my notes.\n\nOr, the app could register a custom stage, that computes a happiness score over each item, allowing the model to generate a response, solely on the computed top-scoring results.\n\nTo build a custom stage that computes a happiness score, we'll want to operate on CSSearchableItem as the input, and return a scored version as the output. The score could be computed by running a sentiment analysis model over the notes attribute on the item, or by some other custom logic, perhaps taking into account hikes rated with 5 stars. And since this is a Generable type, we can add properties with Guides to inform the model on which results to prefer. Then we simply register the stage by adding it to the tool's configuration. There's one more thing: remember how SpotlightSearchTool returns replies with search results for display? Well, the model may decide to send back a search reply with the output data of a pipeline stage, as another kind of partial result. From aggregate counts and tables, to free-form text or computed numeric values, your app can display some or all of these data types. And each reply comes with a handy LLM-generated label describing the content, giving your app the most flexibility for its user interface. With so many options for customization, from the model we choose and the searchable content our app donates, to guidance levels and custom reasoning, how can we verify, in a broad way, how well the model is responding in our app? Well, the Evaluations framework can help us in a few important ways. Not only can we quickly build evaluations to see how well the model is calling the tool, and how meaningful the response; we can also rapidly iterate on our app's searchable content paired with different guidance profiles on SpotlightSearchTool itself. The Evaluations framework has some great APIs for building an end-to-end evaluation suite, from large-scale dataset generation, to evaluation runs using custom metrics, and reporting. There are some great sessions that go in-depth on sample data generation APIs, and the video on creating robust evaluations for an agentic app is a great resource to get started on evaluating model responses with tool-calling.\n\nFor our purposes, we're going to focus on result coverage as a way to evaluate the hiking trails conversational experience. We want to know, given a dataset that's indexed in Core Spotlight, how well does the model generate responses based on the items we expect it to find. We'll start by defining a dataset that adopts the ModelSampleProtocol. Our TrailRequest already includes the natural language input that a person might ask about trails in our app, the output is a language model response and an expectation of the trajectory of the request. We'll also be adding a set of unique identifiers of searchable items that we expect the tool to return for that prompt.\n\nIf we have real data to test against, that's great; but if not, we can use Sample Generation APIs to generate data based on a prompt. Let's take a look at this in Xcode.\n\nFor our evaluations, we can define a set of hiking trails with the metadata that our app is expected to donate to Core Spotlight.\n\nThen we'll build a set of seed samples to use in our evaluations. Samples can be serialized in any Codable format, and JSON works well for that purpose. Our samples include the query and the set of item identifiers we expect to be returned for the search. We can also provide a sample response that we can use later in a quality comparison with the model's actual response.\n\nUsing the Sample Generation APIs in a command line tool, I can expand this seed set to many more variations, to get broad coverage on how people might want to ask about trails.\n\nThe next step is to define our evaluation with metrics and trajectory. For our samples, we expect the trajectory of a response to include a call to SpotlightSearchTool to perform a query, so here's how we might define that expectation.\n\nAnd here's an overview of an evaluation flow that takes into account how many expected items were included in the final response. In our test target, our evaluation will load the trail items and samples from our generated datasets. Then, we'll donate the trail items to Core Spotlight, and configure SpotlightSearchTool for this evaluation. Once the evaluation completes its run, we can set the expectation for any metric we've included, like result coverage.\n\nThis is just the start towards building comprehensive evaluations that will help you craft the best experience possible for your app. It's a big year for Foundation Models, and we hope you'll make the most of it. Download our sample code to see the hiking trails app in action. Try adding your own custom functionality to the app to really see what's possible. You might also want to add your own evaluation suite, with some inspiration from the evaluations agentic deep dive.\n\nAnd remember, we're not writing search queries anymore. We're providing the content, and letting intelligence do the rest.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "0:59",
+ "title": "Ask the model with a Foundation Models session",
+ "language": "swift",
+ "code": "let response = try await session.respond(to: \"What are some nice hikes near water?\")"
+ },
+ {
+ "timestamp": "4:20",
+ "title": "Set up SpotlightSearchTool",
+ "language": "swift",
+ "code": "// Set up SpotlightSearchTool\n import CoreSpotlight\n import FoundationModels\n\n // In one line, the tool is ready to search your app's Core Spotlight index\n let tool = SpotlightSearchTool()\n\n // Or provide a custom configuration — e.g. search file paths in your app's sandbox\n let fileTool = SpotlightSearchTool(\n configuration: .init(\n sources: [\n .files\n ]\n )\n )"
+ },
+ {
+ "timestamp": "4:50",
+ "title": "Add SpotlightSearchTool to a session",
+ "language": "swift",
+ "code": "// Add SpotlightSearchTool to a session\n import CoreSpotlight\n import FoundationModels\n \n let tool = SpotlightSearchTool()\n\n let session = LanguageModelSession(model: model, tools: [tool], instructions: instructions)\n\n let response = try await session.respond(to: \"What hikes have I gone on?\")"
+ },
+ {
+ "timestamp": "6:24",
+ "title": "Implement an index delegate",
+ "language": "swift",
+ "code": "// Implement an index delegate\n import CoreSpotlight\n\n class IndexDelegate: NSObject, CSSearchableIndexDelegate {\n\n // Called when the index requests searchable items for the provided identifiers\n func searchableItems(forIdentifiers identifiers: [String]) async -> [CSSearchableItem] {\n let entries = await mystore.fetchEntries(ids: identifiers)\n return entries.map { makeSearchableItem(from: $0) }\n }\n }"
+ },
+ {
+ "timestamp": "7:37",
+ "title": "Track the query token for refresh",
+ "language": "swift",
+ "code": "// Track the query token for refresh\n import CoreSpotlight\n import FoundationModels\n\n let tool = SpotlightSearchTool()\n\n for await reply in tool.searchResults {\n \n if reply.queryToken != currentToken {\n // New query — start a new display section\n currentToken = reply.queryToken\n }\n\n switch reply.content {\n case .items(let searchItems):\n }\n }"
+ },
+ {
+ "timestamp": "8:42",
+ "title": "Set a dynamic guidance profile",
+ "language": "swift",
+ "code": "// Set a dynamic guidance profile\n import CoreSpotlight\n import FoundationModels\n\n let profile = SpotlightSearchTool.GuidanceProfile(\n textMatch: true,\n dates: true,\n people: false,\n attributes: [.title, .altitude, .completionDate]\n )\n\n let tool = SpotlightSearchTool(\n configuration: .init(\n guide: .init(level: .dynamic(profile))\n )\n )\n\n // On-device models have smaller context — prefer focused guidance\n let focusedTool = SpotlightSearchTool(\n configuration: .init(\n guide: .init(level: .focused(.items))\n )\n )"
+ },
+ {
+ "timestamp": "9:32",
+ "title": "Implement a ContactResolver",
+ "language": "swift",
+ "code": "// Implement a ContactResolver\n import CoreSpotlight\n import FoundationModels\n\n struct MyContactResolver: ContactResolver {\n \n func userIdentity() -> ResolvedContact {\n // Pull from whatever identity source your app has —\n // account profile, Contacts framework, sign-in session, etc.\n var contact = ResolvedContact(displayName: \"Jane Doe\")\n contact.emailAddresses = [\"jane@example.com\", \"jdoe@work.com\"]\n contact.names = [\"Jane\", \"JD\"]\n return contact\n }\n }\n \n tool.contactResolver = MyContactResolver()"
+ },
+ {
+ "timestamp": "11:34",
+ "title": "Define a custom stage",
+ "language": "swift",
+ "code": "// Define a custom stage\n import CoreSpotlight\n import FoundationModels\n\n @Generable\n struct HappinessStage: CustomStage {\n static var name = \"happiness\"\n static var description = \"Scores hike by how happy the author was\"\n static var inputTypes: [SearchPipelineDataType] = [.items]\n static var outputTypes: [SearchPipelineDataType] = [.scoredItems]\n\n @Guide(description: \"Minimum happiness score (0.0-1.0) to include in results\")\n var threshold: Double?\n\n func execute(on input: SearchPipelineData) async throws -> SearchPipelineData {\n return SearchPipelineData(payload: .scoredItems(sorted))\n }\n }\n\n // Register the stage by adding it to the tool's configuration\n let tool = SpotlightSearchTool(configuration: .init(\n customStages: [.happinessBoost(threshold: 0.5)])\n )"
+ },
+ {
+ "timestamp": "12:10",
+ "title": "Handle a reply data types",
+ "language": "swift",
+ "code": "// Handle a reply data types\n import CoreSpotlight\n import FoundationModels\n\n for await reply in tool.searchResults {\n\n let label = reply.label\n case .items(let searchItems):\n case .scoredItems(let scored):\n case .groupedItems(let groups):\n case .count(let count):\n case .table(let table):\n case .statistic(let statistic):\n case .text(let text):\n continue\n } \n }"
+ },
+ {
+ "timestamp": "13:47",
+ "title": "Define an evaluation dataset with ModelSampleProtocol",
+ "language": "swift",
+ "code": "// Evaluations\n import Evaluations\n \n struct TrailRequest: ModelSampleProtocol {\n \n typealias ExpectedValue = String // sample response\n typealias Expectation = TrajectoryExpectation \n \n var input: ModelSampleInput\n var output: ModelSampleOutput\n \n var expectedIdentifiers: [String]\n }"
+ },
+ {
+ "timestamp": "15:06",
+ "title": "Define the trajectory expectation",
+ "language": "swift",
+ "code": "// Evaluations\n import Evaluations\n \n TrajectoryExpectation(\n unordered: [\n ToolExpectation(\"searchSpotlight\", arguments: [.keyOnly(argumentName: \"query\")])\n ] \n )"
+ },
+ {
+ "timestamp": "15:17",
+ "title": "Run the evaluation test —",
+ "language": "swift",
+ "code": "@Test(\"Trail search evaluation meets quality thresholds\")\n func trailSearchEval() async throws {\n \n let items = try Self.loadItems()\n let samples = try Self.loadSamples()\n \n try await Self.indexDelegate.indexSearchableItems(items)\n let tool = Self.makeSearchTool()\n \n let evaluation = TrailSearchEvaluation(\n tool: tool,\n dataset: ArrayLoader(samples: samples)\n ) \n \n let result = try await evaluation.run()\n let coverageMean = result.aggregateValue(.mean(of: Metric(\"ResultCoverage\")))\n #expect(coverageMean >= 0.5, \"Result coverage should be at least 50% across queries\")\n }"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Spotlight search tool",
+ "url": "https://developer.apple.com/documentation/CoreSpotlight/Spotlight-search-tool"
+ },
+ {
+ "title": "Making your indexed content available to Foundation Models",
+ "url": "https://developer.apple.com/documentation/CoreSpotlight/making-your-indexed-content-available-to-foundation-models"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/246/4/b390ab9d-d231-4cf5-9d1b-e4270ef5012b/downloads/wwdc2026-246_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/246/4/b390ab9d-d231-4cf5-9d1b-e4270ef5012b/downloads/wwdc2026-246_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:15.187Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-250.json b/data/wwdc/videos/2026-250.json
new file mode 100644
index 0000000..be21a85
--- /dev/null
+++ b/data/wwdc/videos/2026-250.json
@@ -0,0 +1,44 @@
+{
+ "id": "250",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/250/",
+ "title": "Principles of great design",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Accessibility & Inclusion",
+ "Design",
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hi, I'm Linda. And I'm Doug. We're Design Evangelists at Apple.\n\nLet's start with a question: what is design? Take a second with it, because I think a lot of us, if we're being honest, would jump to \"design is how something looks\" or maybe even \"design is how something behaves\" And these definitions aren't necessarily incorrect, they just paint an incomplete picture. For us at Apple, design is making something with intention. It's focusing on what's most important to people, so you can build something they will truly value.\n\nEvery feature you add to your product, asks something of the person using it. It asks for their time, their attention, and their trust. These are valuable things you can't afford to waste. So choosing what to build, is often a matter of deciding what not to include.\n\nBefore you a draw a single sketch, or write a line of code, think about whether what you're making has purpose.\n\nPurpose, is one of the foundational principles, that you can use to design great experiences on Apple platforms. Experiences that serve people, that respect and adapt to their lives, that are clear and considered, and at their best, a genuine joy to use. Now, one thing we want to say upfront. There's no formula or right way, to combine these principles that guarantees you'll arrive at the perfect solution. You might even find that leaning into one principle, feels like you're compromising on another. But that's what makes design so interesting. Ultimately, it's up to you to use your knowledge and intuition to find the best path forward. Okay let's get into it! After purpose, there's agency.\n\nAgency is about putting people in control. People feel in control, when you let them do things their way.\n\nFor example, do you want to take this next part? Oh, um, I'm okay, you can keep going! Okay! Offering choices is the best way to bring agency into your experience. An interface should never stand in the way of what someone is trying to do.\n\nInstead of guiding someone down a pre-determined path, let them dive right into your experience. And give them the autonomy to decide what to explore at their own pace.\n\nPeople are far more engaged when they have agency to control their own experience. Of course, that does mean people will make mistakes, or go down paths they didn't mean to.\n\nWhen that happens, offer forgiveness.\n\nPeople accidentally send, change, and delete things all the time.\n\nYou can provide forgiveness by making it easy for them to undo any of their actions.\n\nAnd, when someone's about to do something destructive, double-check it's what they actually mean to do. In some cases, interruptions can be helpful. But use interruptions carefully, and only when someone is about to make a big mistake. Oops, I don't wanna do that! Just one sec here.\n\nLet's keep going.\n\nPeople really appreciate it, when you help them avoid disaster.\n\nForgiveness supports agency because it gives people confidence that they can always recover from anything they try, and helps them feel capable, secure, and free to explore. Exactly. But when you give people freedom, you also have to protect their well-being.\n\nWhich leads us to responsibility. On Apple platforms, responsibility means acting in people's best interest. And it starts with privacy. Privacy is a human right. And even though there's tons of information you can request from someone, it's not the best way to build a relationship. Imagine someone comes up to you like this: Hey! Hi! Give me your phone number.\n\nFor what? I just need it.\n\nUh, why? I'll tell you, once you give me your phone number.\n\nYou wouldn't trust a person like that in real life. And yet, interfaces do this all the time.\n\nThey throw permission prompts the second you launch them. Long before you've figured out what the app actually does.\n\nOr ask you for information, without providing context for what it's for.\n\nA responsible design treats people and their private information with respect, just like you would in the real world.\n\nResponsible interfaces wait for the right moment to ask for personal data. They only ask for what's necessary, and are transparent about the data is for.\n\nIt's your responsibility to protect anyone using your product, and anyone who could be affected by it. That's why in addition to privacy, you should keep people safe.\n\nFor interfaces, this means looking closely at what functionality you offer, and asking yourself some hard questions. How could this feature be misused? Who would be harmed by this? And how do I prevent it? Think about what it means to responsibly add AI capabilities to your product.\n\nWhen you build intelligent features, you have to anticipate that a model might generate something unexpected or inaccurate.\n\nTake something as innocent as a recipe app.\n\nIf someone logs an allergy, you have to anticipate that the model may suggest an ingredient that could cause a severe reaction. That could do real-world harm and it's just something you can't leave to chance. Think realistically about what could go wrong, and add safeguards. Previews, confirmations, disclaimers can help, but consider removing features entirely, if the risks to people's safety outweigh the value. Ultimately, your work has a real impact on people's lives. So, when you take that responsibility seriously, it leads to a product people can trust.\n\nThe next design principle, familiarity, is all about how people bring their existing knowledge to your design. Familiarity is about building on what people know.\n\nYour audience comes to you with a lifetime of experience. They understand how the real world works, and they've learned conventions from other interfaces. You can lean on that existing knowledge, to make your design intuitive.\n\nA great way to do this, is by using metaphor. Metaphors have been used since the earliest interfaces, to help people get familiar with software. What does this thing do? Same as the real world! Stuff I don't want, goes in the trash.\n\nAnd actually, if I made a mistake, I can retrieve it from the trash! Just like the real world! Okay! The trick to metaphors, is making sure they aren't too literal or abstract. In an interface, an inspector shows details of whatever is selected. If your inspector metaphor is too literal, people might not be familiar with what you're trying to show.\n\nIf it's too abstract, you risk your idea not getting across.\n\nA good metaphor draws on something people know and helps people predict what it will do. When used correctly, metaphors instantly click. When used incorrectly, metaphors can be surprising, in a bad way.\n\nLet's go back to the trash can. If you use a trash can icon to mean something other than delete, it goes against people's familiarity with what this symbol represents, in other software.\n\nThe same is true if you take creative liberty with the delete icon. People don't get that immediate recognition.\n\nFor common actions, there's no need to reinvent the wheel. Just use the metaphors people are already familiar with, and make sure they do what people expect. Familiarity is also a product of being consistent. Consistency helps people predict what will happen next.\n\nSimply put, things that look the same should behave the same.\n\nIf one of these buttons moves to a different screen, another toggles an action, and another pops up a modal, there's no pattern to understanding how this interface works.\n\nConsistent behavior helps people navigate your interface, because they can anticipate what will happen and where they're going. And so does consistent placement. On Mac, you can always close a window by going to the top left corner. It's always in the exact same spot.\n\nWhen someone can find an action in the same location across screens and devices, it speeds them up. They don't have to think about it.\n\nCreating something familiar is all about knowing which metaphors and patterns to use and when to use them. But familiarity doesn't mean recycling the same solution everywhere.\n\nThat brings us to flexibility. A flexible design, recognizes a simple truth.\n\nThat people use your design in ways as unique as they are. So support all the different contexts people find themselves in.\n\nAdding flexibility into your interface, allows it to adapt to people's actual lives. So, take listening to music.\n\nThe way someone interacts with their music, changes completely depending on their context.\n\nThey might be at home, controlling their music through the speakers.\n\nOr on a run with their AirPods and watch. Or driving, using a completely hands-free experience.\n\nAn interface that accommodates different situations feels more comfortable and works for a wider audience. Right! And when you design for specific contexts, it shows you're paying attention to what a person wants to do.\n\nWhen someone pulls out their iPhone, they want quick, touch-based interactions. On Mac, they expect deep workflows, and precise pointer controls.\n\nEvery device deserves a solution that takes advantage of what makes it unique.\n\nOf course, hardware is only half the story. The other half is the person using it. Another way make your design flexible, is to cater to the wide range of abilities people have.\n\nGet curious about who your audience is. How old are they? What languages do they speak? Are they a pro or a novice? Do they rely on accessibility features? You might not solve for every type of person on day one, but you can start examining how your experience can be more inclusive. Often, adding flexibility means you're not going to land on a single design solution that makes everyone happy. Sometimes, the best option, is to let people personalize your experience to suit their own preferences. Take something like controls. It's really hard to nail down a single layout that works perfectly for every person, so offer people the flexibility to rearrange them, to support their personal workflow. Or allow them to hide controls they never use.\n\nFlexibility is an investment, but it's worth it, because it proves to people that you designed with them in mind.\n\nNext, simplicity. Simplicity is about stripping away the unnecessary, so the core purpose of your design can shine.\n\nWhen we say simple, we don't mean minimal.\n\nIf you bury all your functionality inside a single place, that might make your interface look more minimal, but it doesn't make it simple.\n\nSimple designs are frictionless and intuitive. People can find what they need without effort.\n\nAnd they get there by being concise and clear.\n\nConcise interfaces use plain language.\n\nThey strip away jargon and speak naturally.\n\nThey avoid redundancy and get straight to the point.\n\nConcise interfaces also respect people's time. They reduce the number of steps it takes to get things done. You can also achieve simplicity through being clear.\n\nA clear design perfectly communicates what it does.\n\nClarity is built with hierarchy, using order, spacing, and contrast to guide people to what's most important.\n\nWhen your hierarchy is strong, the most important item on the screen is always the most obvious one.\n\nClear interfaces answer people's questions: What do I pay attention to? What can I interact with? And how do I interact? In a simple interface, every element earns its place. So take a look at your design and identify where there's information you can distill down to its essence.\n\nIs there complex data that might be better understood as a graphic? Are there opportunities to summarize information so people can focus on what they care about? Make sure every element helps clarify your point. And in some cases, making an interface simpler can mean adding more to it.\n\nTake this video play/pause control. It's simple and familiar. I can pause what I'm watching, and when I come back to it later, I need more context. This control clarifies where I am and how much time is left. Sometimes, simple means adding context and information, so people can make informed decisions.\n\nSimple interfaces support the reason people are there. And you'll know you've arrived at simplicity when you have exactly enough! Our next principle is about executing what you have flawlessly.\n\nCraft is the attention to detail that tells people you really care about the experience you're giving them.\n\nWe all know what a cheap product feels like. A rickety door that doesn't close properly. A shirt that unravels when you wash it. You can just tell when someone took shortcuts. And it's the same with software.\n\nYou know exactly what it feels like to use an interface that was rushed out the door.\n\nYou tap a button and you just have to wait for it do something. Scrolling is jittery. Icons are misaligned. You rotate your phone, and the layout gets all messed up. It feels fragile.\n\nWhen software feels thrown together, you question the quality of the results you'll get from using that product. But a meticulously crafted design does the exact opposite; it inspires confidence. So, what are the actual ingredients to a well-crafted design? Just like in the real world, it starts with high-quality materials.\n\nBeautiful fonts that look great across devices. Thoughtful colors that adapt seamlessly across light and dark environments.\n\nClear graphics and iconography.\n\nResponsive animations that feel fluid and provide immediate, natural feedback.\n\nAll built on a solid foundation of reliable and secure SDKs.\n\nThese are the details that matter. But getting to that level of quality requires time. Craft comes from iteration and making sure every last piece of your interface functions beautifully. And this is a continual process.\n\nA large part of craft, is how you maintain your design over time.\n\nGreat design has longevity, so keep evolving it. When new features or hardware are introduced, explore whether they make sense for your experience. When your product evolves with these changes, people feel supported and rewarded.\n\nCraft is an uncompromising commitment to the details. When you get those details right, people will know you care.\n\nFinally, delight. Delight is one of those things that's hard to define, but you instantly recognize it when you experience it.\n\nDelightful interfaces are satisfying, enriching, and create a real emotional connection.\n\nThat connection starts when an experience feels human.\n\nThe way to make a design delightful isn't by adding confetti or tacking on extra flourishes at the end of your process. You create a delightful interface by identifying the emotion you want your audience to feel. Relaxed, confident, excited, and finding opportunities to reinforce that through your design. Delight is the sum of the consideration you put into your product. It's the natural result of getting all the design principles right.\n\nBecause when you design with intention and care... ...when you give people the agency to act, the safety to explore, the comfort of familiar patterns, and the ability to make it their own... ...you create an experience that's a true joy to use.\n\nAnd now it's your turn! The Human Interface Guidelines are the best resource to get started with designing for Apple platforms. And we've added a new design principles page, so you can learn more.\n\nUse these principles to guide your design, and go make something that people really love.",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Human Interface Guidelines: Design principles",
+ "url": "https://developer.apple.com/design/human-interface-guidelines/design-principles"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/250/4/ad804f32-2805-48aa-891c-8c742579acab/downloads/wwdc2026-250_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/250/4/ad804f32-2805-48aa-891c-8c742579acab/downloads/wwdc2026-250_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "290",
+ "year": "2026",
+ "title": "Craft clear names for features and labels in your app",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/290"
+ },
+ {
+ "id": "802",
+ "year": "2017",
+ "title": "Essential Design Principles",
+ "url": "https://developer.apple.com/videos/play/wwdc2017/802"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:15.848Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-251.json b/data/wwdc/videos/2026-251.json
new file mode 100644
index 0000000..e584687
--- /dev/null
+++ b/data/wwdc/videos/2026-251.json
@@ -0,0 +1,28 @@
+{
+ "id": "251",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/251/",
+ "title": "Communicate your brand identity on iOS",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Design"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Branding is likely top of mind for you and it's a helpful way to communicate and differentiate your products. But, how do you achieve the expression of your brand identity while keeping the integrity of familiar paradigms on iOS? I'm Sarah, a Design Evangelist, and today I'm going to unpack that. I'll share how to approach design in ways that feel familiar where it matters, but bespoke where needed.\n\nThere's an aspect of branding that is aesthetic such as the use of refined and memorable typography, the expressive use of color, or the visual language of iconography. I'll share practical guidance as these relate to iOS apps.\n\nBut branding is also a feeling it's an extension of how you see a product that bleeds into how you experience a product. This is less tangible, but in many ways, more meaningful. It's possible your product has a website, digital marketing, a retail store, or even an app on another platform. And you might be tempted to make your brand identical across all of these experiences. But each of these placements should consider the context. They shouldn't default to being the same.\n\nPeople who use iPhone's expect the apps on their phone to look and feel like iOS. This is comprised of components, patterns, and functionality that has been meticulously refined for the iPhone.\n\nAnd people don't typically have experience with other versions of your app outside of Apple's ecosystem. The tips I'll share today are centered around how branding can be of service to your app experience. This doesn't mean your app needs to look identical to an Apple app. There are a lot of ways to refine your brand within the bounds of the operating system.\n\nI'll share some great examples from developers who strike the balance of distinct branding without compromising the native feel.\n\nFirst, you'll learn how the UI and components of your app can be customized, while still feeling like they belong on the operating system. I'll cover ways to make your content shine, the appropriate places to use color in your interface, considerations when using custom fonts, and examples of great iconography and resources you can use.\n\nLet's set the foundation for how to think about where brand belongs.\n\nWith the introduction of Liquid Glass in iOS 26, we started thinking about interfaces in a slightly different way.\n\nThink of your app as two distinct layers: the UI layer, which serves as the global navigation, and the content layer, which sits beneath these controls and contains all the features that make your app unique. Conceptually, the content layer is the best opportunity to express your brand identity. This allows the UI layer of your app to act as a foundation that helps people get around and find what they're looking for.\n\nThe UI Layer is an app's navigation and actions. Expressed through components like tab bars and top toolbars they stay floating above the content layer for easy access.\n\nTry to lean into what's already familiar for people on iOS, rather then reinvent the wheel.\n\nGentler Streak is an app that keeps you motivated and helps you build a fitness routine. At a glance you can tell the app has a distinct identity: playful illustrations and detailed data visualizations.\n\nBut their navigation: the tab bar and top toolbar actions, are all native. They don't heavily customize the UI or deviate from the patterns.\n\nEstablishing a baseline of platform familiarity is important. Otherwise people will need to learn how to use your app.\n\nStandard components such as grid views and grouped tables are highly flexible and functional. If you draw upon what people already understand, they'll instinctively know how to use your app.\n\nNow, it's expected that components will occasionally need to be customized to fit your app needs.\n\nThe messaging app Slack has built a custom top toolbar where a middle action shows Channel information. But the overall look of the component: like button sizes, placement of floating actions, and the popover behavior - all feel very iOS.\n\nAnother example that I love is from the app Moonlitt. The app tracks the lunar cycle for photography and moon phases. The UI is simple, it's a flat hierarchy and doesn't need a tab bar.\n\nThe lunar cycle calendar is a custom component that shows moon phases throughout the month at a glance. But it leverages the design language of iOS. With a Liquid Glass backing, a primary action to dismiss, and the sheet has concentric edges to match the hardware. This app is totally unique. And yet, clearly belongs on iOS.\n\nCustom components take refinement. So you should focus your time building them for areas of your app that have the biggest impact or make your content stand out.\n\nThen, audit your app for opportunities to use standard components for aspects of your app that are functional.\n\nFor example, beyond navigation, one standard component that's often overlooked is context menus. They're often displayed from a button in the top toolbar and contain actions related to the whole screen, as opposed to in-line. They provide convenient access to frequently used items, just a tap away.\n\nAnd they're super flexible. Your actions can have icons. They can be grouped into sections with optional headers. Or present a secondary menu or modal.\n\nMoonlitt uses a Context Menu for their settings. Notice the animation that's built into the control deliberately morphing from the action that was tapped. With SwiftUI, you get these components and interactions out of the box.\n\nThis is just one example where creating a custom component will require work to build and maintain. These types of custom elements, the ones that serve a very utilitarian purpose, don't usually reinforce a brand.\n\nIn fact, they can make the product appear less native - or even dated - because they feel misplaced, replicating something tried and true.\n\nWhen it comes to your UI, there are endless possibilities, but try to build on what people know. Use platform components for conventional tasks and customize components to cater to your specific needs.\n\nWith this perspective about your UI, think of the content layer as your canvas. This can include imagery, videos, even words it's the information your app provides to people.\n\nFor example, the Crumbl app uses full-bleed videos to highlight their weekly flavors. These video's aren't just generic assets, they help draw a deeper connection to the product because they change weekly. Incorporating content well is all about making sure it has a clear purpose. Moonlitt takes a different approach: their content is edge-to-edge color mimicking gradation in the night sky. The 3D elements dynamically portray your position in relation to the moon. It feels relevant for their content to feel immersive and take over the entire interface.\n\nBut, content can also be the words used throughout your messaging. And words are powerful because they can effect how we feel. I encourage you to explore how voice and tone can shape your brand.\n\nSometimes an app is targeting a specific emotion such as feeling playful and fun, or trustworthy and safe. Be very deliberate about how you want to make people feel.\n\nThere's a lot more to say here, so to learn about copy writing for your content, check out the video: \"Add personality to your app through UX writing\".\n\nKeep in mind, people don't experience apps as static screens. They're dynamic experiences as people scroll, tap, and interact. So, transitions and animations are the ways in which your content is experienced.\n\nOne example is the NYT Cooking app. In a recipe detail view, comments are an important part of the content. The app uses SwiftUI's Zoom Transitions for the comments related to a recipe. These kind of transitions are delightful and feel fluid, but they also improve interaction by connecting the tap target to the transition state.\n\nFor Gentler Streak, motion makes the app feel active, engaging, and approachable. As you scroll through a monthly recap page, your activities feel almost magical spring animations make the content really pop. Motion helps emphasize hierarchy and brings attention to what matters.\n\nBut delayed load times or dropped frames translate poorly to peoples perception of your app, even if they couldn't pinpoint exactly why.\n\nPeople remember how a product makes them feel design an experience that's satisfying, enriching and a joy to use.\n\nNow that you're mindful of how the foundation of your app should honor platform patterns, let's talk about color.\n\nBefore iOS 26, apps would often use solid backgrounds on the top toolbars and tab bars of their apps.\n\nBut these UI elements were bulky and letterboxed the content area, restricting it to an even smaller portion of the screen.\n\nWith the new design language we introduced, our recommendation is to move color into the content area of your app, into the scroll view.\n\nThat way, Liquid Glass controls sit above the content layer and pick up your brand color dynamically. The other thing to consider here is that color can be distracting and make an interface feel overwhelming.\n\nAim to use color in ways that create meaning such as portraying hierarchy, groupings, or indicating interaction. Which is why color is often seen on controls and actions. This is referred to as your app's accent or tint color.\n\nSlack uses color sparingly. They use tint for primary actions like: sections with new information, badges to show unread messages, new message creation, and the selected state in the tab bar. Intentional use of color communicates status, feedback, and selection states helping people focus on what's important.\n\nAnd they moved their solid top toolbar color into the content area so it scrolls away, allowing content to spread edge to edge.\n\nDon't be afraid to use color in your app! It can enhance communication, evoke your brand, and provide visual continuity.\n\nKeep in mind, the iPhone is an incredibly personal device. Features like Dark Mode are preferences people set for their comfort and needs. Gentler Streak, Slack, and NYT Cooking have a refined color palette for low-light environments. If your app doesn't support Dark Mode, people may have a negative experience that translates to your product.\n\nThere are also various touch points where your brand identity can extend beyond your app. For example, if people find the data in your app valuable, they can choose to use your Widget.\n\nHere's an example of Crumbl's Widget's branded with their pastel color palette and distinct imagery. They don't just look delicious, they're also immediately recognizable.\n\nWhen working with color, exercise restraint — use it sparingly and with intention so it has the biggest impact.\n\nOf all the tools at your disposal, typography is often a favorite. It can be expressive, bold, or elegant, but should always be functional.\n\nLet's revisit the Crumbl app. They've built their own Typeface, Crumbl Sans, that they use throughout their marketing and in moments of their iOS app that are memorable. This is particularly obvious with large headers like their cookie flavors.\n\nThe primary thing to be aware of when using custom fonts on iOS is how they scale.\n\nDynamic Type is the setting within Accessibility to increase font sizes across the operating system. It's important to think about this for people with vision and cognitive disabilities but Dynamic Type is also a preference, that many people rely on.\n\nThis is built into Apple's System Fonts, but you'll need to build support, and test for this, when you use your own Custom Fonts.\n\nNotice how when someone increases their system font size through Dynamic Type the Crumbl app is still legible. As the font size increases, rather than truncating the labels their layout accommodates them, dropping text to multiple lines as needed.\n\nDynamic type also applies to standard components. When the type is set to larger accessibility sizes, the tab labels and icons appear larger in the center of the screen.\n\nStrive to have your app accommodate as many people as possible. It might sound simple, but when people can clearly see the UI and read the text in your app, they're going to have a more positive experience.\n\nSan Francisco is the system font for all Apple platforms. It's a typeface that provides a consistent, legible, and friendly typographic voice supporting over 150 languages.\n\nSF Pro is the default, but there are other variants: SF Compact is optimized for small sizes, SF Mono is designed for row and column alignment which is great for coding, and New York is a Serif for traditional reading and a graphic display face.\n\nSome apps, like Gentler Streak, use system fonts entirely. With a mix of font widths, and variants like SF Rounded, they've achieved variety and hierarchy, while still feeling distinct.\n\nTypography is a strong way to express identity. Try to design the fonts in your interface to be flexible and adapt to people's needs.\n\nAnd finally, iconography. For the most part, you can use your own iconography anywhere throughout your app, from the content view to controls.\n\nAn example of custom iconography that works really well is the NYT Cooking app. Their icons have sharper edges and typically use a line-weight variant. I like that they're unique to their app, but not overly detailed, so they scale well to small sizes.\n\nTheir icons are used on their tab bar, top toolbars, and in-line actions in the content layer. They're consistent, cohesive, and a simple nod to their brand.\n\nSomething I respect about their iconography is platform consideration. Here are three different versions of the Share icon across iOS, Android and Web. Even though they've created their own style of icons each of these actions stays true to the platform pattern of sharing.\n\nIconography doesn't need to be heavily stylized. It should aim to be identifiable and serve a clear purpose.\n\nAnd… not every app needs custom iconography. SF Symbols is a Mac app with over 7,000 symbols that you can use for free.\n\nSF Symbols are built like a font, so they scale dynamically like text. And they're designed to be neutral the style of SF Symbols is intended to work for all types of apps across Apple platforms. They have various line weights, accessibility and localization support.\n\nBest of all? They're built right into Xcode, so no need for design teams to export icon libraries and manage file handoff.\n\nOn the topic of icons, let's touch briefly on logo's. In the context of iOS, people don't need to be reminded which app they're using. And logo's can take up precious real estate best reserved for more pertinent information.\n\nIn NYT Cooking their logo is only displayed on the Home tab, and it fades on scroll.\n\nThere's something really elegant about how understated this is. Aim to incorporate branding like this in refined and unobtrusive ways that don't distract people from your experience.\n\nIf you choose to use your own iconography, make sure they're recognizable, consistent and that they scale well to small sizes.\n\nAll of the apps I talked about today, have approached branding in a way that complements the experience, rather than distracts from it. They have predictable navigation patterns that help people quickly understand how to use their app. But they've integrated their own identity in ways that are subtle, but meaningful.\n\nWe encourage exploration of your brand throughout your iOS apps. Just remember that iOS is a platform with established interactions and forcing your brand can compromise the user experience. Be mindful of where it oversteps with system behavior or confuses familiar conventions. We're delighted by the creative ways people inject their brand identity, so continue to exercise your creativity.",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Human Interface Guidelines",
+ "url": "https://developer.apple.com/design/human-interface-guidelines"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/251/4/52fb4c75-99ba-419f-90d6-bfef374ac966/downloads/wwdc2026-251_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/251/4/52fb4c75-99ba-419f-90d6-bfef374ac966/downloads/wwdc2026-251_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:15.332Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-252.json b/data/wwdc/videos/2026-252.json
new file mode 100644
index 0000000..3a04a62
--- /dev/null
+++ b/data/wwdc/videos/2026-252.json
@@ -0,0 +1,60 @@
+{
+ "id": "252",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/252/",
+ "title": "Design no-code games with Reality Composer Pro 3",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Design",
+ "Developer Tools",
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, my name is Saschka Unseld. And I am a Creative Director at Apple.\n\nWhenever I work on an idea it is really important to me that I can quickly mock it up and iterate on it again… and again… and again. Reality Composer Pro 3 lets me do just that without writing a single line of code.\n\nIn this session I'll share how I build a RealityKit game from scratch in Reality Composer Pro 3's Script Graph.\n\nIf you want to get the basics first, there's a Reality Composer Pro 3 session just for you. But if you're creative and find yourself with an idea you want to quickly mock up, this is the session for you.\n\nI'll explain what a Script Graph is. I'll show how I build my game. And as I flesh it out.. I'll dive deeper into some more advanced techniques.\n\nOk, Script Graph, what is it? It is Reality Composer Pro's Node-Based Visual Scripting.\n\nA Script Graph lets me build games through Event Driven logic. What does that mean? It means you can create games like this. Here I listen to Pinch Events to animate these leaves to open and close.\n\nI listen to Drag Gesture Events to let the player drag this nut around. And I create custom events that tell the game to scroll the level when the character reaches certain waypoints.\n\nBut my favorite part of Script Graph is that I can directly test them and iterate on them in Reality Composer Pro and on my Vision Pro.\n\nWhen I create something new, it always starts with a wish that something might exist and then I work on it until it does.\n\nThe wish for this game started with something small. It started with… a little Squirrel. It is blissfully asleep because it found a nut. But the Squirrel forgot that night was about to fall. So I wished that the player could help steal its nut but just to guide it back home. And what better platform is there to steal a nut from a squirrel than Vision Pro.\n\nThe player will literally be able to reach out with their hand, steal the nut from the Squirrel and freely drag it around to help guide my little friend back home.\n\nLet's start to build the game. I want to allow the player to be able to pick up the Squirrel's nut and move it around. I already created the scene with my Squirrel and Nut Entity.\n\nI decided on a cut-out look so the Squirrel and the Nut are just simple planes with textures on them.\n\nTo make my nut draggable I add a few components to it. To make it a gaze target I add an Input Target Component. To define the size of the gaze target I add a Collision Component. And since I also want it to highlight when the player gazes at it, I add a Hover Effect Component.\n\nNow I'm ready to create my first Script Graph.\n\nI right click in my Project Browser, select New, and then Script Graph.\n\nThis Script Graph is where I will create all my logic.\n\nLet's call it dragNut.\n\nI want the logic of my Script Graph to run on my Nut Entity so I add a Scripting Component to it and in the Inspector assign my dragNut Script Graph.\n\nWonderful.\n\nLet's open my Script Graph and start to build some logic. Any logic that a Script Graph runs starts with an event that triggers the logic.\n\nThere are many nodes that can listen to a whole range of events. In my case I want to listen to the event of the player trying to pinch and drag the nut. For this I utilize the \"On Drag\" Event node. Now all I need to do is use the info I get from the On Drag node and apply it to the transform of my Nut Entity.\n\nFor this I use a Set node. Set nodes are nodes that allow me to write data into components and loads of other things.\n\nIn my case I want to adjust the position of the Nut's transform component so I use a Set Transform node. Now I just need to connect these two. First I connect the On Drag Event to trigger the Set Transform node. This means the Set Transform gets triggered every time the On Drag Event is triggered. Next I connect the Scene Location that I get from the On Drag node to the Set Transform's translation input. This way the position of my Nut will be set based on the drag transform I get from the drag gesture. And that's it. To me fast testing and iteration is key to all creative workflows. And I can do this right in Reality Composer Pro 3. I press the Play button at the top of my workspace and it allows me to test my logic right here in the viewport.\n\nI can click and drag the Nut around just as I defined in my Script Graph. But let's go one step further with Live preview, a feature that will be available later this year. To truly know how an interaction feels I want to try it on my Vision Pro.\n\nI switch the Simulation Mode to \"Preview on Device\", select my Vision Pro, and press Play.\n\nAnd Tada! My Squirrel and its Nut appear right next to me. I can move the app around to place it nicely and I can press Play. And now, I can look at the Nut, and tap to drag it. Hmmm... I feel like I have to move my hand too much to drag the Nut around. To do adjustments like this, I love to bring up my Mac Virtual Display so I can directly see my adjustments.\n\nThere's tons of nodes that allow me to apply math and logic to my Script Graphs. To fix the jumping at the start and to get the Nut to be more responsive to my drag I'll use the On Drag's Scene Translation with a \"Multiply by Number\" node. I know I will want to noodle around with the multiplication factor. To do this I'll create an Input variable. I add one in the Inspector of my Script Graph.\n\nLet's call it dragSpeed, make it of type number, set it to public, and give it a default 1.3.\n\nThen I add an Input node and plug the dragSpeed variable into the multiply node.\n\nSince I set dragSpeed to be a public variable, it shows up in my Nut's Scripting Component. Which means that I can, while testing it all on my Vision Pro, adjust my dragSpeed to find just the right value.\n\n1.5? Nah… too much… Maybe 1.1… Nearly there… 1.15...\n\nYeah. I think that's it.\n\nOne more thing… When I changed the dragSpeed value, its name shifted to be shown in bold. This means my dragSpeed value is not applied to the script itself, but as an Override. Overrides are unique variations of a variable that are unique to each Scripting Component. This means I can have multiple Nuts in my scene and, while they share the same Script Graph logic, can use different dragSpeed settings.\n\nI love to iterate like this on how my interactions feel. But to be honest, that movement still felt a bit bland. I want the Nut to feel more dynamic and most of all I want to be able to toss it around.\n\nTo achieve this, let's add some physics. First I need to make the Nut be part of the physics simulation. So I add a Physics Body Component to it.\n\nNow I need to change my Script Graph so that my drag gesture drives the Nut's physics.\n\nI can do this with the Add Force node.\n\nThis node adds a force to the Nut's physics simulation which makes the Nut move in the direction of the force.\n\nBut this is an additive force, so I need to know how much my drag changes over time. This is not data I get by default from the drag gesture. So let's add it.\n\nInstead of directly applying the drag's translation, I am going to store it. To do this I created a variable called targetPosition and then use a Set Variable node to store the drag's translation into it.\n\nNext I want to calculate how much the drag has changed. All I need to do is subtract the position I got previously from the position I got currently. I wired up this logic to calculate this change and store it in a variable I called dragDelta.\n\nNow all I need to do is trigger the Add Force node, wire dragDelta via a little multiplication to give it more weight into the Add Force node.\n\nLet's try it out to see how it behaves.\n\nWell, I love that it feels more physical, and that I can toss the Nut around and it falls down. But it's kinda hard to lift it up. That's cause gravity is a constant drag in life, it pulls you down. So let's turn it off while I hold the Nut.\n\nTo do this I will use another Set node, the Set PhysicsBodyComponent node. This node allows me to change the settings of my PhysicsBodyComponent dynamically.\n\nIn my case I want to not have the Nut be affected by gravity while it is dragged. And I want it to be less finicky when dragging it. To do that I'll raise its linear damping value. This will add more friction and will make it slow down faster.\n\nI added some logic to my Script Graph that triggers this change in the PhysicsBodyComponent when the Nut gets picked up and when it gets dropped. Let's test it out.\n\nOh, perfect, this feels so much nicer. And that's the basics of Script Graph. They listen to event nodes, do some logic with the data they get from them, and then utilize set nodes to modify components. But with that basic idea I can do so much.\n\nLet's dive into some more advanced techniques. Like a lot of things in life, over time things have a tendency to get complicated.\n\nAnd there's one thing I love nearly as much as creative work. It's getting things organized.\n\nThat's where Prototyped Subgraphs come in.\n\nThey can not only help me clean up my logic but also create reusable logic.\n\nLet's look at this piece of Script Graph I built earlier.\n\nAll this does is that it checks if the isEnd bool of my Drag Event has just changed and if it does, trigger some logic.\n\nBut when I look at it I'm like… Whaaaaaat does this do again? So let's clean it up with the Subgraph.\n\nI select all the nodes that make up this part of my logic, right click, and select Compose Subgraph.\n\nI'll call this one Check for Change.\n\nAhhhh… This looks so much less complicated already. Wait a second… Triggering things when a bool variable changes is something I need all the time.\n\nThat's where Prototyped Subgraphs come in. They allow me to reuse my Subgraphs in all my Scripts.\n\nAnd making one is easy as pie. I just right click and select Convert to Prototyped Subgraph... It will show up in my asset browser.\n\nAnd from now on I see my Subgraph alongside all the other nodes in the add node menu.\n\nBuilding reusable pieces of logic like this allows me to speed up my workflow and keep my logic streamlined.\n\nOk, back to my Squirrel.\n\nRight now, if I steal the Squirrel's Nut, the Squirrel doesn't react at all. That's doesn't feel right. Let's fix it. I want the Squirrel to look at the Nut when I drag it around. To do this I'll give the Squirrel its own Script Graph.\n\nBut how will it know that the Nut is being dragged around? I can create my own Custom Event for it. And for that, I need a Custom Node Library.\n\nI can create one right here in my Project Browser. And in it, add a Custom Event. Let's call it \"nutIsDragged\".\n\nIn order for the Squirrel to know where the Nut is I want to send the Nut's position with the Event. For that I add a property to my Custom Event. Let's call it nutPosition.\n\nThe only thing I have to do now is click Sync Nodes so my custom node is being made available.\n\nWhat I want now is to have the Script Graph on my Nut send this Event to a Script Graph on my Squirrel.\n\nSince I added it to my Node Library, the Send \"nutIsDragged\" node is available alongside all the other nodes. I wire it up to be triggered when the Nut is dragged and pass along the Nut's world position.\n\nAnd in the Squirrel's Script Graph I create an On \"nutIsDragged\" Event so I can listen to it.\n\nThen I use the nutPosition of the Event to drive the Squirrel's rotation.\n\nLet me check how this feels in my Vision Pro.\n\nWonderful. The logic I added gives the Squirrel a nice snappy flip when looking at the Nut.\n\nThis underscores the game's cut-out design and just makes it more fun.\n\nOh and yea, I snuck in another small thing. When I steal the Nut, the Squirrel now looks appropriately upset.\n\nIt was an easy addition. Where I drive a change in the Squirrel's Material from my Script Graph.\n\nIn the Shader Graph of my Squirrel's Material I use a public input variable I called isNutDragged.\n\nAnd I use it to decide which one of my two Squirrel textures to use in the Material.\n\nIn my Squirrel's Script Graph I added a Set Material Parameter node, set the Parameter to type Bool, and called it isNutDragged.\n\nI then told it where to find the Entity that has the Squirrel's Model Component, made sure it gets triggered, and told if the Nut is dragged or not dragged. And that was it.\n\nTo really have my Squirrel express its disagreement though I want to give it a voice so it can properly complain. Sticking with my cut-out style, I want that voice to be a Speech Bubble.\n\nI love using SwiftUI for interfaces like this, it makes it effortless and they look beautiful. But to use SwiftUI I need to run my game via Xcode.\n\nEasy… I just switch preview mode to \"Run with Xcode\". But wait… I don't have an Xcode project… No problem. Reality Composer Pro 3 can just create one for me.\n\nOver in Xcode I made this little SwiftUI Speech Bubble.\n\nBut how do I get it to pop up when I steal the Squirrel's Nut? Again… Script Graph Events are there for me. Because they can also be listen to and send from Swift.\n\nAll I need to do in my Script Graph is use a Send Scene Event node, let's call it squirrelTalk, and since I want to send over what the Squirrel should say, I add a variable to it.\n\nCall it sayThis, set it to type String, and set it to what I want Squirrel to say. For example: \"Hey, that's my nut!\" Then in Xcode I needed to write some code. As a designer, I love that I can now simply prompt Coding Intelligence to write this code for me.\n\nTo listen to my Scene Event all I needed to do was tell my coding assistant to subscribe to my Scene Event called squirrelTalk and to store its sayThis variable.\n\nAnd then, when squirrelTalk is called, show my SwiftUI Speech Bubble as an Attachment over my Squirrel Entity and of course use the sayThis variable as its text.\n\nNow my Squirrel can really let me know what it thinks of me when I steal its nut.\n\nThere's so many more things Script Graphs allow me to add to my game, like letting the Squirrel walk and jump to get its nut back and creating draggable leaves that allow my Squirrel to traverse through a whole level.\n\nOr even a visionOS ornament that, if I get stuck, I can use to jump to any place in the level. So don't worry my little Squirrel. I'll get you back home.\n\nI love using Script Graphs like this. They help me sketch out my ideas, noodle around on how to best steal nuts, and most of all, allow me to bring my wish to life. If you want to keep going, download Reality Composer Pro 3.\n\nTake a deep dive into advanced workflows or check out the full Squirrel Sample Project from the Apple Developer Website.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "17:23",
+ "title": "Squirrel Talk",
+ "language": "swift",
+ "code": "// Advanced techniques\n\nif let scene = entity.scene {\n scene.subscribe(forEventName: \"squirrelTalk\", on: { event in\n if let sayThis: String = try? event.value(\"sayThis\") {\n self.sayThis = sayThis\n }\n } ).store(in: &cancellables)\n}\n\n...\n} attachments: {\n Attachment(id: \"squirrelTalk\") {\n SquirrelTalkAttachmentView(text: sayThis)\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/252/6/572c2388-69f6-4e57-9eba-c71b65f5f6ed/downloads/wwdc2026-252_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/252/6/572c2388-69f6-4e57-9eba-c71b65f5f6ed/downloads/wwdc2026-252_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "279",
+ "year": "2026",
+ "title": "Explore advances in RealityKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/279"
+ },
+ {
+ "id": "281",
+ "year": "2026",
+ "title": "Extend Reality Composer Pro 3 functionality with Xcode",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/281"
+ },
+ {
+ "id": "280",
+ "year": "2026",
+ "title": "Iterate your spatial scenes faster with Reality Composer Pro 3",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/280"
+ },
+ {
+ "id": "393",
+ "year": "2026",
+ "title": "Supercharge your spatial workflows with Reality Composer Pro 3",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/393"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:15.506Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-253.json b/data/wwdc/videos/2026-253.json
new file mode 100644
index 0000000..ce7e6a8
--- /dev/null
+++ b/data/wwdc/videos/2026-253.json
@@ -0,0 +1,142 @@
+{
+ "id": "253",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/253/",
+ "title": "Meet the Music Understanding framework",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Audio & Video"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi. I'm Conner from the Computational Music Team. And I'm excited to introduce you to a framework called Music Understanding. It gives you access to on-device musical intelligence across all Apple platforms. It handles all the signal processing and model inference for you so you don't need any expertise in signal processing or machine learning to use it. And because it runs entirely on-device, the audio you analyze stays private and works offline.\n\nAt Apple, The Final Cut Pro team used the Music Understanding framework to power two features of their app.\n\nIn the beat detection feature, Final Cut Pro analyzes a song for its rhythm and structure to reveal its beat grid.\n\nThis helps editors visualize and align their edits to song parts, bars, and beats.\n\nAnd in Final Cut Pro for iPad the montage feature analyzes for rhythm, pace, and structure to automatically synchronize clips to the music.\n\nI'll start by going over what the framework can do. Then, I'll follow that up by explaining how you can use the framework. Finally, I'll go through the API and show how it was used to build a sample app for understanding music.\n\nThe framework provides analysis around six main areas: key, rhythm, structure, pace, instrument activity, and loudness.\n\nRhythm is the pulse of a song, driven by individual beats. These beats build into bars.\n\nThe number of beats in one minute is called beats per minute or bpm.\n\nBars form phrases, which you can think of as musical sentences.\n\nPhrases combine into segments, creating a more complete musical statement...\n\nand those segments ultimately build the sections. You can think of a section as a chorus, verse, intro or bridge.\n\nDuring a song, instruments such as a drum, bass, or vocals may be playing at different times and at different intensities. These instruments play around a common set of notes called the key.\n\nWhile the song may have a consistent pulse or bpm, different parts of the song may feel slower or faster. This is called pace.\n\nOver time the song may sound louder at some points than others.\n\nThese are the building blocks of the Music Understanding framework, and by integrating it in your app, you unlock a whole new level of possibilities. Next, I'll talk about how to use the framework. At a high level, apps interact with a MusicUnderstandingSession, initializing with either an AVAsset or a custom audio provider. To start analysis, clients call analyze and await results.\n\nBy default, the framework analyzes for all analysis types. For the highest performance, you can specify which analysis types you are interested in to avoid unnecessary computations.\n\nTo explore the framework more deeply, I'll review a sample app called Music Understanding Lab, available on developer.apple.com. Let me show you how Music Understanding Lab works. First, I'll select a song on the device.\n\nThe app uses the Music Understanding framework to analyze the audio, turning it into a visual experience with a dedicated tile for each result. When I hit play, notice, the Rhythm and Structure tiles update as the song plays. The playhead ties the experience together, letting you follow along with the music.\n\nI'll start by talking about how the Select Song... button is implemented.\n\nUsing the SwiftUI fileImporter, I'll select a file to get its URL.\n\nThen I'll use that URL to create an AVURLAsset. Be sure to set PreferPreciseDurationAndTimingKey to true to ensure the most accurate results. Next, I'll create the session from the asset and call analyze and await the return of the session results. Inside the SessionResult struct, every feature Music Understanding analyzes gets its own results field. These are all optionals. When you use the general analyze() API, all results will be available. However, if you use the targeted analyze(for:) API, the framework will only return the results you asked for, and the rest will be nil.\n\nThroughout the Music Understanding framework, there are two standard types used to associate time with a value. A TimedValue associates a value with a CMTime. Similar to TimedValue, a RangedValue associates a CMTimeRange with a value. With these time-based types in mind, I'll discuss the features Music Understanding analyzes by showing how they are used in the Music Understanding Lab UI. First I'm going to start with the Key tile. In this song the musical key is D flat major.\n\nFor key analysis, the Music Understanding framework returns a KeyResult struct.\n\nThe result contains an array of ranges, mapping a KeySignature to a specific time range using a RangedValue. A KeySignature contains a tonic and a mode.\n\nA tonic can be any of the standard chromatic pitches. It represents the root note, like C or G, around which the song is built... and the mode, which is either major or minor.\n\nNext to Key is the Rhythm tile. The tile displays bpm on the left and indicators on the right that light up as each beat plays.\n\nWhen you analyze for rhythm, you get back a RhythmResult.\n\nIn this struct, Music Understanding gives you the timestamps for every beat and bar as arrays of CMTime. The framework also provides the overall global tempo with beatsPerMinute. Notice bpm is optional.\n\nThat's because if the framework hasn't processed enough audio to find at least two beats, the bpm will be set to nil. Now, I'm going to talk about the Structure tile.\n\nIn the tile there are 3 rows of rectangles that indicate a song's structural hierarchy.\n\nEach rectangle is a time range in the song. Music Understanding supports three levels of structure: sections, segments and phrases. The top row represents sections of a song. Each block shows the time range of a section.\n\nEach section is made up of one or more segments, which appear below the section rectangles, and each segment is made up of phrases. During playback the current section, segment and phrase appear highlighted.\n\nWhen you request structure analysis, the framework returns a StructureResult. It has three properties for sections, segments and phrases. For each of these, you get an array of CMTimeRanges.\n\nThe next tile is Pace. It tells you how fast the music feels to the listener. Parts of a song that feel faster or more energetic will have a higher value compared to slower or less energetic parts. In this UI, taller bars represent higher energy, while shorter bars represent lower energy.\n\nWhen you request pace analysis, you get back a PaceResult.\n\nThis struct has a single property containing an array of ranged values.\n\nNext, I'll talk about the instrument activity tiles.\n\nMusic Understanding Lab displays several tiles that visualize instrument activity. Either as time ranges, where color-coded bars indicate the active instruments present or as a detailed activity graph. The graph plots values between 0 and 1 representing the strength of each instrument.\n\nThe closer the value is to 1, the louder the instrument is in the mix.\n\nWhen you request instrument activity, the framework returns an InstrumentActivityResult. It has two properties, one for ranges and one for activity.\n\nThe Ranges API provides a dictionary, mapping each Instrument to an array of CMTimeRanges. This is great for situations where you just want to know if an instrument is present or not. But sometimes you need more detail and activity provides that.\n\nActivity maps an instrument to a TimedValue of Floats. The activity results express how intensely an instrument is playing over time, and is a great source to drive audio-reactive animations.\n\nThe Loudness tile appears below instrument activity.\n\nThe framework provides measurements in Loudness Units Full Scale, or LUFS, which is the industry standard for modeling how the human ear perceives volume.\n\nAt the top of the tile is a single Integrated Loudness value, which gives the average loudness for the whole song.\n\nBelow that is a graph of momentary loudness, showing how loudness varies over time.\n\nThe framework also provides a peak value, which describes the absolute highest audio volume in decibels.\n\nWhen you request loudness analysis, the framework returns a LoudnessResult struct.\n\nMusic Understanding supports integrated, momentary and shortTerm loudness. Integrated provides a single value that represents the overall loudness of the audio. Momentary and shortTerm provide time-stamped values every 100 milliseconds. Momentary values are calculated over a window of 400 milliseconds. They are useful to detect short, sudden spikes in loudness. ShortTerm values are calculated over a window of 3 seconds, which provides a smoother view of the loudness trend over time. The peak value tells you exactly where the track hits its maximum level and is measured in decibels. MusicUnderstandingSession also provides a streaming API for loudness. Values are delivered via an AsyncSequence for every 100ms of audio analyzed by the framework.\n\nHere's an example of how it can be used. I'll initialize the session as I did previously but this time I'll set up two tasks: one to consume the loudness results as they are delivered, and another to begin the analysis.\n\nNow I'll talk about the AudioProvider in this example.\n\nAn AudioProvider conforms to AsyncSequence and yields AVReadOnlyAudioPCMBuffer objects. When the AudioProvider has sent all audio buffers, it must send a final nil to signal completion.\n\nI'd like to focus now on the two icons at the top of Music Understanding Lab.\n\nAt the far right there is a Share button.\n\nWhen you tap it, a JSON file is exported with all analysis data.\n\nAll MusicUnderstanding results are codable. To encode to JSON, just create a JSONEncoder and encode the session results. A button with a filmstrip icon appears next to the export button.\n\nWhen tapped, it opens the Video tile. The Video tile uses structure and pace to create a video synced to the music. I'll talk about how it works. The Video tile displays a series of video clips that match the feeling of the music.\n\nThe algorithm starts by identifying the song section time ranges.\n\nThen it uses the pace of each section to determine how many clips to display in that time range.\n\nSince pace is an event per minute rate, it can be divided by 60 seconds to determine the time per clip. This produces clips that always begin at the start of each section, and the clips within a section match the energy of the song.\n\nI then used this timing information to construct a video synced to the music. The video clips are retimed to match the target clip duration with longer, slower clips during the less energetic parts, and shorter, faster clips during the more energetic parts.\n\nThis overview gives you a sense of how these APIs work together in practice.\n\nWith these basics covered, you are ready to dive in! With Music Understanding, you can sync visuals to the beat, loudness, or pace of a song to build powerful video editing features, organize music catalogs by tempo or key to drive a dj app or pre-compute and bundle analysis data to animate your game to music. Check out the Music Understanding Framework documentation on developer.apple.com, and download \"Music Understanding Lab\" to help kickstart development of your own applications.\n\nThis technology was built to solve real challenges here at Apple. The ideas were there but the tools to build them were not. These tools are now in your hands. Make something awesome. Thanks for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "4:47",
+ "title": "Initialize the session",
+ "language": "swift",
+ "code": "import MusicUnderstanding\n\n.fileImporter(isPresented: $isPresented, allowedContentTypes: [.audio]) { result in\n switch result {\n case .success(let url):\n let asset = AVURLAsset(url: url, \n options: [AVURLAssetPreferPreciseDurationAndTimingKey : true])\n let session = try await MusicUnderstandingSession(asset: asset)\n let results = try await session.analyze()\n }\n}"
+ },
+ {
+ "timestamp": "5:24",
+ "title": "Inside SessionResult",
+ "language": "swift",
+ "code": "import MusicUnderstanding\n\npublic struct SessionResult: Codable, Sendable {\n public let instrumentActivity: InstrumentActivityResult?\n public let key: KeyResult?\n public let loudness: LoudnessResult?\n public let pace: PaceResult?\n public let rhythm: RhythmResult?\n public let structure: StructureResult?\n}"
+ },
+ {
+ "timestamp": "5:53",
+ "title": "TimedValue",
+ "language": "swift",
+ "code": "import MusicUnderstanding\n\npublic struct TimedValue: Codable, Equatable, Sendable\nwhere Value: Codable & Equatable & Sendable {\n public let time: CMTime\n public let value: Value\n}"
+ },
+ {
+ "timestamp": "5:58",
+ "title": "RangedValue",
+ "language": "swift",
+ "code": "import MusicUnderstanding\n\npublic struct RangedValue: Codable, Equatable, Sendable\nwhere Value: Codable & Equatable & Sendable {\n public let range: CMTimeRange\n public let value: Value\n}"
+ },
+ {
+ "timestamp": "6:27",
+ "title": "Key analysis",
+ "language": "swift",
+ "code": "public struct KeyResult: Codable, Sendable {\n public let ranges: [MusicUnderstandingSession.RangedValue]\n}"
+ },
+ {
+ "timestamp": "10:13",
+ "title": "InstrumentActivityResult",
+ "language": "swift",
+ "code": "import MusicUnderstanding\n\npublic struct InstrumentActivityResult: Codable, Sendable {\n public let ranges: [Instrument: [CMTimeRange]]\n public let activity: [Instrument: [MusicUnderstandingSession.TimedValue]]\n}"
+ },
+ {
+ "timestamp": "11:45",
+ "title": "LoudnessResult",
+ "language": "swift",
+ "code": "import MusicUnderstanding\n\npublic struct LoudnessResult: Codable, Sendable {\n public let integrated: MusicUnderstandingSession.TimedValue\n public let momentary: [MusicUnderstandingSession.TimedValue]\n public let shortTerm: [MusicUnderstandingSession.TimedValue]\n public let peak: MusicUnderstandingSession.TimedValue\n}"
+ },
+ {
+ "timestamp": "12:48",
+ "title": "Streaming API for loudness",
+ "language": "swift",
+ "code": "import MusicUnderstanding\n\npublic var loudnessResults: some AsyncSequence & Sendable"
+ },
+ {
+ "timestamp": "12:55",
+ "title": "Streaming API for loudness",
+ "language": "swift",
+ "code": "import MusicUnderstanding\n\nlet audioProvider = AudioProvider()\nlet session = MusicUnderstandingSession(audioProvider: audioProvider)\nawait withThrowingTaskGroup(of: Void.self) { taskGroup in\n group.addTask {\n for try await result in await session.loudnessResults {\n updateAudioLevel(result.momentary.value)\n }\n }\n\n group.addTask {\n try await session.analyze(for: [.loudness])\n }\n}"
+ },
+ {
+ "timestamp": "13:19",
+ "title": "Audio Provider",
+ "language": "swift",
+ "code": "import MusicUnderstanding\n\nstruct AudioProvider: AsyncSequence, AsyncIteratorProtocol {\n func makeAsyncIterator() -> Self {\n return self\n }\n\n mutating func next() async -> AVReadOnlyAudioPCMBuffer? {\n // Return the next audio buffer, or nil to signal completion\n }\n}"
+ },
+ {
+ "timestamp": "13:55",
+ "title": "Encode to JSON",
+ "language": "swift",
+ "code": "import MusicUnderstanding\n\nlet session = try await MusicUnderstandingSession(asset: asset)\nlet results = try await session.analyze()\n\nlet encoder = JSONEncoder()\ntry encoder.encode(results)"
+ },
+ {
+ "timestamp": "14:47",
+ "title": "Suggestion for using pace",
+ "language": "swift",
+ "code": "let timePerClip = 60 / paceValue"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Creating visuals with Music Understanding analysis results",
+ "url": "https://developer.apple.com/documentation/MusicUnderstanding/create-visuals-using-musicunderstanding-analysis-results"
+ },
+ {
+ "title": "Music Understanding",
+ "url": "https://developer.apple.com/documentation/MusicUnderstanding"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/253/5/db1c3715-aaaf-42db-8e9e-66d2a0011430/downloads/wwdc2026-253_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/253/5/db1c3715-aaaf-42db-8e9e-66d2a0011430/downloads/wwdc2026-253_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:15.588Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-254.json b/data/wwdc/videos/2026-254.json
new file mode 100644
index 0000000..8b15290
--- /dev/null
+++ b/data/wwdc/videos/2026-254.json
@@ -0,0 +1,107 @@
+{
+ "id": "254",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/254/",
+ "title": "Integrate MusicKit into your app",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Audio & Video",
+ "Swift"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, and welcome to WWDC 2026. I'm Cathy, an engineer on the MusicKit team! Today, my teammate Alan and I want to demonstrate how to integrate MusicKit into an app.\n\nMusicKit is a Swift framework for Apple platforms that offers a set of APIs for your apps to access and play music. Designed with Swift concurrency and SwiftUI in mind, MusicKit streamlines integration with Apple Music. You can build rich, music enhanced experiences, so people can browse and play the Apple Music catalog and a person's media library straight from an app. Alan and I will cover many key MusicKit concepts today while enhancing the workout app he and I made. First, I'll explain how to configure Xcode and handle music access. Then, I will cover what a MusicKit music item is and how I can select one.\n\nAlan will build on my music selection work and prepare selected songs for playback. And lastly, Alan will dive into music catalog requests to further customize the app he and I made. Now, I want to go through the flow of the workout app before diving into MusicKit integration. To follow along, you can find the completed sample code at the developer documentation site.\n\nI'll run the app and go through the current flow. When I start a bike workout, a screen pops up with a stopwatch, along with a button to end my session.\n\nThis is a good start, but I'd like to pick music to play during my workouts, so I want to integrate MusicKit! Before I start to code, I need to configure some settings as part of my project setup. One of those settings is registering for a developer token on the developer portal, which is needed to make MusicKit requests. Once you register for a developer token, it's generated on your behalf automatically.\n\nTo enable automatic token generation, I'll navigate to the page where I register my App ID. I need to make sure the MusicKit checkbox is checked in the App Services tab.\n\nThe tokens are associated with my developer account, so I'll want to verify that I'm logged into that same account in Xcode. Now, I'll return back to the app! I'd like to pick music to play during my workout, but before I can pick music, I have to give permission for MusicKit to access my music content. To request authorization, I will use MusicKit's MusicAuthorization request method, which is an asynchronous method that returns whether the app's music access is approved. MusicKit will then prompt the person with a permissions alert. I have the ability to configure the description of this alert with more context for how my app will use the access, which I can do in my project settings.\n\nTo provide a reason to access music content, I'll navigate to the Signing & Capabilities tab of my Xcode project, and add the Media Library capability. In the text box, I can describe how my app intends to use the person's music library. This description will appear at the bottom of the permissions alert when you request authorization. If the person isn't subscribed but wants to listen to content in the Apple Music catalog, I want to give them a way to subscribe. An Apple Music subscription is not required to use MusicKit, but the app will only be able to access purchased or synced music without one. If there isn't an active subscription, I can use a MusicKit subscription offer view modifier to give an opportunity to subscribe to Apple Music, without leaving my app. The .musicSubscriptionOffer is a view modifier that accepts an isPresented binding parameter, which changes in this view when the button is tapped.\n\nWhen presented, the subscription offer UI gives people steps to quickly sign up for an Apple Music subscription. As a developer, you have the potential to earn commissions when someone subscribes to Apple Music through your app, as part of the Apple Services Performance Partner Program. You can specify your information for this program in a MusicSubscriptionOffer.Options structure, and pass it into the view modifier. The options struct also allows you to set a message identifier, which changes what UI is presented. Since my ultimate goal is to play music, I'll set the messageIdentifier for my options as .playMusic.\n\nYou can customize the message identifier to have different UI treatments for your use case.\n\nIn the main view, I need to declare a @State property representing the subscription status. I only want to have the subscription button if the person isn't subscribed, and has the potential to become subscribed. To update the subscription status, I can add a .task that runs when authorization is granted. In here, I'll grab the current value, as well as listen for any updates that might occur and set the value accordingly.\n\nNow that I'm authorized and subscribed, I can use my subscription to play music from the Apple Music catalog during my workout! First, I need to pick a song to play. Specifically, I am going to pick a MusicKit song object. MusicItems are the building blocks for using MusicKit APIs, so I'll begin with explaining those. Then, I'll explain how to pick music items using the music picker. I'll dive into music items first. An Album music item, for example, is a value type in MusicKit's model layer.\n\nEach music item has Attributes, which are simple built-in properties. An Album object, for example, has attributes that describe the title of the album, or what the album's contentRating is.\n\nMusic items also have Relationships which describe related content, like an Album's tracks, another MusicKit music item type.\n\nAssociations describe a type's related content as well, but associations generally have weaker ties to the type than a relationship has. One Album association is the otherVersions, which is a collection of other albums.\n\nSo far I've focused on Album but there are many other MusicKit music item types, like Genres, Stations, and Playlists. Now that I've covered what music items are, it's time to start using them! To pick music to listen to for my workout, I can utilize the music picker, which surfaces both the Apple Music catalog and the music library in a single, unified interface. The music picker leverages many kinds of MusicKit requests in one place, allowing for several ways to discover music someone may want to pick. To pick a MusicKit song, I need to add the .musicPicker SwiftUI view modifier! I have a base button already, but I need to add some state variables, starting with a toggle for if the picker should be shown. Next, the selected song property represents an initial song selection. I don't have anything selected, so it can be nil here by default.\n\nNow, I can add the modifier.\n\nNow, I'm going to add my musicPickerButton button to my main view where I added the other buttons. The music picker does not require a subscription, which is why I have it regardless of the subscription check. If there is no subscription, the picker will only show music items from the person's library, rather than both the library and catalog. I'll now build and run this! I really like Olivia Dean, so I'll pick my current favorite Olivia Dean song. To do so, I'm going to tap the search bar, and search for the song. Once I find the result I want, I can tap the plus button on the right of the song information, and dismiss the picker. Great, I have \"Lady Lady\" selected for my bike ride, but for a 30 minute workout, I want to listen to more than one song.\n\nI need to allow for multi-selection in the picker by changing the selection object to an array.\n\nI'll pick 3 songs to start, but I can also select entire albums or playlists by going to their detail pages and pressing the plus button at the top. Okay, I've chosen my songs and can now dismiss the picker.\n\nNow that I've selected music, I want to play it. My teammate, Alan, will take it from here to go over adding playback to the workout app. Thanks, Cathy. Time to play some music, using MusicKit's MusicPlayers! MusicKit offers two different players, SystemMusicPlayer and ApplicationMusicPlayer. Both players are subclasses of MusicPlayer. SystemMusicPlayer controls the system Music app, while ApplicationMusicPlayer plays from your app.\n\nWith SystemMusicPlayer, you may only set the queue. You can't see what is in the queue except for the currently playing item. Meanwhile, you have full read and write access to the ApplicationMusicPlayer's queue.\n\nBoth music players let you set whether a queue will show up in the Music app's Recently Played and both allow you to set playback state, such as the Repeat and Shuffle mode.\n\nFinally, because SystemMusicPlayer controls the system's Music app, it will continue playing even when your app is backgrounded or quits. For the same backgrounding behavior using ApplicationMusicPlayer, enable the Audio Background Mode capability in the Xcode project settings. A queue consists of a collection of playable music items, such as songs. A queue is set on a MusicPlayer. The MusicPlayer's repeat or shuffle behaviour is configurable by its state.\n\nTo start playing, call play() on the MusicPlayer. To stop playing, call pause().\n\nFirst, the MusicPlayer loads the queue.\n\nThen, the MusicPlayer has to load the audio assets before the player can output music! This may take a bit of time.\n\nIf you know ahead of time what to play, buffer the MusicPlayer using prepareToPlay(). This reduces the amount of time needed to output music when you call play().\n\nA queue can be created from any playable music item, such as songs or container types, such as an album or playlist.\n\nUsing the special queue initializers for container types allows the music player to lazily load the container's items, further reducing the load time! When music is played using MusicKit, it will generally appear in the person's listening history in the Music app. affectsListeningHistory is an instance property that determines whether the queue will show in the person's Recently Played shelf in the Music app. It defaults to \"true\" but respects the Use Listening History setting for the Music app. To observe and control the player, both MusicPlayers have observable properties for their playback state and queue.\n\nApplicationMusicPlayer has a queue where you have full control. These are observable classes that you can use directly in your SwiftUI view . To learn more about observation in Swift, check out the Discover Observation in SwiftUI session from WWDC 2023. In the workout app, I'd like to put the artwork front and center during a workout. I'll also show the title and subtitle of the current song, and a set of controls.\n\nTo get the currently playing song, I'll reference the player's queue. Then, if the currently playing song in the queue has an artwork, I'll use MusicKit's ArtworkImage SwiftUI view to display the artwork.\n\nTo show the song info, I'll use the title and subtitle of the currently playing entry.\n\nTo control play/pause, I'll add a button. I'll read the state of ApplicationMusicPlayer and derive whether it's currently playing by checking playbackStatus.\n\nThe button calls \"pause\" when the player is currently playing and \"play\" otherwise.\n\nFinally, the Back button calls skipToPreviousEntry to go to the previous song and skipToNextEntry in the Next button.\n\nIn the music picker, I can tap on the plus button at the top of my running playlist to choose all the songs in this playlist. Then, I'll tap on Done. The first song is playing now, and the ArtworkImage I just put reflects the currently playing song! I'll tap on Pause, and the player pauses. Tap on Next, and the artwork now shows the next song! I'd like to make it more convenient for those using my app to get their workout going by suggesting some songs that they can tap on in the workout view to quickly start listening! Catalog requests allow your app to query Apple Music and provide music content independent of the person's library, such as curated content for your app.\n\nIn MusicKit, structured music catalog requests allow you to fetch content from Apple Music API. MusicKit offers several structured requests, such as getting items based on a specific filter, searching for music, and other Apple Music curated and personalized content! Explore the MusicKit documentation for the full list and how to use them. MusicCatalogResourceRequest is a structured catalog request for a specific resource. In this example, we'll make a request for songs. A request contains some configurations, such as options, if you want to set the behavior of the request. I'll talk more about options in a moment.\n\nYou can set properties on the request that specifies relationships and associations you also want to fulfill as part of this songs request. For example, you may also want the Artists relationship of the song.\n\nAnd, you can set a limit on the number of items to return in the response.\n\nWhen you call the asynchronous response() method, MusicKit fulfills the request.\n\nThe method returns a MusicCatalogResourceResponse which contains the results of the request as a strongly-typed MusicItemCollection. MusicItemCollection is a MusicKit type, containing a collection of music items. In this example, it contains Songs.\n\nMusicItemCollection supports pagination, so if your request produced too many results, hasNextBatch will be \"true\" and you can get the next page using the asynchronous nextBatch() method. When making resource requests, resource availability depends on the account's settings and storefront or region. For example, a resource you request in one region may have an equivalent resource with a different ID in another region. Additionally, a resource for explicit content may have an equivalent clean resource when the account does not allow explicit content.\n\nI'll make a fetchSongs method that has an input of a collection of song IDs, where the first ID is treated as a featured song. The method uses a MusicCatalogResourceRequest for songs that match the IDs in the songIDs argument.\n\nI'll also add the findEquivalents option flag to enable the resource equivalency behavior I just talked about. Then, I'll call the response() method to fetch the content.\n\nTo capture the featured song, I'll use the item(for:) method using the first ID in the input collection of IDs. The catalog request is not guaranteed to return everything that was requested, such as if a resource is unavailable.\n\nFinally, I'll get the other songs in order.\n\nNow I'm ready for my workout! I'll go back to my app, start another workout, and now I have a shelf of some songs that you can quickly pick when you want to get your workout going now! Tapping on one of these artworks will immediately start playing it! That's all you need to know about integrating MusicKit into your app! As Cathy talked about, adopt the music picker view modifier to provide a unified and familiar music picking experience in your app! The Apple Music catalog contains a wealth of content that your app can play. For example, add some background music to enrich your app experience! And, check out other MusicKit APIs, such as requests to browse and modify library content covered in \"Explore more content with MusicKit\", from WWDC2022. If you're interested in integrating on Android or the web, check out \"Meet Apple Music API and MusicKit\". Thank you for watching.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "4:47",
+ "title": "Presents the Apple Music subscription offer",
+ "language": "swift",
+ "code": "@State var showSubscriptionOffer = false\n\nlet options = MusicSubscriptionOffer.Options(\n messageIdentifier: .playMusic\n)\n\n@ViewBuilder\nvar musicSubsriptionButton: some View {\n Button(\"Subscribe to Apple Music\", systemImage: \"music.note\") {\n showSubscriptionOffer = true\n }\n .musicSubscriptionOffer(isPresented: $showSubscriptionOffer, options: options)\n}"
+ },
+ {
+ "timestamp": "5:59",
+ "title": "Adds subscription button to main view",
+ "language": "swift",
+ "code": "@State var subscription: MusicSubscription?\n\nvar body: some View {\n \tVStack {\n // ...\n if let subscription, subscription.canBecomeSubscriber {\n musicSubscriptionButton\n }\n }\n .task(id: isAuthorized) {\n\t self.subscription = try? await MusicSubscription.current\n for await subscription in MusicSubscription.subscriptionUpdates {\n self.subscription = subscription\n }\n }\n}"
+ },
+ {
+ "timestamp": "8:48",
+ "title": "Add .musicPicker() modifier",
+ "language": "swift",
+ "code": "@State var showMusicPicker = false\n@State var selectedSong: Song? = nil\n\n@ViewBuilder\nvar musicPickerButton: some View {\n Button(\"Pick some Music\", systemImage: \"music.note.list\") {\n showMusicPicker = true\n }\n .musicPicker(isPresented: $showMusicPicker, selection: $selectedSong)\n}\n\nvar body: some View {\n VStack {\n if let subscription, subscription.canBecomeSubscriber {\n musicSubscriptionButton\n }\n musicPickerButton\n }\n}"
+ },
+ {
+ "timestamp": "14:49",
+ "title": "Artwork",
+ "language": "swift",
+ "code": "@State var queue = ApplicationMusicPlayer.shared.queue\n\nvar body: some View {\n VStack {\n if let artwork = queue.currentEntry?.artwork {\n ArtworkImage(artwork, width: 200, height: 200)\n } else {\n // Placeholder artwork\n RoundedRectangle(cornerRadius: 16)\n .fill(.quaternary)\n .frame(width: 200, height: 200)\n }\n }\n}"
+ },
+ {
+ "timestamp": "15:06",
+ "title": "Current entry info",
+ "language": "swift",
+ "code": "@State var queue = ApplicationMusicPlayer.shared.queue\n\nvar body: some View {\n VStack {\n // ...\n if let currentSong = queue.currentEntry {\n Text(currentSong.title)\n .font(.title3.bold())\n \n if let subtitle = currentSong.subtitle {\n Text(subtitle)\n .font(.subheadline)\n .foregroundStyle(.secondary)\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "15:14",
+ "title": "Playback controls (play, pause)",
+ "language": "swift",
+ "code": "let player = ApplicationMusicPlayer.shared\n@State var state = ApplicationMusicPlayer.shared.state\n\nvar isPlaying: Bool {\n state.playbackStatus == .playing\n}\n\nvar playPause: some View {\n Button (\n isPlaying ? \"Pause\": \"Play\",\n systemImage: isplaying ? \"pause.fill\" : \"play.fill\"\n ) {\n if isPlaying {\n player.pause()\n } else {\n Task {\n try await player.play()\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "15:38",
+ "title": "Playback controls (next, previous)",
+ "language": "swift",
+ "code": "let player = ApplicationMusicPlayer.shared\n\nvar controls: some View {\n HStack {\n Button(\"Back\", systemImage: \"backward.fill\") {\n Task {\n try await player.skipToPreviousEntry()\n }\n }\n // ...\n Button(\"Next\", systemImage: \"forward.fill\") {\n Task {\n try await player.skipToNextEntry()\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "18:58",
+ "title": "Music catalog resource request",
+ "language": "swift",
+ "code": "func fetchSongs(songIDs: [MusicItemID]) async throws -> (featured: Song?, other: [Song]) {\n var request = MusicCatalogResourceRequest‹Song>(matching: \\.id, memberOf: songIDs)\n request.options = [.findEquivalents]\n \n let response = try await request.response()\n \n let featuredSongID = songIDs[0]\n let featuredSong = response.item(for: featuredSongID)\n \n let others: [Song] = songIDs[1...].compactMap { songID in\n return response.item(for: songID)\n }\n \n return (featuredSong, others)\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Integrating MusicKit into your app",
+ "url": "https://developer.apple.com/documentation/MusicKit/integrating-musickit-into-your-app"
+ },
+ {
+ "title": "Apple Services Performance Partner Program",
+ "url": "https://performance-partners.apple.com/home"
+ },
+ {
+ "title": "MusicKit",
+ "url": "https://developer.apple.com/documentation/musickit"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/254/5/d4b2c60a-8a2a-41d1-a55a-0fd60d927798/downloads/wwdc2026-254_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/254/5/d4b2c60a-8a2a-41d1-a55a-0fd60d927798/downloads/wwdc2026-254_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "10149",
+ "year": "2023",
+ "title": "Discover Observation in SwiftUI",
+ "url": "https://developer.apple.com/videos/play/wwdc2023/10149"
+ },
+ {
+ "id": "110347",
+ "year": "2022",
+ "title": "Explore more content with MusicKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2022/110347"
+ },
+ {
+ "id": "10148",
+ "year": "2022",
+ "title": "Meet Apple Music API and MusicKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2022/10148"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:16.122Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-256.json b/data/wwdc/videos/2026-256.json
new file mode 100644
index 0000000..ae3d521
--- /dev/null
+++ b/data/wwdc/videos/2026-256.json
@@ -0,0 +1,37 @@
+{
+ "id": "256",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/256/",
+ "title": "Discover generated subtitles and subtitle styles",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Accessibility & Inclusion",
+ "Audio & Video"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm James and I'm an engineer on the AVFoundation team. I love to travel, so today, I'll take you on a trip to explore a couple of features related to subtitles. People use subtitles for many reasons. Subtitles are vital for people who are deaf or hard of hearing, or who have other accessibility needs. They need subtitles to understand the content. Some people read subtitles to assist in understanding the spoken dialogue. I use subtitles when I physically can't hear the audio, like when I'm in a busy airport.\n\nApple AI-generated subtitles can be created live, locally on the device as the media plays. In this video, first, I'll show you how the Apple AI-generated subtitles appear in your app.\n\nAfterwards, I'll show the subtitle style preview feature that lets people customize subtitles while playing video.\n\nNext, I'll show how media assets are authored.\n\nThe media creation journey usually begins with filming and editing the Video and Audio content.\n\nThen, subtitles, text that represents spoken words in the audio, are created manually. Later in this video, I'll refer to such subtitles as Authored subtitles. The content author can create multiple subtitles, each for one particular language. Also, they can create multiple audio languages. The final media contains the Video, Audio and Authored subtitles all together.\n\nEven content with multiple audio and subtitle languages might not have a language the viewer understands.\n\nPeople can have their device create generated subtitles to help fill the gap when the original content doesn't have the subtitle language they need.\n\nThis enables more people to access and experience the content.\n\nNext, this is how generated subtitles are created.\n\nThere are two use cases for generated subtitles. In the first use case, subtitles are generated from audio.\n\nThe source audio goes into the on-device Speech-To-Text model and subtitles are created.\n\nThis is called Speech transcription.\n\nSecond, for language translation, subtitles are generated from other subtitles. The source subtitles, English in this case, go into the on-device translation model. New subtitles in a different language are created, Italian, for example. This is called Language translation. Generated subtitles provide additional languages. The authored subtitles are preferred and remain unchanged.\n\nThe good news is that you don't need to implement anything to turn on generated subtitles.\n\nThey're available automatically during video playback.\n\nGenerated subtitles are provided for many playback scenarios. Subtitles can be generated for HTTP live streaming content, including Live streams, like TV channels. It also includes Video on demand movies and shows, including travel videos and live events, like sports.\n\nFile-based content is also supported, like app-bundled videos or downloaded media.\n\nSeveral content types are supported. Professional content, like movies and series, is supported.\n\nSo is customer-created content, like camera capture from the iPhone and social media videos.\n\nNow, I'll show you the supported devices and languages. Generated subtitles are available on multiple devices. This table shows the supported devices and languages.\n\nStarting in iOS and macOS 27, English subtitles can be generated from English audio. This is also supported on tvOS and visionOS 27. Also, multiple subtitle languages can be generated from English subtitles on iOS and macOS. Now that you know about generated subtitles, I'll talk about how to present them in your app.\n\nIt's important to provide subtitle selection UI during video playback.\n\nHere are the options for your app.\n\nThis is the AVPlayerViewController UI on iOS. It fully implements subtitle selection and player controls. You don't need to do anything extra. AVPlayerView on macOS provides similar functionality.\n\nThis is AVLegibleMediaOptionsMenuController. It presents subtitle selection controls and implements the behavior. It's a good choice when you want to add subtitle selection UI to your existing player UI, since it does not provide player controls.\n\nOr, you can implement custom controls for media selection to match the style of other controls in your app. Like the ones I created here in my app.\n\nGenerated subtitles make the content in your app more accessible. More people can understand and enjoy the content.\n\nNow I'll show you another subtitle feature that makes your app even more accessible.\n\nSubtitles help make content accessible, but the presentation of the subtitles is also important. The Settings app has let people select and change subtitle and caption styling for many years.\n\nThere are a few built-in styles, and people can create custom styles to fit their needs. I've created a custom style called Bold Yellow. It has yellow text with a little extra border to make it easier for me to read. I can select the style in the Settings app, but it would be easier, and more accessible, to change the style while watching a video. That's exactly what the subtitle style preview does.\n\nI'm watching a video and I have the Style menu open. I have the same styles available that were in the Settings app.\n\nThe subtitle style can now be changed right from the menu where you select subtitles during video playback. Not only that, but a preview of the style is shown to make selection easier.\n\nThis is what my Bold Yellow style looks like in a video.\n\nThere are several ways to implement the subtitle style preview feature in your app.\n\nFirst, this is AVPlayerViewController on iOS. It fully implements the subtitle style preview and player controls. AVPlayerView on macOS provides similar functionality.\n\nThis is AVLegibleMediaOptionsMenuController. It presents subtitle style preview controls and implements the behavior. It's a good choice when you want to add the subtitle preview to your existing player UI.\n\nNext, AVPlayerLayer has an API to show the style preview. I'll show you how to implement it in a moment. Also, AVCaptionRenderer can provide the style preview, but you are responsible for rendering it. Now, here's the AVPlayerLayer implementation. Each subtitle style in the system is assigned a profile ID.\n\nFirst, fetch all of the styles by their profile IDs. Populate your UI with the names for the styles.\n\nWhen a style is selected, show the stylized preview. New subtitles are shown using the specified style. Any existing subtitles are automatically hidden so they don't interfere with the preview.\n\nPass nil for the text parameter. In that case, localized system text is shown.\n\nUse the position parameter to avoid any UI controls. This is an offset from the default location of the preview text.\n\nCall this function again to show a different style. You can call it as many times as needed.\n\nStop the preview when selection is done. This removes the preview text and restores any existing subtitles that are active.\n\nSet the chosen style. It will be used for all subtitles on the system.\n\nYou can build this feature in your app to let people quickly choose subtitles that are easier to read while watching a video. Now, I will show you both of these subtitle features together. I'm planning a camping trip to Italy. I'm going to watch this camping video for some inspiration.\n\nIt's has English subtitles, but I want Italian subtitles, so I can brush up on my Italian.\n\nTo change the language, I'll open the Subtitles menu, then click Language.\n\nSeveral subtitles are available. Some are authored and some generated. The generated options are marked with a sparkle symbol and the word Translated. I'll pick the Italian generated subtitles.\n\nNow I have Italian subtitles in my video.\n\nI'll change the style to make them easier to read. First, I'll open the Subtitles menu, then select Style. I'll try Large Text. The existing subtitles have been replaced with a placeholder message, in Italian, using the Large Text styling.\n\nThe text is larger, but let me try my custom style.\n\nOh, I like that.\n\nI'll dismiss the menu.\n\nThe subtitles are now using my custom style. This trip is going to be amazing. Before I go, here are your next steps.\n\nNow that you know about the generated subtitles feature, I encourage you to go explore it. For example, watch some travel videos and turn on generated subtitles.\n\nAlso, make sure your app has UI to select subtitles. Implement the subtitle style preview too. People appreciate this accessibility feature when they need to change the style of their subtitles. Thank you for watching. I have to go catch a plane to Italy. Ciao ciao.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "7:43",
+ "title": "Implement subtitle style preview",
+ "language": "swift",
+ "code": "// Implement subtitle style preview\n\nimport AVFoundation\nimport MediaAccessibility\n\nfunc updateProfileList() {\n subtitleStyleProfileIDs = MACaptionAppearanceCopyProfileIDs() as? [String] ?? []\n}\n\nfunc showPreviewStyle(subtitleStyleProfileID: String) {\n playerLayer.setCaptionPreviewProfileID(subtitleStyleProfileID, position: .zero, text: nil)\n}\n\nfunc stopPreviewStyle() {\n playerLayer.stopShowingCaptionPreview()\n}\n\nfunc setSubtitleStyle(subtitleStyleProfileID: CFString) {\n MACaptionAppearanceSetActiveProfileID(subtitleStyleProfileID)\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "What's new in HTTP Live Streaming",
+ "url": "https://developer.apple.com/streaming/Whats-new-HLS.pdf"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/256/4/d28efb5e-5550-468d-b1d1-caec51ce55e6/downloads/wwdc2026-256_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/256/4/d28efb5e-5550-468d-b1d1-caec51ce55e6/downloads/wwdc2026-256_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:15.805Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-258.json b/data/wwdc/videos/2026-258.json
new file mode 100644
index 0000000..ba91b7a
--- /dev/null
+++ b/data/wwdc/videos/2026-258.json
@@ -0,0 +1,79 @@
+{
+ "id": "258",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/258/",
+ "title": "What’s new in Xcode 27",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hey, I'm Jake, a designer on the Xcode team.\n\nMy colleague Breckin and I are super excited to share what's new in Xcode 27. We've really been into paper airplanes lately and have been working on an app to manage our grand fleet. It's been a blast! Xcode 27 makes it super easy to kick off tasks with Coding Agents, quickly iterate on new project ideas, and its workspace is more customizable than ever! Let's dig into what's new. We'll start with the new workspace look and feel and how it can be customized.\n\nNext, we'll check out how easy it is to kick off a new project when an idea strikes. Then we'll dig into the awesome new updates for working with coding agents in the editor.\n\nAfter that, we'll take a look at how Device Hub makes it a breeze to evaluate apps on devices and simulators.\n\nThen, Breckin will share how to keep an app's experience soaring after launch. Let's start with the workspace.\n\nThe Toolbar and Themes in Xcode have been revamped to allow for more customization. And when editing code, inline issues now get a subtle look, to minimize distractions while typing. Here's the standard look for the new workspace in Xcode 27.\n\nLet's take a look at the Toolbar. Some controls that were previously in the jump bar, like history navigation and editor controls, have moved up into the Toolbar.\n\nActivity information, like build progress, appears under the window title.\n\nIn the center, we have our new entry point for working with coding agents, more on that in a bit, and our trusty scheme and destination picker.\n\nIn the top right, we have controls for adding tabs and editor panes, customizing your editor's settings, and a 3-way chooser for swapping between editor modes.\n\nThe first option displays previews & playgrounds in the canvas.\n\nThe second reveals related content in the Assistant Editor.\n\nAnd the last enters a mode to review source control changes.\n\nOh, and speaking of source control… the branch picker has moved to the bottom bar where it can more easily fit long, beautiful branch names.\n\nBut the best part about the new Toolbar is that it's fully customizable. Now you can add or remove your favorite items and reorder them to your heart's content.\n\nCustomizing the workspace goes beyond just the Toolbar. With Xcode 27's new themes, you can choose from beautiful presets, or simply play around with a couple sliders to tune a theme in your own way. Let me show you… The new Appearance panel in Xcode's settings window has everything you need to configure a theme. The standard theme has been revamped to be brighter and more colorful. I can play around with the first slider here to influence the intensity of the text colors.\n\nAnd the second slider lets me ramp up the background intensity.\n\nnow we can see a full window background color. And if I keep bringing the intensity all the way up, my background turns into a vibrant gradient.\n\nAnd of course, I'm not locked into these starting tints. I can choose other values, like a pink tint for my background, and my theme automatically updates for this new color palette.\n\nIf I want to choose another theme, I can browse a list of presets.\n\nTo get in the mindset of soaring through the sky, I'll use a theme with a lot of blue tones like Neon Noir. And I love how this one looks with a high background intensity.\n\nOf course, if I want to specify individual colors, I have the full list below. Every value in here is generated from that base palette I've been editing. When I customize a value, like setting my keyword color to pink, the choice is locked in. Edits I make to the palette will no longer influence this color. And it's easy to use this reset action if I want to restore it back to being an automatic color.\n\nFonts use a similar customization system. These basic fonts for code, prose, and console, act as the palette which generates how fonts are used in my other editors. If I change the size, variation, or family, the rest of the fonts will automatically update in turn.\n\nSomething I love about these themes is how it influences the entire workspace window, including other editors. If I check out my project settings, I can see a subtle tint of my theme coming through the background.\n\nThere's one more thing to know about themes, you're not limited to just one! If you want, you can pick a separate theme for a given workspace. Your font settings are saved separately, so it's super easy to swap out themes depending on your mood or workflow.\n\nLike you saw, to help get myself into the mindset of paper aerodynamics I like to use the Neon Noir theme for my paper airplane project.\n\nThis helps identify at a glance which project is which. So, if you find yourself needing to quickly differentiate projects that are perhaps eerily similar side-by-side, try giving them unique themes. Oh, and while themes for the light appearance have a subtle look to them, in the dark appearance, they can be truly vibrant and expressive. Go give those background sliders a whirl.\n\nWarnings and errors have also been revamped to work with the new themes. Predictive or \"live\" issues have a new subtle look to them to reduce distractions while you type. And to differentiate them from warnings and errors you get when you build.\n\nAs you make changes to your code, Xcode will automatically predict issues as if you were going to kick off a new build. These predictions use a subtle background that blends in with your theme so you can keep your focus primarily on what you're typing. When do you build, the subtle predictions will either turn into build warnings and errors with a full intensity color or they will be dismissed if they were resolved.\n\nAh! Speaking of editing code, Breckin just sent me a great idea for our app. I want to try the idea out in a new project.\n\nXcode 27 makes it a breeze to to kick off new projects for testing out new ideas like this, or for creating the foundation of your next great app. Let's check it out… I can create a project from Xcode's File menu. Then I can choose from a list of starting points, depending on the kind of project I have in mind. If I want to use SwiftUI, App is a great choice.\n\nIf I don't need UI, I can keep it simple with a macOS Command Line Tool.\n\nIf I want to create a library that others can use, I could choose Swift package. And Playground is a great choice when I just need a simple standalone Swift file with a Playground macro. In my case, I want to make an app.\n\nA brand new untitled project is created, no questions asked. Literally! I can take my time and play around in this project to flesh out my idea.\n\nWhen I'm ready, I can choose to give it a name and save it, or discard the project entirely.\n\nThese untitled projects make it a breeze to try out something in a blank canvas.\n\nOh! I just got something else from Breckin. This time, it's a Swift file.\n\nOpening the Swift file gives me a new workspace window. Even though this file isn't a part of a project, Xcode 27 can display playground results and UI previews in the canvas! This makes it easy to share lightweight ideas with each other, and what Breckin shared here looks like a great start for how airplane stats could appear in our app. I want to add something like this to the real project. Coding agents could really help here.\n\nWorking with coding agents in Xcode 27 has been supercharged. It's easier than ever to kick off and stay on top of parallel agent tasks and conversations. The transcript has moved into the editor pane, so you can compose it with other editors with tabs, splits, or whatever suits your workflow. The editor also includes an easy way to see what the agent changed and any artifacts that got produced.\n\nRemember that fancy button in our Toolbar? I can use it to kick off a new conversation or task for a coding agent. The conversation appears as an editor, so it works with tabs, split editor panes, or however you want to organize your workspace. I really liked Breckin's idea for adding stats for different kinds of paper airplane designs, so I could kick off a simple request… But I want to dig into how this feature should work with the agent before we start making changes. I can use the plan command for that.\n\nUsing slash plan will use the plan tool.\n\nI can take the time to specify all the details I want my agent to consider. Then the agent will gather the necessary context for this plan without making any changes yet. While it's exploring, it can kick of sub-agents to work in parallel.\n\nIn this case, I need to provide some input on how to tackle the problem. I'll give some guidance so it can keep working towards creating the plan. Once the plan is ready, I can read it over, give inline feedback, or have the agent go ahead with implementation.\n\nAs the agent works, any changes it makes to the codebase appear on the right-hand side.\n\nAny produced files, artifacts, or screenshots will appear here as well. This is a great way to see how my app is evolving as the agent interacts with the app in the simulator and in previews.\n\nWhile I let the agent dig into implementing the plan, I'll open up the coding assistant sidebar.\n\nIt contains a list of my other agent conversations and tasks that may be happening in parallel.\n\nThe sidebar list makes it easy to check in on conversations and see if they need any input or have unread messages.\n\nAnd there's so much more here to explore, I recommend checking out the session \"Xcode, agents, and you\" to get the full look at agentic workflows in Xcode 27.\n\nNow that the plan is implemented, I want to try out the app. When launching an app on a simulator, it will open as a new window in Device Hub. As the name implies, Device Hub has some great ways to explore and evaluate your app across simulators and physical devices, let's take a look. If I choose to run my app on iPhone 17 Pro simulator, I get a new window from Device Hub. You can see the window is compact and sized to fit the device. I have some quick actions like going home, taking a screenshot, or rotating the device. But I can also expand the window, which grants more space and access to more controls.\n\nI can open the Inspector and get even more ways to evaluate my app. It's important to test an app with different accessibility settings. For example, I can increase contrast, choose a larger dynamic type size, and try out my app with a dark appearance.\n\nThe app is looking good with these settings, so let's go back to the defaults.\n\nFor my iPhone app, it's also important to consider how it appears in iPhone Mirroring. On macOS 27, the iPhone Mirroring window can be resized, so it's also a good idea to test out my app in the new resize mode.\n\nI can try out different aspect ratios and content sizes.\n\nMy iPhone app already has great support for resizing thanks to the standard SwiftUI views I'm already using and the effort I've already put into making my custom views and layouts support resizable windows on iPad and Mac, so this is already looking great.\n\nDevice Hub makes it a breeze to work with simulators, but the coolest thing is what I've shared so far works with physical devices as well! If I open the sidebar, I see a combined list of simulators and devices. I have a paired iPad Pro already running my app, and I can see and control it directly in Device Hub! Now I can try my app out on many different form factors right on my Mac! Device Hub is super powerful and unlocks a lot of great workflows like working with files, data containers, evaluating app configurations, and more. We have a whole session on it, go check out \"Get the most out of Device Hub\".\n\nI've had a lot of fun getting my app up and running, but delivering a truly great app is about so much more than those prototyping sessions and building out the initial features.\n\nMy colleague Breckin will share how Xcode 27 keeps your app soaring after launch! Thanks Jake! It's a nice one! It's true, there's a lot of distance between a prototype and a finished app. Our app works, but it isn't ready for the world yet. Let's close that distance with some other updates in Xcode 27. First, we'll prepare for departure with app Localization. Then, we'll keep the wings de-iced with updates to Organizer.\n\nPerformance is incredibly important, so we'll cover how Instruments helps us keep our app level with the horizon. And finally we'll make sure our Paper Airplanes continue to soar into Xcode Cloud. I'll start with Localization. Jake and I want to bring the joy of flying perfectly-folded paper airplanes to people around the world. Localization is the natural place to start. And in Xcode 27, my coding agent takes on a lot of the work.\n\nEverybody wins when you localize your app, because you can reach more people in more languages. But localization can be a big undertaking. Thankfully, Xcode 27 makes it much easier and faster to localize your app. Because my agent is a large language model, it is perfect for suggesting appropriate translations for my strings.\n\nI've created a new conversation. I'll ask the agent to setup localization for our app's project. You can choose as many languages as you like, but I decided to start with Spanish. The agent reads through our app's code, ensures string literals are ready for localizable references, and creates a String Catalog containing every UI string. Just a few turns of conversation, and the app is ready to be translated.\n\nBy opening the String Catalog I can observe the agent working through the list in efficient batches. Let's check how the agent is doing by returning to the chat.\n\nGreat! It's done. The agent analyzed our app, performed any code changes necessary for localization, created a String Catalog, and translated every UI string into Spanish just as I requested.\n\nWhen the agent generates translations, it uses the full context of your project as well as language-specific style guidance from Xcode. Within minutes, our Paper Airplanes app has a localized build I can run. I can test it immediately, fixing common issues like awkward layout or truncated text.\n\nI like to use The String Catalog for focused, per-language work. I reviewed the Spanish translations, they looked great. I've added a new entry for Simplified Chinese. I can add languages with the + button in the bottom left.\n\nAnd new in Xcode 27, with a language selected in the String Catalog I can click the Generate Translations button.\n\nThe agent goes to work in the background, adding localization for Simplified Chinese. I can check on its progress at any time, either in the agent conversation itself or by inspecting the String Catalog entries as they fill in. When the agent is finished, I can run the app to spot check these changes. If all is well I can distribute an update to our new users for testing.\n\nA great way for native speakers to review our app's support for their language is through TestFlight! TestFlight users can provide translation feedback just as they would provide feedback for any other feature in the app.\n\nLocalization in Xcode has never been easier or faster; here are a few more tips to reach more people than ever before with your app.\n\nAsk the agent to ensure the existing strings in your code are ready for localization.\n\nIt's best to start with just one or two languages, so issues are easy to spot. And be sure to test the app - even if you don't read every language - to catch those awkward layout or truncated text issues early. And remember that in addition to any internal testing you perform, TestFlight is perfect for getting feedback from native speakers.\n\nThere's a lot more to localization than what we covered today. For a deeper dive into these translation features, check out \"Translate your app using agents in Xcode\". And for some true immersion into the world of app localization, land on \"Code-along: Explore localization with Xcode\".\n\nJake and I have folded quite a few languages into our Paper Airplanes app. We're almost ready for takeoff. Between Jake's work earlier and mine here, we were ready to put our app in front of people, people, through TestFlight and then launching on the App Store. But shipping isn't the finish line. It's where we learn how our app actually behaves in the world. That's where the Organizer comes in. The conversation between our app and its users keeps going long after the App Store launch. And the Organizer is where a lot of that conversation lives. In Xcode 27, it does more than collect reports. It also helps me act on them. The Organizer has always been where I see what my users are running into, and where I can make our app better. But… seeing the issue is one thing. Figuring out what to do about it is another. In Xcode 27, the Organizer goes further. It helps me see more issues and can even recommend ways to fix common issues, like hangs. hangs. For our app, that's the difference between \"I know there's a hang\" and \"here's where the hang is, and here's what I should try next.\" Xcode 27 brings four new things to the Organizer. The redesigned Overview surfaces the highest-impact issues first, so I can focus immediately on big trouble spots.\n\nThe Overview page puts diagnostics and metrics in the same view. A spike in the metric chart up top tells me that something needs attention. The diagnostics below show me where in the code to start looking. One screen, instead of jumping between two.\n\nNew metrics for storage and animation hitches flag issues the old metrics couldn't see.\n\nIn Xcode 27, there is a new Storage metric that shows how much space our app and our app's data have been taking up. Storage on a phone is shared across every app, so when one app over-uses it, every app feels it. The metric breaks down documents, data, and binary size – since binary size impacts cellular downloads and launch time. It tells me what my app's footprint is, and where to focus for the biggest size reduction wins. You can see that version 1.0 and 1.01 of our app were quite large. This let Jake and I know that we should compress some images. The App Size chart shows that this made a big difference, one our users could feel. The Organizer has tracked animation performance for a while, in the context of scrolling hitches. The new hitches metric surfaces issues in more places than scrolling, like understanding how apps use Liquid Glass and SwiftUI views. The updated metric gives me a more complete picture, including animations the old one missed. For our app, that's the difference between catching a choppy animation and not. Speaking of which, it looks like we introduced a pretty bad hitch in version 1.3… I'll make a note to take a look at that when I'm done exploring the Organizer.\n\nIn Xcode 27, app recommendations have become Metric Goals, Last year, the Organizer started showing recommendations for launch time. Xcode 27 offers an expanded set of goals for your app to meet. They are achievable, realistic, and based on technical and functional similarities between your app and other apps. and they cover more metrics: hang rate, disk writes, battery, and the storage and hitches metrics we just discussed.\n\nThe goals are calibrated to my app, compared with similar apps based on what my app actually does, and what technologies it's built with. And alongside those, comparisons include our app's own historical baselines, so we can see whether we're getting better or encountering some unexpected turbulence.\n\nOrganizer is great at providing information about issues impacting your users, but you may be wondering how to fix them. New in Organizer, you can get guided performance analysis and generate recommendations using coding agents.\n\nA lot of the time I spend on a regression is just figuring out why it happened, and how to reproduce it. That's the part Generate Recommendation tackles. From the Organizer, I click Generate Recommendations..., pick my project, and the agent works through the diagnostic data with me. Like any agentic tool, I can iterate. Try a different angle, try a different fix, until something fits the codebase. And that's a tour of Organizer in Xcode 27.\n\nBut a great app does more than just work. It also has to do its job fast, and not drain the battery. The more efficient our app is, the longer our users stay in the park instead of diverting for an unscheduled charging stop. Tuning our app's performance is one of my favorite parts of app development. A fast, efficient app keeps our users in the air longer. That's where Instruments comes in. And in Xcode 27, Instruments fits right alongside the rest of my work.\n\nEvery app has its rough edges. Even our Paper Airplanes app. Some show up right away, others take more digging, like that animation hitch the Organizer showed me earlier. Instruments is what I reach for when I want to know what my app is really doing. And in Xcode 27, finding the answers takes a lot less time.\n\nWhen my app feels slow, the first question is always: where is the time really going? Performance investigation has always felt like detective work, and Instruments has been my favorite partner. New in Xcode 27, Top Functions makes the patterns jump out faster. I want to try to reproduce that hitch the Organizer flagged. I have our app's project open in Xcode 27. I'll run it in the Simulator and try to reproduce the animation problem.\n\nOooh… looks like the plane is having a little engine trouble. Let's do that again and think about what might be happening.\n\nWe're definitely spending too much time on each frame. I have an idea about what's wrong, but with Instruments there's no need to guess.\n\nOh. Jake just sent me an Instruments recording. Jake's CPU profile shows a bunch of activity on the right. I selected that time range.\n\nI've pressed the Top Functions button and I can see that we're spending a lot of time in several parts of my app. Top Functions is perfect for finding performance problems that arise due to expensive operations that are performed many times. In this case we can see that we are doing a lot of work in the animation pipeline. The top function is paperPhysics, which looks very expensive indeed. Let's go back to Xcode and look at this function.\n\nI'll use one of my favorite shortcuts CMD+Shift+O to bring up Quick Open.\n\nI can open a file but I can also type the name of a function or other piece of code.\n\nThere it is! Oh! Oops, I'm iterating way too many times in this loop. We don't need to simulate our airplane that accurately for this animation. This is some code from a debugging session that I accidentally left in. I'll fix that by reducing it to 5, the number I really wanted.\n\nThat should fix the issue. I'll launch the app again in the simulator to confirm.\n\nThe hitch has disappeared. Top Functions pointed me directly to the most expensive part of the code. Thankfully the fix was simple and we'll soon have much happier flyers. I recorded another run in Instruments after making the fix. It's much better. Notice, none of our app's methods show up in Top Functions now. That's a great sign! We have real evidence that our next update will not only make our users happy, but their batteries too. Top functions in Instruments helps me reveal where my app is really spending its time. Instruments is truly an incredible tool to see exactly what your app is doing, and, by the way, processor trace is the coolest thing ever. Speaking of exploring all of the awesome features in Instruments, there's so much more that I'd love to show you including how you can compare performance runs so you know whether code changes actually improved things or made it worse. But we have an app update to ship. thankfully, there's a deep library of performance-related sessions. If your app is turbocharged with agentic features you'll definitely want to soar over to \"Debug and profile agentic app experiences with Instruments\". We also recommend you dive deeper by checking out \"Profile, fix, and verify: Improve app responsiveness with Instruments\".\n\nAt this point, people are loving our app and we don't want to impact their experience. Every change is a chance to break what's already working. And shipping updates without knowing whether a regression snuck in is a stunt plane maneuver that no one wants to try. Thankfully, that's the next thing Xcode 27 has my back on. Catching regressions is one of the parts of app development I'd rather not do by hand. Every fix and every new feature I add brings with it a chance to break something I already shipped. That's where Xcode Cloud comes in! Xcode Cloud is a Continuous Integration and Delivery service built right into Xcode, and made expressly for developers shipping to Apple platforms.\n\nIt builds and tests your app in the cloud, in parallel, across multiple devices, Xcode and OS versions. And in Xcode 27, it's easier and faster than ever to get started with Xcode Cloud.\n\nI'll set up our app's Unit and UI Tests to run in Xcode Cloud automatically when we make changes to our main or feature branches. Each run will be a great signal for potential regressions. I'll click the Get Started… button to start the set up.\n\nThe app and developer team look correct, so I'll click Next.\n\nI'll connect Xcode Cloud to our remote source code repository...\n\nand that's it! Once I click Start First Build our build and test workflow is ready to run on every commit! The benefits of using Xcode Cloud don't stop there. Xcode Cloud also helps you deliver your app to users, seamlessly integrating with TestFlight and the App Store.\n\nThere's a lot more to Xcode Cloud than what I showed today. For a deeper dive, check out \"Build, deliver, and automate with Xcode Cloud\" and for more on how to extend Xcode Cloud to work with your own services and more, watch the session \"Extend your Xcode Cloud workflows\".\n\nThe work I covered today is the work I keep coming back to. From delivering new features to ensuring what already works keeps working. Whether I'm creating or refining, Xcode 27 meets me there. Jake, I'll hand the controls back to you. Thanks Breckin! As you saw, Xcode 27 is there for you every step of the way through your app's lifecycle. From fleshing out initial ideas into prototypes, working collaboratively with agents, localizing your app, and resolving issues in the field, Xcode can help you focus on what truly matters — making something special. It's been a blast sharing what's new, but there's even more to discover. Download XCode27, customize it for your workflow, and explore the new features. You can find more info in the release notes and in this session's resources. And there are ton of other sessions beyond what we've already recommended you check out out, and loads more goodies to explore, so get out there and have a great WWDC!",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Xcode updates",
+ "url": "https://developer.apple.com/documentation/Updates/Xcode"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/258/4/66bc9c90-649b-4a16-a2bb-1e6f16b1ec73/downloads/wwdc2026-258_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/258/4/66bc9c90-649b-4a16-a2bb-1e6f16b1ec73/downloads/wwdc2026-258_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "261",
+ "year": "2026",
+ "title": "Build, deliver, and automate with Xcode Cloud",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/261"
+ },
+ {
+ "id": "243",
+ "year": "2026",
+ "title": "Debug and profile agentic app experiences with Instruments",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/243"
+ },
+ {
+ "id": "260",
+ "year": "2026",
+ "title": "Get the most out of Device Hub",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/260"
+ },
+ {
+ "id": "268",
+ "year": "2026",
+ "title": "Profile, fix, and verify: Improve app responsiveness with Instruments",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/268"
+ },
+ {
+ "id": "213",
+ "year": "2026",
+ "title": "Translate your app using agents in Xcode",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/213"
+ },
+ {
+ "id": "259",
+ "year": "2026",
+ "title": "Xcode, agents, and you",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/259"
+ },
+ {
+ "id": "225",
+ "year": "2025",
+ "title": "Code-along: Explore localization with Xcode",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/225"
+ },
+ {
+ "id": "10200",
+ "year": "2024",
+ "title": "Extend your Xcode Cloud workflows",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10200"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:16.171Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-259.json b/data/wwdc/videos/2026-259.json
new file mode 100644
index 0000000..34b610b
--- /dev/null
+++ b/data/wwdc/videos/2026-259.json
@@ -0,0 +1,44 @@
+{
+ "id": "259",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/259/",
+ "title": "Xcode, agents, and you",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hello! I'm Devin! And I'm Maxwell! We are both members of the Xcode Intelligence team. And we're excited to show you how using agents in Xcode can enhance your development. In Xcode 26.3 we introduced coding agents along with tools to help you tackle complex, multi-step tasks.\n\nIn Xcode 27, you can be even more productive. We've expanded the tools Xcode provides and redesigned how you interact with agents. All with the goal of keeping you focused on the work that you enjoy. You drive the vision for your code, and Xcode helps you get there faster and with confidence.\n\nXcode adapts to you, whether you are just getting started with agents or consider them an essential part of your workflow.\n\nTogether with Devin, I'll walk you through using agents in Xcode throughout your development. I'll start by exploring a project to gain an understanding of its structure.\n\nI'll use that understanding to plan and build out a new feature.\n\nThen, Devin will refine what I've built. And finally, he'll orchestrate multiple conversations to accomplish specific tasks. Let's get started! When Maxwell and I are not at the office working on apps, we're spending time at the gym. We've wanted something that helps us track exercises in our workouts. We want something that is personalized and adaptive to how we train. So, together we're building an app that has exactly the features we want. I just got this app off the ground using an agent in Xcode, and it already has the basic functionality we need. It has a workout page to track exercises. And a history section to show previous sessions.\n\nThis app is really great. One thing that I would like is if I could see how I'm progressing over time, now that I'm tracking all these workouts. I was thinking the same thing! We need an insights view. That sounds like a great idea. I'll start by creating a prototype to show analytics using previous session data. Perfect! I can refine it from there once everything is in place.\n\nDevin has the project in a great starting place, and I want to familiarize myself with everything he's done already. So I'll start by using Xcode as a guide to both explore the project and learn what options I have for adding insights data. When you use agents in Xcode to explore, they can see your whole project. From your source code, build settings, and even open files and active selections, agents are tuned into exactly what you're working on in the moment.\n\nTo start exploring, I'll hold Option and Shift while clicking on the button in the toolbar to create a new conversation. This opens a conversation as a separate editor pane so I can also see individual files and look into specific ones as I explore. I'll ask the agent to summarize the project's data models and current view hierarchy, and provide a walkthrough. I'll send this prompt, and the agent uses Xcode's tools to gather context, and piece together the project's structure.\n\nWhen jumping into a new project, it can be challenging to determine where you should start. When reading complex architecture and a collection of source files, piecing together how everything connects to build an understanding can take considerable time.\n\nXcode can help you here. You can ask for a walkthrough to distill down the architecture into something easier to understand, complete with rich details like outlines of data flow, tables of key areas and source code references. When you want to investigate at a deeper level you can navigate directly to key files.\n\nThe walkthrough is complete, and I can read through it to build an understanding at a high level. And if I want to look deeper into our workout views, I can click on the links and jump directly into the individual files. This walkthrough is a great reference. But I don't want to lose all this detail in one conversation, I'd like to preserve it. That way I, or anyone else on my team, can reference it later and get up to speed faster.\n\nI've read all the source files that I need to for now, so I'll close the other editor pane and focus in on just the conversation. I'll ask Xcode to draft up two architecture documents with the information gathered, and place them directly in the project with the source code.\n\nIn this conversation, the agent started from scratch. It needed to search, read, and investigate all the code in the project to reach an understanding. This discovery loop takes time away from getting started on the work you enjoy.\n\nBut instead of losing that effort, it can be captured into a documented knowledge base in your project. This knowledge base can be dynamic and evolve right alongside the codebase. Now, when you or your team start on a new feature, you have a map to quickly find the relevant files and get started.\n\nAll the information from the walkthrough has been collected into two documents that I can see as new artifacts alongside the transcript.\n\nWhen working with agents in Xcode, there are two distinct areas of information that you'll see. On the left, there is the transcript, this is where you see your conversation, including commentary on progress, tool calls, running sub-agents, and more.\n\nOn the right, we have artifacts. These are things that are created: files, edits, and previews. You can focus on everything produced in the latest message, or look at the entire conversation as a whole.\n\nAfter this exploration, I have a good understanding of the project in its current state. So now I can start working to add insights, with some tables to visualize them. I'm not completely familiar with how to implement tables in SwiftUI and the nuanced details of SwiftData models that will need to be adjusted. So, I'll use Xcode to research these APIs and gain an understanding. I'll create a new conversation for this. so I'll open the coding assistant in the sidebar, click new conversation, and choose an agent. I'll ask about the areas I'm unfamiliar with, like what relationships I'll need for the SwiftData Models, and what options I have for creating tables in SwiftUI. I'll also reference the two documents I created earlier.\n\nI'll send this prompt, and the two documents are read right away to gain context on the project. Since I asked about options for adding tables, Apple Document Search is invoked automatically to gain more understanding of SwiftUI table support.\n\nWhen adopting APIs or entirely new frameworks, having the most current information is important to get the best result. However, depending on the agent you're using, its base knowledge might not include the latest framework information. With Apple Document Search, your agent can access high quality documentation to help you ensure your project is adopting the right solution for the feature you're building. I now have a nice walkthrough of the APIs I'll want to use, complete with key details to consider that I might not have realized otherwise. Details like ensuring my SwiftData model relationships are right, and the best presentation of data on smaller screen sizes. With this, I have a good understanding of both the project and the APIs that I'll need to build out my feature.\n\nExploring with agents in Xcode is a fast way to familiarize yourself with a new project, or an unfamiliar feature of an existing one. Agents can see your entire project context, can supplement that context with high quality documentation from Apple, and can provide guided explanations for new areas, so you can get started faster than ever.\n\nNow that I have a good understanding of the project, I can start building towards my goal of adding insights.\n\nIn the same conversation, I'll put Xcode in plan mode by using the slash command. Plan mode allows you to be the architect, outlining your approach before any code gets written.\n\nI'll specify that I want to add a new tab for workout insights. I'll provide a high level overview of the features I want, outlining any requirements to make sure that this stays focused.\n\nI'll specify the device specific details learned from the explore phase for presentation.\n\nAnd finally, I'll ask for a preview to ensure the new tab looks like I expect.\n\nI'll send the prompt and enter plan mode. Xcode works with the agent to start planning how to implement the insights tab.\n\nWhen you build with agents in Xcode, you can turn ideas into features so quickly. Because of this, your workflow naturally shifts, ensuring you have the right plan, that captures your ideas and the architecture you want to build, becomes a more important step.\n\nWhen you focus on planning, you ensure that later on, refinement is spent polishing up the feature that you want rather than fixing a poorly built foundation.\n\nXcode gives you the tools to make this happen. It turns planning into a discussion, aligning on strategy before moving to implementation. Speaking of discussion, I have a general idea for what metrics I want to show. But I want to make sure I agree with what's included in the plan.\n\nI'll send a follow up, asking for some ideas. This queues up the message so that once the agent is finished with its current work it can address my question. Queuing messages like this allows me to express my ideas in the moment rather than waiting for the agent to finish what it's currently doing.\n\nXcode has submitted my message to the agent, and it's come back with some options to choose from. Looking at what it provids, I think that a per exercise view and a top level summary will be good presentations to start out with, so I'll choose those options and send my response.\n\nWhen you use queued messages to provide additional requirements, and the agent is able to ask questions for clarity, you create a tight communication loop between you and the agent. And because you're involved in the discussion throughout the process, you end up with a much stronger, well thought out plan.\n\nLooks like the plan is finished! We can see the full plan in markdown, which allows me to review it and make any edits that I need directly. This looks pretty thorough to me though, and captures everything discussed earlier. So I'll approve and Xcode starts working with the agent to implement the plan.\n\nThe first change has already been made, and we can see the exact diff of the modification as an artifact.\n\nSource code modifications and new files appear as artifacts as they're made and are added to your project. Allowing you to stay in sync and review the code changes to make sure they are what you expect.\n\nLooks like all the source changes have been made, and the agent has moved onto validation. Xcode's build tool is being used to ensure that the changes are correct and it looks like there are initially some issues. But since build errors are communicated directly to the agent, it knows exactly what failed, can quickly iterate, and build again to make sure it's right.\n\nThe agent is also updating our architecture documents with the new code, making sure this knowledge base stays up to date.\n\nNow, it's moved onto previews, after resolving some issues with the SwiftData changes, it renders a preview on the current run destination. Previews are also artifacts, so we can click on the rendered preview and take a look. It looks great, and is exactly what I was looking for on iPhone, a concise view with a top level summary. Xcode provides agents with the same tools that you have to ensure that new code does exactly what it's supposed to. From building the project, to rendering a preview of a new UI, you can have confidence that the code is correct.\n\nLet's take a look on iPad so we can see the new tables as well. I'll run this on an iPad simulator so I can see it on device hub. We have the new insights view with a collection of data about our recent training and most recent exercises. And we can see even more detail with the exercise breakdown table. Now that I'm happy with the UI that we see in the simulator, I want to make sure all the changes are built on a solid foundation. I'll ask Xcode to write some unit tests for the changes made to the swift data models.\n\nI can see my existing test suite is picked up for the project, so these tests will be grounded alongside the other testing that we already have.\n\nIn addition to build and preview, agents can use Xcode's test tools to ensure that the new code is correct by writing new tests or running existing ones. When agents are provided these opportunities to validate work as they go, you can focus on the high level goals for the feature.\n\nThe agent wrote a whole suite of tests to validate the new changes, and it was able to run and verify all twelve new test cases passed. Now we can know with confidence that the insights view is reliable.\n\nBuilding in Xcode lets you focus on the vision for a feature. You can align on ideas with plan mode, steer discussion in real time, view artifacts as they're produced, and use Xcode's validation tools to ensure new code is correct. Throughout every part of the development process, Xcode is with you to build it right. This is a great start. With Xcode I've been able to get the insights view prototyped in no time. Now, that we have this analytics data, I think the app needs a fresh perspective, from someone with an eye for detail. Hey Devin, the insights view is off to a great start, and already has some great analytics for previous workouts. I do think though that this app could really use some visualizations to show progress over time. Do you want to take over from here? Absolutely! I think Swift Charts would be perfect for that. Maxwell has left this project in a great state. We have a good starting place for adding some visualizations, so let's jump right in! in! So far, we've shown you how you can use agents to explore a new codebase, plan out features, and build them with ease. Now, we need to take the next step: getting things to look and feel exactly how we want. I really like what Maxwell has done on our analytics screen. To bring it to life, we're going to add some charts using Swift Charts.\n\nNow, how a chart looks, how animations feel, and what colors work with your app's style, these are subjective preferences that often change as you work. You can iterate on visual design incredibly fast, which is why staying in the loop for every change matters… making sure the end result reflects your vision.\n\nYou can communicate your intent with more than just text using images, sketches, and documents to show exactly what you have in mind. And when you know exactly where an adjustment belongs, you can point directly to that spot in your code with inline annotations resulting in focused, targeted changes. Let me show you what this looks like.\n\nI'm rather new to Swift Charts, so I'll start by exploring what chart styles would work for our data. Here's the prompt. I'm asking what options would work best for the Insights view.\n\nThese are all great options, but I want to see them in context. Let's generate previews for each chart type using artificial workout data.\n\nWith Xcode, we go from text descriptions to working prototypes in seconds, so instead of just imagining what a chart might might look like we just look at it. Now I can see what these charts actually look like with realistic data. The \"volume over time\" option is the clear winner. It shows our progress in the weight room at a glance, which is exactly what we want.\n\nThe previews look great, but there are still a few things I'd like to personalize. Let me grab my iPad to sketch out a museum worthy chart design. I have Freeform open on my iPad let me sketch out what I'm picturing.\n\nWow, that really is museum worthy. Yeah, it's really coming together.\n\nOk, maybe I shouldn't quit my day job but thankfully, I think this gets the idea across! Now I'll send my sketch and ask for a line chart to be added directly to the InsightsView, matching the style I've drawn. While that's being implemented, let me explain what's happening behind the scenes. With access to previews in Xcode, agents don't just generate code and stop. Previews can be rendered incrementally to visually verify results, confirming that what was generated matches what you asked for. If something doesn't look right, adjustments are made before you even need to step in.\n\nAnd here's what that looks like in practice. My sketch was interpreted and translated into a chart that matches exactly what I was looking for. You can see that a preview was rendered along the way that self-verification loop in action.\n\nThis is looking great! There are still a couple of things I'd like to tweak though. I want to add a subtle animation to the chart, and change the color scheme to match our app's theme. I know exactly where these changes should go, and with inline annotations, I can point to that exact spot in my code.\n\nI'll leave two annotations right in the chart view. One here, asking to add a fade-in animation.\n\nAnd another here, asking to adjust the trend line color to match our theme Inline annotations carry something that a typical conversation prompt doesn't, the exact location in your code where you want to make a change.\n\nWhen you annotate a specific line, the surrounding code becomes part of the context. The annotation doesn't just say what to change, it shows where, so the result is precise and scoped to exactly what you intended.\n\nThis looks incredible.\n\nEvery step of that process kept us in the driver's seat. We chose the chart type. We sketched the design. We pointed to exactly where the final adjustments belonged. The creative direction was ours the entire time. In Xcode, you have the tools to make refinement even better: Rich previews to see changes with realistic data; Inline annotations to direct your changes right from your source code; and image Attachments to show exactly what you have in mind. For more on design-focused techniques for working with agents, check out \"Create UI Prototypes using Agents in XCode\" session. Next, I'll show you how to orchestrate comprehensive feature development by translating our app into new languages, and making it accessible.\n\nFor Maxwell and me, it's really important that the apps we develop are accessible. A friend of ours at the gym primarily speaks Filipino, and we'd love for him to be able to use this app just as easily as we can. Accessibility and localization are how you make sure your app works for everyone. And with agents, incorporating these features into your app is easier than ever. Xcode provides a rich set of tools that extend what you can accomplish. Maxwell and I have already shown you tools like Document Search, Preview Rendering, and Run All Tests in action but there are many more. Some are built into Xcode, some are provided by Apple framework teams, and you can even add your own. The right tools are discovered and used automatically based on the task at hand. I'll start a new conversation for localization, asking to translate every user-facing string into Filipino and configure the strings catalog. I'll send this off. You can see the machine translation tools being discovered. These provide the context needed to translate our app, and the work can now begin.\n\nThis is orchestration in action. I described a high-level goal: \"localize the app into Filipino\" and the right tools were discovered automatically. From here, the work gets broken into parallel pieces, and smaller sub-agents are deployed to located and translate strings across the app.\n\nWhile localization runs, I'll start a second conversation for accessibility, asking to add VoiceOver labels and accessibility identifiers to all interactive elements.\n\nThere are now multiple workflows running at once. Accessibility is implementing changes across our views, and localization is coordinating translation through sub-agents. What's interesting about localization is that the translation tools aren't being called by us directly they're invoked by the sub-agents under the hood. The main conversation read those tools for context, broke the work into pieces, and each subagent is now calling on specific translation capabilities as it works through its portion of the app.\n\nThis is the power of orchestration: you describe the goal, and the right tools are used at the right level some by the main workflow for planning, others by sub-agents for execution. And through all of this, you can check in on progress at any time. Now it's time for the most important part, reviewing the results. The app is localized in Filipino. Our 'Start Workout' button, the 'History' tab, the 'Insights' section, all translated. And the strings catalog has entries for every user-facing string in the app. Let's enable VoiceOver.\n\n\"VoiceOver On\" \"Start New Workout button\" \"Landscape\" \"Charge port to the right\" \"Start New Workout\" \"Workouts\" \"Back button\" \"Workouts\" \"Heading\" \"VoiceOver Off\" Every primary element is labeled and navigable. What would have previously been hours of repetitive work was accomplished across two parallel conversations. And we stayed in control the entire time.\n\nWorking with agents in Xcode puts you in the driver's seat. Orchestration is where you get to describe high-level goals, and Xcode determines how to accomplish them discovering the right tools, coordinating sub-agents, and completing tasks in parallel. And through powerful Xcode tools, you can direct impactful work like localization and accessibility with a single prompt. This app is really starting to come together. For sure! The analytics look great, and the charts really help us see our progress at a glance. Using Xcode made it easy to get off the ground quickly, and there's still so much that we could add. Throughout this session, Xcode supported how each of us needed to work, and kept us in the loop at each step.\n\nWith Xcode, you can work the way you want to with agents whether that's exploring, building, refining, or orchestrating. Let's reflect on everything that we used to build out this new feature. It started with exploration, using Xcode Tools and Apple Document Search to map out the code base and learn about the APIs that we wanted to use.\n\nWhen it was time to build, we used plan mode to design an architecture before writing any code, and relied on queued messages and agent questions to align with the agent on the ideas.\n\nOnce we had a prototyped feature, we used Xcode's Build, Preview, and Test tools to verify our work.\n\nWhen we refined the UI, we attached images to express ideas, inline Annotations to guide changes grounded in the source code, and Previews to verify the appearance.\n\nAnd finally, we orchestrated larger goals, leveraging tools for localization and accessibility, with sub-agents working in parallel.\n\nHere's what you can do next. Download Xcode 27 and start using agents on your own project. Explore the agentic tools available in Xcode, add your own, and check out the \"Create UI Prototypes using Agents in Xcode\" and \"Translate your app with agents in Xcode\" sessions for a deeper dive into the concepts we discussed today. Now we both have time to head to the gym and test out the new features. Meet you there! Thanks for watching!",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Writing code with intelligence in Xcode",
+ "url": "https://developer.apple.com/documentation/Xcode/writing-code-with-intelligence-in-xcode"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/259/4/f4d40bb5-32db-418f-8a6e-396c77044afb/downloads/wwdc2026-259_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/259/4/f4d40bb5-32db-418f-8a6e-396c77044afb/downloads/wwdc2026-259_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "227",
+ "year": "2026",
+ "title": "Create UI prototypes using agents in Xcode",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/227"
+ },
+ {
+ "id": "213",
+ "year": "2026",
+ "title": "Translate your app using agents in Xcode",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/213"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:16.066Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-260.json b/data/wwdc/videos/2026-260.json
new file mode 100644
index 0000000..f51e5e4
--- /dev/null
+++ b/data/wwdc/videos/2026-260.json
@@ -0,0 +1,49 @@
+{
+ "id": "260",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/260/",
+ "title": "Get the most out of Device Hub",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "I'm Matt, and I am an engineer on the Devices team. Later, we'll be joined by my colleague - Hassan. We know how important it is to you that your app looks great everywhere - on every device, at every appearance, at every text size, and more. And we also know - that can be a lot to account for. So, in this session, we'll introduce you to Device Hub, an app that makes this easy and we'll walk through everything it can do. Here's our plan. First, an introduction to Device Hub - an app that is your home for working with both devices and simulators. Then, a feature walkthrough - we'll learn how to use it to organize, control, and configure your devices. Finally, you'll see it in action. We'll show you an example. Hassan and I will use Device Hub in a real workflow.\n\nLet's get started. This is Device Hub. It's an app that ships alongside Xcode 27. But you don't need to launch Xcode to use it because it's built for anyone who works with devices, whether you're developing an app, testing across device configurations, or managing an inventory of devices.\n\nIt offers the same set of tools - whether you're working with a device or a simulator. You can configure either of them however you need.\n\nIf you're developing an app you'll encounter Device Hub right away. When you build and run to a simulator in Xcode it's automatically launched. It presents a window with a live, interactive view of the screen.\n\nThis is Device Hub in compact mode. It's a focused and lightweight window. Just the screen and a few essentials.\n\nAt the bottom are device controls. Things like the home button, screenshots, and rotation. And, they're contextual, so - they change depending on the device you're viewing. For example, on an Apple TV you'll find controls for both play/pause and navigation. On an Apple Vision Pro - you'll see controls for environment and camera movement. Or, on an Apple Watch, you'll find both the side button - and the Digital Crown. But Device Hub is much more than this. When these essentials aren't enough, click the expand button at the top to transition to the full window. Here, you'll have access to a much broader set of tools. There is a lot going on here in the full window. So, let's walk through all of its features.\n\nThey fall into three areas: the ability to control, organize, and configure your devices. To tell you more about how to control your devices, I'll hand it over to Hassan.\n\nThanks Matt! When developing, my devices can be all over the place, and driving the device from my screen can help me stay focused on my work. Device Hub makes this easier for me by giving me Advanced features to control my devices, They can be found in the canvas at the center of the window. There is a live display of the screen. You can interact with it directly. Click, drag, scroll or use natural trackpad gestures. This works the same whether it's a device or simulator. Take a look here. I have my Apple Watch connected. See how I can control it using Device Hub.\n\nAt the bottom there are the same controls as the compact window. And just like them, they are contextual and will change depending on the device. Above the canvas there are controls that are not in the compact window. These allow you to zoom in and out, or snap to one-to-one physical size to see your app at its real world dimensions.\n\nResize mode, allows you to transform your app's dimensions freely. And that will be discussed in the \"Modernize your UIKit app\" session. Capture keyboard routes your Mac's keystrokes straight to the device. Making it easy to test key commands and hardware support. And a button allowing you to switch back to compact mode. So, that's the canvas. It's a powerful way to interact with your devices right from Device Hub. Back to Matt, to talk to you about how to organize and configure your devices. Thanks Hassan. I work with dozens of devices and simulators and it can become hard to keep track of all of them. Fortunately Device Hub has tools both to organize them and offer quick access.\n\nThey can be found in the sidebar. In it - you can see your full inventory. That's each of your devices and simulators all in one place.\n\nUse the filter menu at the top to change which are visible. Or use it to sort and group your inventory from several different options.\n\nContext-click any device for quick actions. Things like restarting or pairing an iPhone and Apple Watch simulator together. For quick access, you can also view any number of devices at once using either tabs or stand-alone compact windows. For example, say you are building an iOS app, and you want to make sure it looks correct on a few different phone sizes. Select them all, double click them in the sidebar, and you'll get a compact window for each. Making it easy to compare your app across screen sizes. So, I can organize my devices using the tools in the sidebar. But, testing different text sizes, simulating different locations, or installing different profiles - that can still take a lot of time.\n\nSo, Device Hub offers deep control over device configuration. It has five different panels to do this.\n\nAnd, they can be found in the inspector area - on the right.\n\nThe first tab contains device settings. Here you can change how your device looks and behaves. The first section contains appearance options. Things like dark mode, text size, and more. And these changes take effect instantly. So there's no need to dig through your device's settings. The second, lets you test how your app responds to different conditions, like a change in location. And the third has audio options for sound levels and I/O.\n\nThe middle tab contains diagnostic reports. It's where you will start investigating if your app hangs or crashes. It contains everything your device has logged - such as crashes, spins, and other diagnostics.\n\nThe third tab has three panels: device Info, Apps, and Profiles. Let's go through each.\n\nThe Info panel contains things such as storage, model, and serial number - stuff that you need at a glance.\n\nThe Apps panel lets you install, uninstall, and manage apps, including downloading and replacing their data containers.\n\nAnd, in the Profiles panel you can manage both configuration and provisioning profiles.\n\nSo, that completes our feature walkthrough of Device Hub. It's a feature rich app, and we've covered a lot of ground. From organizing your devices in the sidebar, to controlling them from the canvas, to configuring them with five panels in the inspector.\n\nNow, I'll hand it over to Hassan, - to show you how Device Hub fits into our development workflows. We are going to walk through a scenario: finding a bug on a real device and reproducing it on a simulator. Thanks Matt! Now let's see it all come together.\n\nMatt and I have been building a feature in a workout app.\n\nWe will show users recovery advice, based of the altitude of their current location. But this feature has a bug! For example, in landscape mode some of the text gets cut off.\n\nSo let's go through my workflow. What features of Device Hub that I use the most? What do I do when I see an issue like this one? There are three things I focus on. Pairing devices, ensuring that I have enough logs, and capturing diagnostics. Our Workout app runs on the Apple Watch and the iPhone. Using Device Hub I can wirelessly pair both of them to my Mac. My iPhone is already paired. Let's look at how I would pair an Apple Watch.\n\nI'll click the add button in the sidebar and choose Pair Nearby Device.\n\nThen I'll follow the instructions on the screen.\n\nMy Mac shows up right away on my Apple Watch. I'll select it, tap pair, and finally enter the pin number that appears in Device Hub.\n\nAnd that's it. My Watch is paired. It shows up right here in the sidebar, just like my iPhone. And from now on whenever it's nearby it'll be available on Device Hub.\n\nNext, since we are working on a location-based feature, I would like to have the CoreLocation logging profile installed. Let's take a look at how to add a configuration profile. I'll go to the Profiles panel, drag and drop the configuration profile and confirm the installation on my iPhone.\n\nThen, I'll make sure to reboot my iPhone for privacy reasons.\n\nFinally, Matt is our UI engineer, so he'll be the best person to investigate this issue. I just don't know what he will need to debug it. So I'm going to collect as many diagnostics as possible. Let's take a look at how to do that. After building and running the app from Xcode, I can see and interact with the app right here on my Mac.\n\nI need to reproduce the bug, so I will rotate the device.\n\nHere's the bug! The recovery recommendation is getting cut off! So first, let me take a screenshot of the bug, so Matt can see that our UI is broken.\n\nNext, in case Matt needs to dig deeper, I'll kick off a sysdiagnose to capture system-level diagnostics.\n\nThat'll take some time. While it's running, I'll send the text that's getting clipped to Matt.\n\nI'll select it right here on the device, copy it and paste it into a file on my Mac.\n\nMatt might need my app's data to be able to reproduce this bug. And working with app data containers is a common part of my workflow.\n\nDevice Hub gives you everything you need to work with app data containers.\n\nAs of now you can inspect a saved state in Finder, restore to a known baseline, or capturing a snapshot for later.\n\nHere's all of the data my app has stored.\n\nI will download that, so Matt can load it into his simulator.\n\nThat should be enough. Let's send this all over to Matt so he can take a look. Matt, this UI issue is all you.\n\nThanks Hassan. So, when I see UI issues like this, that don't affect performance, I like to reproduce them using simulators. There's a few steps that I like to take to do this.\n\nFirst, I'll need to choose the correct simulator, then match the app's data, and finally mirror the device's configuration. Let's start by selecting a simulator.\n\nHassan was using an iPhone 17e, and I don't have one. But, I do have a corresponding simulator. I have it here in the sidebar. I'll select it, and verify it says iPhone 17e in the info panel.\n\nNext, I want to ensure that I'm using the same data as the app that reported the bug. Just in case any of it is problematic. So, I need my app to match his app's data. My version of the app is empty. But, Hassan sent me his data container, so I'll go to the Apps inspector and replace mine with his.\n\nNow, when I relaunch the app I can see all of his workouts.\n\nFinally, I want to mirror his config. I want my simulator's settings to match his device's, as much as possible.\n\nLet's go ahead and do that and match any appearance or accessibility settings that could affect the UI. To start, looking at the screenshot he sent over, I can see his phone was in landscape mode. So, I'll mirror that. I can rotate my simulator using the device controls.\n\nNext, I see that his location is set to Johannesburg. It seems like at that high elevation we have a long string for a recovery suggestion. So, I'll simulate his location by navigating to the settings inspector and selecting Johannesburg.\n\nNow, it doesn't seem like we are seeing the same text get truncated like he was. So, something else must be different. Taking another look at the screenshot.\n\nI can see his text size is pretty large. So that might be it, let's match that as well. I'll bump it up and yup! Now we can see that truncation happening. And, I bet if I rotate it back it doesn't happen. So, this bug seems to have been a rather striking confluence of things. The device had to be in landscape, it needed to be at a specific location, and his text size needed to be all the way up. Only with all of these were we able to reproduce the issue. Device Hub let me do all of it. Now, with it reproduced, it is easier to go verify a fix. So that was an example of a workflow: Hassan found a bug, and I reproduced it. All using Device Hub. Thanks for those great diagnostics Hassan! Of course, Matt! Whether you're working with a simulator or a device, Device Hub gives you a consistent experience across both. From live screens and hardware controls to app management, appearance settings, and more. And when something goes wrong, Device Hub gives you everything you need to collect diagnostics and investigate issues But that's not all! For scripts and automation, use devicectl. It's a command line tool based on the same underlying technology as Device Hub. It's great for managing devices in a test environment, managing apps, capturing diagnostics, and more. For example, you can use devicectl to list your devices, install an app onto your device, change settings such as switching between dark and light modes, or getting more information about one of your devices. And if you want a structured output, you can use the json-output option to easily integrate into scripts or CI workflows.\n\nThere's a lot more to explore. Download Xcode 27 and try Device Hub for yourself. If you want to integrate into scripts or CI workflows, check out devicectl. See how Device Hub can help you improve your app for resizability in the \"Modernize your UIKit app\" session. Check out \"Getting the Most Out of Simulator\" from WWDC 2019. And for everything else, we've linked the Device Hub documentation in the video description.\n\nThank you for watching. We can't wait to see what you build next.",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Device Hub",
+ "url": "https://developer.apple.com/documentation/Xcode/device-hub"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/260/4/87d4b48f-1dfb-413f-a4f8-44d80b0f3432/downloads/wwdc2026-260_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/260/4/87d4b48f-1dfb-413f-a4f8-44d80b0f3432/downloads/wwdc2026-260_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "278",
+ "year": "2026",
+ "title": "Modernize your UIKit app",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/278"
+ },
+ {
+ "id": "258",
+ "year": "2026",
+ "title": "What’s new in Xcode 27",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/258"
+ },
+ {
+ "id": "418",
+ "year": "2019",
+ "title": "Getting the Most Out of Simulator",
+ "url": "https://developer.apple.com/videos/play/wwdc2019/418"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:16.291Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-261.json b/data/wwdc/videos/2026-261.json
new file mode 100644
index 0000000..53c0536
--- /dev/null
+++ b/data/wwdc/videos/2026-261.json
@@ -0,0 +1,50 @@
+{
+ "id": "261",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/261/",
+ "title": "Build, deliver, and automate with Xcode Cloud",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hi, I'm Tony, an engineer on the Xcode Cloud team. Xcode Cloud is a continuous integration and delivery service built into Xcode and designed expressly for Apple developers. It builds and tests your app in the cloud and offers a seamless way to set up distribution so you can take your app to TestFlight or to the App Store! This year, we focused on improving the fundamentals of Xcode Cloud. It doesn't matter if you are just starting with an app idea or if you have been using Xcode Cloud since day one. You'll find meaningful refinements every step of the way, from building and testing, to distribution. Today, we'll touch on some essential concepts. Then, I'll show you how seamless it is to get started with Xcode Cloud with a new app I am building. Next, we'll set up distribution so we can collect feedback from users. And finally, we'll take a peak at how webhooks and built-in repository management tools can extend your workflows and take you to the next level. First, let's ground ourselves on some basic concepts. App development is constantly changing, and this year is no exception. With support for agents arriving in Xcode, a lot of developers are writing more code than ever before, iterating quickly on features and changes, and delivering more value to users, and that's awesome! But how do you scale and keep up with the rapid pace of code, new features, and ensure you can catch bugs and performance issues before they reach customer devices? Xcode Cloud is your companion for quality. Compared to local development, Xcode Cloud builds and tests your app in the cloud in parallel across multiple devices and OS versions. Then, when you are ready to get feedback from testers, it is so simple to setup distribution, deliver builds to TestFlight and to the app store. Time to get started! As a full time engineer, I'm always looking for interesting problems to solve, but I'm also a part time barista. I love making coffee in the office, and my coworkers have taken notice. I've been making coffee for them for a while now, but it's starting to become difficult to track all the orders and preferences. To help operate my \"office\" coffee shop, I've been working on an iOS app. As the app grows, so does the test suite; unit tests for business logic, UI tests to make sure critical flows like checkout never regress. Running all of these tests locally after every change is starting to take real time. Time that I could spend working on adding new functionalities or responding to user feedback. Time to bring in Xcode Cloud, so that builds and tests are automated, and I am no longer constrained to my local machine! This is my app in Xcode.\n\nTo get started, I'll navigate to the Report navigator, and select the tab called Cloud.\n\nI'll choose Get Started… and I see all the products in my workspace. Right now there's only one. The Developer Team is already set to match my Signing & Distribution settings, so I'll go ahead and click Next.\n\nIn order to build my app, Xcode Cloud needs to access the source code. The Onboarding assistant will load my repository, and I will just need to follow the steps to connect. Depending on your source provider, the steps may vary. To learn more about connecting your project, check out Connect your project to Xcode Cloud.\n\nGreat, we're connected! But before I click Next, let's talk about your source code.\n\nXcode Cloud Builds run on ephemeral virtual machines. Your source code is only fetched when a build starts, and once your build is done, it's thrown away. None of the source code is ever stored and Apple has no way of accessing it.\n\nBack to Xcode. I'll click Next, and we're done! Xcode Cloud has created my product and a default workflow, which I'm happy with right now. It's only one more click to start my first build! Expand onboarded product, look, our first build is running. Nice! My iPhone app is onboarded, but when working in the office, I am often at my MacBook, and I want a way to see the orders come in and manage them. So I built a macOS app just for that. Let's see how straightforward it is to start onboarding that with Xcode Cloud.\n\nMy macOS app is located in the same Xcode workspace, as it shares a style framework I've built for it. Move to the Report navigator again, and choose the More button in the bottom left of the navigator.\n\nFrom here, I'll select Create Workflow… Now I can see my macOS app in the Assistant, as well as my onboarded iOS app. I'll pick macOS app and click Next.\n\nSince we have previously allowed Xcode Cloud to access the remote repository, there is no Connect Repository step again.\n\nSetup Complete. Select MacOS branch, let's start our first build.\n\nThe cloud section now shows me builds and workflows for both of my apps. This is a great staring point, but it's just the beginning. I can build out more complex workflows covering more scenarios, more platforms, and more edge cases across both of my apps. If you want to learn more about how to utilize workflows to help ensure quality of your app, Check out Create practical workflows in Xcode Cloud for more.\n\nNext, let's talk about a crucial step of bringing your ideas to life - distribution. I have two great apps running builds and tests in Xcode Cloud helping me ensure quality, but some of my users are eager to test out new features early and provide feedback. TestFlight is great at this, and Xcode Cloud makes it simple to integrate. In the Cloud Navigator, I'll find my iOS app and secondary click it.\n\nI will select Set Up Distribution...\n\nXcode Cloud needs content to create my official app record on App Store Connect. Here, I need to provide a few properties, like my app name, the Bundle ID which is a unique identifier, and the SKU for the store. Xcode Cloud tells you when these properties are taken already, and you can change it here without leaving the Assistant. I can see my app name is taken, so I'll need to be creative here.\n\nOk, good to create.\n\nIt will take a couple of seconds to onboard it to distribution. Xcode Cloud will create app record, verify, then register Bundle ID and SKU all at background.\n\nBack to Xcode... Nice! I'm set up for distribution.\n\nA new distribution workflow has been created for internal TestFlight, ready for me to start anytime. This is one way to setup distribution, but there's one more way I want to show you. I haven't set up distribution for my macOS app yet, and I want to create a new workflow for it. Secondary click the app and choose Manage Workflows… Press the plus sign at the bottom of the workflow manager.\n\nHere, I need to create an archive action in this new workflow, as this is a required action to distribute to TestFlight.\n\nAs I am attempting to do this, I'm offered a way to set this up. Select Set Up...\n\nI'll review all the properties and click Create.\n\nXcode Cloud will follow the same process to onboard my MacOS app to distribution.\n\nJust like that, I'm done again. Building, Testing, and Distribution is at the core of Xcode Cloud, but you can do so much more with it. My colleagues are really enjoying ordering coffee and have been providing me great feedback about the app. Because a few of them are engineers too, they've been tracking their own bugs as they are fixed. I think we can do way better than manually tracking by leveraging webhooks.\n\nWebhooks are the perfect tool for building advanced automations. When a webhook is configured, Xcode Cloud automatically sends a payload containing information about your build to a service of your choice.\n\nThere are hooks for every stage; when a build is created, when it starts, and when it's completed. This is going to be perfect for my dashboard I'm building. To setup this automation, I'll find the iOS app, secondary click it, and select Manage Webhooks...\n\nHere I'll add a new webhook.\n\nI'll name the webhook, \"Dashboard,\" and for the Payload URL, I'll enter a publicly resolvable endpoint, and click Add.\n\nOnce configured, it shows up in my webhook list now with no delivery history. Let's start a new build to test out webhook configuration.\n\nThe build just finished. Navigate back to the Webhooks view.\n\nI can see events of webhooks being sent to my dashboard. There are three here that correspond to the webhook lifecycle, and the green indicates successful deliveries. You can learn more about webhooks in our documentation. My Coffee app is gaining traction around the office. TestFlight user feedback is now full of great ideas, feature requests, and more. As the app grows, so does its complexity. A common practice at this stage is to split out code into separate repositories, especially if functionality is shared. This keeps your codebase modular and easier to maintain as your app scales. I've recently done exactly that — splitting my style framework into its own repository. It's important to make sure Xcode Cloud stays in sync with new access change. Let's include that style framework in the project. I'll secondary click iOS app again, and select Manage Repositories...\n\nThe repository setting page opens up with my primary repository already at the top. The Additional section is empty. I'll click Add, and paste in Git remote URL for my style framework, then click Add.\n\nSince I've already granted Xcode Cloud access to my remote provider, I don't need to grant authorization again. Xcode Cloud can now build the project with all the right dependencies in place.\n\nNow, every new feature I push is automatically built, tested, and deployed. My team gets notified the moment anything changes, and every build has access to exactly the right dependencies. Time to try it yourself! Revisit Xcode Cloud concepts, onboard your app to build, test, and distribute. Then take it further — configure webhooks and additional repositories to automate your workflow and scale your project. To learn even more about Xcode Cloud check out Extend your Xcode Cloud workflows and Simplify distribution in Xcode and Xcode Cloud. Xcode Cloud builds your confidence to ship with quality. You can move faster and focus on the important matters, worry free, so relax, sit back, and enjoy a great cup of coffee...\n\n...while it's still hot. Thanks for watching.",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/261/7/35c49f2b-3f0a-4956-826b-d54d9fed678e/downloads/wwdc2026-261_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/261/7/35c49f2b-3f0a-4956-826b-d54d9fed678e/downloads/wwdc2026-261_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "258",
+ "year": "2026",
+ "title": "What’s new in Xcode 27",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/258"
+ },
+ {
+ "id": "10200",
+ "year": "2024",
+ "title": "Extend your Xcode Cloud workflows",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10200"
+ },
+ {
+ "id": "10278",
+ "year": "2023",
+ "title": "Create practical workflows in Xcode Cloud",
+ "url": "https://developer.apple.com/videos/play/wwdc2023/10278"
+ },
+ {
+ "id": "10224",
+ "year": "2023",
+ "title": "Simplify distribution in Xcode and Xcode Cloud",
+ "url": "https://developer.apple.com/videos/play/wwdc2023/10224"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:16.339Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-262.json b/data/wwdc/videos/2026-262.json
new file mode 100644
index 0000000..b5cb548
--- /dev/null
+++ b/data/wwdc/videos/2026-262.json
@@ -0,0 +1,264 @@
+{
+ "id": "262",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/262/",
+ "title": "What’s new in Swift",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Becca from the Swift team. My teammate Evan and I are here to tell you about some of the improvements we've made during the development of Swift 6.3 and 6.4. I'll start by showing you some changes to the language that'll streamline your day-to-day coding.\n\nThen Evan will discuss updates to important libraries and developments in support beyond Xcode and the Apple platforms.\n\nAfter that, I'll walk you through some specialized features for tuning performance-sensitive code without compromising on safety.\n\nThen Evan will finish up by talking about how you can follow along or contribute to Swift's development.\n\nBut let's start with some of the simple improvements that you might use every day.\n\nSome of them you might barely notice except as little annoyances going away, so let's look at some of the smaller changes first.\n\nFor example, previously if you wanted to use some or any with an optional type, you had to parenthesize it.\n\nThis behavior fell out of Swift's operator precedence rules, but it was a little pedantic. So, in Swift 6.4, you can simply get rid of those parentheses and the compiler will take that to mean the only thing that really makes sense.\n\nYou'll now get a warning if you silently ignore an error thrown from a Swift Concurrency task, reminding you to either handle the error in the task, or save the task and check for the error later. And the old restriction on calling async functions from a defer block is now gone. If you have a class that needs to use @unchecked Sendable because it has a weak var property, you can now change that property to weak let so it's immutable and doesn't stop you from using Sendable checking.\n\nOr if a type shouldn't be Sendable, you can state that explicitly with the new tilde Sendable syntax. Which, as an added bonus, doesn't stop subclasses from being Sendable.\n\nAnd a struct with a mix of internal and private properties will now have a second memberwise initializer that you can use from other files in your project. But there are also some changes you'll definitely notice.\n\nThe Apple ecosystem has grown over the last two decades, and as it has, the availability attributes in your Swift code have grown with it. Last year, Apple began to address this problem by aligning the version numbers of our OS releases. Now, Swift is taking that even further by letting you condense all of those platform names into one: \"Any Apple OS\". So if the availability lines up on all of the platforms you care about, you can specify all of the OSes at once, or if there are carve-outs, you can use anyAppleOS to establish the default and then add more specific attributes for the exceptions. And this also works for #if os conditions if you want to compile a section of your code out entirely.\n\nAnother thing that changes over the years is APIs.\n\nAs they evolve, older APIs are sometimes deprecated to indicate that you should move to newer versions. But sometimes you can't do that right away, maybe the new API is very different and you need time to adapt.\n\nWouldn't it be nice to turn off these warnings temporarily without affecting the rest of your project? Well, now you can. The @diagnose attribute lets you change the behavior of specific warnings inside a particular declaration. So you can tell Swift to ignore the warning group deprecated declaration and make that kind of warning go away in that one place.\n\nYou can also use it to selectively enable warnings that are off by default.\n\nFor example, you could turn on strict memory safety in security-critical functions to make sure you've audited their uses of unsafe APIs.\n\nOr you can treat certain warnings as errors.\n\nHere, we've upgraded warnings that will become errors in the future to errors right now.\n\nAll that gives you a lot of flexibility.\n\nAnd Swift 6.3 also has a new feature to better handle situations where two modules have APIs with the same name. For example, the Rocket module might have a type representing a real, hundred-meter-tall SaturnV rocket, while the GiftShopToys module has a type representing a child-size model of a SaturnV rocket. If you've imported both of these modules in the same file, Swift won't be able to tell which one you wanted to use, so it'll give you an error.\n\nWhen this happens, you've always been able to clarify your code by using the dot syntax, Rocket.SaturnV will first find the Rocket module, then look for the type SaturnV inside it, without ever noticing that there's another SaturnV somewhere else.\n\nThis usually works, but there are some tricky situations where it breaks down. For example, what happens if the module Rocket also has a type Rocket? Well, I'll tell you what happens, Swift prefers type names over module names, so it decides you must mean the type Rocket and then looks inside it for a member called SaturnV.\n\nWhen it doesn't find one, it gives you an error. Swift 6.3 has a solution to this problem, just change that dot to a double colon. This new syntax is called a module selector, and the name on the left is always treated as a module name. So Swift will ignore the type Rocket and go straight to the module Rocket, and from there, it'll have no trouble finding the type you're looking for. This syntax also works on the name of a method or property, which is handy if a type gets identically-named methods from extensions in two different modules. After all, calling the wrong method could really ruin someone's day.\n\nNow, module selectors are super useful when there's a conflict between two modules you don't control.\n\nLike, if SwiftUI and the database package you're using both have a type called View, a module selector can really save you some trouble.\n\nIt can also be a good idea to use them defensively in macro expansions and other automatically-generated code, since you don't know what else might have been imported into the project. But, we don't recommend you intentionally design your APIs to have name conflicts and then rely on module selectors to distinguish them. Even if your code is unambiguous, some of the error messages and documentation might still be confusing.\n\nSo... don't.\n\nNow that we've seen what's new in the language, here's Evan to tell you about updates to important libraries.\n\nAll of these language improvements share the same goal of helping you express exactly what you mean without extra noise. That same goal carries into updates to the libraries you use every day.\n\nThe standard library, Swift testing, Subprocess, and Foundation.\n\nLet's start with updates to the standard library, specifically with new tools for task cancellation, dictionary transformations, and filepath manipulations.\n\nIt's important to check the task cancellation status before starting any expensive work, but there are times where we actually want to do the work even after the task was cancelled, like finishing writing data to disk to avoid corrupting a file.\n\nNow you can use the task cancellation shield. Inside of the shield, task cancellation checks always return false. It's important to keep this region short, focusing on either finishing or rolling back any work that you already started. Mapping values from dictionaries also received an update. mapValues only passes the old value into the mapping closure, so if you needed the key to compute the new value, you had to construct a new dictionary by hand.\n\nNow, we can call mapKeyedValues, which passes both the key and the old value into the mapping closure to compute the new value.\n\nPrograms often need to manipulate filepaths. There are subtle differences between how different platforms represent filepaths that can make it tricky to handle them correctly. This year, we added a new filepath type to the standard library, based on the type from Swift System, making it easier to get right.\n\nTesting in Swift 6.4 offers you more control over the behavior of your tests, allowing you to surface non-fatal issues and dynamically skip tests cases.\n\nNow you can set the severity level of issues recorded with Issue.record.\n\nSetting it to a warning means you can surface issues in a test case that are worth investigating, but not worth blocking CI workflows.\n\nYou can cancel tests dynamically by calling the Test.cancel API.\n\nThis is especially powerful with parameterized tests, where you can cancel individual arguments that shouldn't run, rather than running them to completion or failing the test.\n\nSometimes you're trying to tackle a flaky test. The swift test command adds new functionality to repeat a test until it either passes or fails, while also allowing you to control the maximum number of repetitions. If you specify that you want to repeat until the tests pass, only failing tests are re-run, saving time by not re-running tests that are already passing.\n\nMany projects have large test suites that have utilities built on XCTest APIs. In Swift 6.4, XCTest assertion failures are now reported as test issues when called from Swift Testing.\n\nThis means that you can migrate to Swift Testing without worrying about accidentally losing test coverage along the way. And the interoperability also works in the other direction too, Swift Testing APIs, like the #expect macro work when called from an XCTestCase. This means you can build helper APIs with Swift Testing, and they'll have consistent behavior regardless of whether they're called from XCTest or Swift Testing.\n\nIf you're working in an existing project, you might already be calling XCTest assertions from Swift Testing and vice-versa.\n\nTo make this transition easier, these issues are reported as warnings by default. You can opt into promoting them to test failures in the Xcode build settings.\n\nTo learn more about Swift Testing interoperability and migration strategies, check out \"Migrate to Swift Testing\".\n\nLast year, we announced the Subprocess package containing modern APIs for launching subprocesses. This year, we are releasing Subprocess 1.0, incorporating your feedback from real-world use.\n\nThe API refinements include a simplified execution type, improved error handling, and convenience APIs for easily streaming process output.\n\nCross-platform support is also greatly improved, including platform-specific process file descriptors and termination statuses, to more accurately reflect the semantics on different platforms. Here is an example of the refined API in Subprocess 1.0.\n\nWhen calling the run method to launch a subprocess, the standard output and standard error streams can be included in the execution object as AsyncBufferSequences. This model guarantees that each stream is only created once. The new strings() method on AsyncBufferSequence makes it easy to read the output of a subprocess line-by-line.\n\nThis API respects graphene cluster boundaries, so you don't have to worry about multi-byte characters getting split.\n\nFinally, let's talk about improvements to Foundation.\n\nProgressManager is a new type in Foundation for progress reporting. It's designed to work well with async/await style concurrency it cleanly separates progress composition from progress reporting, and provides a structured, type-safe mechanism for attaching additional metadata.\n\nWe announced Swift-Foundation two years ago, an effort to migrate Foundation to a safe, consistent, cross-platform codebase, written in Swift. This year, we continued that effort, replacing decades of Objective-C with modern Swift.\n\nThis year, we modernized more parts of Data, resulting in improvements across the board, including faster span accesses, equality checks, iteration, and mutation.\n\nOn Apple platforms, bridging between Data and NSData is also faster.\n\nNSURL and CFURL were unified to a single Swift implementation.\n\nLeveraging Swift makes these types run faster and use less memory. Migrating a library as large and mature as Foundation is only possible because of Swift's language interoperability.\n\nYou can add entirely new APIs in Swift, and you can migrate the implementation of existing APIs to Swift without changing the API surface.\n\nIn addition to crossing language boundaries, many real-world projects extend into services, devices, web components, cross-platform clients, and more. At Apple, we use Swift in every part of the OS, in apps like Weather, in services like the realtime phone call spam detection, in the kernel itself, and all of the way down to the lowest layers of firmware. Swift is designed to be a language that you can reach for across every layer of your software stack.\n\nTo make it easier to use in your existing software systems, Swift 6.4 extends language interoperability, improves cross-platform IDE support, and makes it easier and safer to bring your Swift code to other environments like the web and embedded devices.\n\nYou've always been able to import C into Swift with ease. Swift 6.4 allows you to expose functions written in Swift back to C.\n\nYou've likely seen or used the @objc attribute when migrating your apps from Objective-C to Swift. The @C attribute works the same way, but for C.\n\nThe @C attribute applies to functions that operate on C-compatible types. Any type you can import from C to Swift can also be used in functions that are exported from Swift to C, like integers pointers, imported C structs, and enums with raw integer value types. And, the compiler prevents you from accidentally passing types that are incompatible with C. Let's see the @C attribute in action. I'm working on an app for scheduling rocket launches. We'll focus on the code tracking the launch windows.\n\nIt's currently written in C, but we're migrating to Swift for memory safety and improving developer ergonomics. Let's use the new features in Swift 6.4 to rewrite portions of the application from C. There are currently two functions declared in the header, one to get the length of a launch window, and another to compute the total time spent in a collection of launch windows, allowing us to compute launch pad utilization.\n\nLet's start by replacing the C implementation of the function computing the length of a launch window.\n\nWe can use the @c and @implementation attributes together to implement a C function without creating a C declaration for it since it already exists in the original header file.\n\nNext, we'll provide an implementation for the function computing the total time spent in a launch window, across many launch windows.\n\nFunctions are imported verbatim when you're implementing them in Swift. We can translate the types into native Swift types allowing us to use the ergonomics of Swift to reimplement our C functions. Safe interop features provide safe wrappers when we call the functions, so instead of passing the array and count separately, we can pass it a span. It's easier to write, and it's safe! While we're working on this, I want to add a new function that gets the average launch window length.\n\nWe don't use the @implementation attribute here because the function isn't declared in the original C header. The Swift compiler emits the appropriate function declaration into the generated C interop header instead, so we can call the new function from our C code. Swift automatically bridges Swift Spans to C. Now, Swift C++ interoperability also supports bridging between Swift and C++ 20 spans. This declaration imports to Swift with a span, so you can pass a span in directly. Interoperability is key to bringing Swift into existing codebases written in other languages.\n\nSwift-Java is the package that enables interoperability between Swift and Java. It now supports calling async and throwing Swift functions from Java. It captures more features of the generics system, including constrained extensions, and conforming Java classes to Swift protocols. All of these improvements make calling Swift code from Java and Kotlin feel natural on Android. And you can now download an official Swift SDK for Android on swift.org. The latest version of the Swift extension for VSCode adds new integration with Swiftly, making it easy to install toolchains from swift.org, right from your editor. So now you'll always have the right toolchain for any platform, right at your fingertips.\n\nLast year, we added the Swift extension to the Visual Studio marketplace.\n\nThis year, we've added it to the OpenVSX marketplace, making the integrated Swift experience available to new editors including VSCodium, Cursor, Kiro, and Antigravity. The latest version of the plugin makes it easier to get started with Swift than ever before with a checklist that guides you through installing Swift, creating a new project, running your code, setting up tests, and generating documentation.\n\nIf you have Swiftly installed, it can help manage your toolchains. I'm installing a nightly toolchain so that I can play with the new language features targeting Web Assembly and embedded platforms.\n\nThe open source toolchain we just installed can compile to Web Assembly. Wasm support in Swift means that you can use the same language to write your native apps, your backend webservers, and your frontend too. Let's look at some of the improvements in the Javascript interoperability story coming out of the open-source project JavascriptKit.\n\nBridging between Swift and Javascript used to involve a lot of dynamic lookups and hoping that types would match up.\n\nThe most recent efforts in JavascriptKit have made bridging between the languages safer and faster. The code looks like native Swift code, but it's making calls to WebGL through Javascript.\n\nThe popular note-taking app, Goodnotes, recently implemented a web-based interface in addition to their native iOS app. The cost of moving their core app to another language, working through the bugs, and then maintaining two codebases was too high.\n\nThey took their existing, battle-tested, Swift code, and compiled it for the web using Wasm.\n\nAnd with the improvements to JavaScriptKit, their benchmarking found the safe bridging to be 35 to 40 times faster than the dynamic bridging. Wasm support in Swift means you can take your Swift code running in your native iOS applications and get it running as part of a webapp.\n\nUnlike with a native application though, you have to send the compiled code to each device every time they visit your site. There are CDNs to help, but big binaries can quickly eat into yours, and your customer's data. This means that size is a bigger concern than ever. With embedded Swift, we can take advantage of the ergonomics of Swift in more constrained environments. We stripped down the language to make it fit, and now, as we learn how people are using Swift in these environments, we are growing the available subset of the language.\n\nEmbedded Swift now has support for existential types. This means you can work with multiple types conforming to a protocol stored in an array or passed to a function.\n\nTyped throws are great when you know the error type, but they're limited to a specific error type. Using the same underlying machinery for handling existential types, embedded Swift now supports untyped throws.\n\nThe debugger needs extra metadata about type layout to display variables. To keep binary size down, embedded Swift does't include that data in the binary itself.\n\nFurthermore, many embedded systems only leave you with a core dump of the memory state when the program crashed, so you're not even debugging a live process. Swift 6.4 saves all of the necessary metadata into the DWARF debug info, keeping binary sizes down, while greatly improving the experience debugging an embedded Swift coredump.\n\nThese are just a few of the improvements we made this year, bridging the gap between full and embedded Swift.\n\nWhile the subset of Swift that works on embedded platforms is growing, it is still a subset of the language. Diagnostics in the EmbeddedRestrictions warning group identify language features that aren't available in embedded contexts.\n\nIf you're working with a library that supports both embedded and full Swift, you may need to expose functions that use features that aren't available in embedded contexts. The @diagnose attribute that Becca showed us earlier lets us control these diagnostics. Embedded code often runs on incredibly constrained hardware environments. That's why it's critical to get the most out of every clock cycle. Back to Becca to talk about how to squeeze performance from your Swift code.\n\nSwift is designed to have great performance even when you've written the most straightforward, expressive implementation of your logic.\n\nBut when you're doing a lot of computation, or running in constrained environments like embedded systems, sometimes it's worth complicating your code to squeeze out just a little bit more when it counts.\n\nYou usually won't need these advanced performance features, but when you do, you'll be glad you have them.\n\nI'm going to focus on two areas we've worked on this year, explicitly controlling optimizer decisions, and further extending the ownership system to safely prevent unnecessary copying.\n\nThe Swift compiler's optimizer applies dozens of techniques to make your code faster, but some of the most powerful ones can be counterproductive when they're applied incorrectly.\n\nThese involve duplicating code so it can be customized for a particular situation, but if the customization doesn't pay off, you could end up with a program that's larger and slower instead of smaller and faster.\n\nFor example, one of the most important optimizations the compiler performs is inlining, replacing function calls with their implementations, and then optimizing those implementations for the specific call site.\n\nWhen inlining pays off, the program does less work to get the same result, but when it doesn't, it just makes the binary bigger without making it any faster. To keep that from happening, the optimizer analyzes the function and call site to decide whether inlining is likely to pay off, but sometimes it makes the wrong decision, so you might want a way to force the issue.\n\nSwift has long had an @inline(never) attribute, that completely forbids inlining, useful when you know that inlining the function will never be a win. In Swift 6.4, there is now a matching @inline(always) attribute which forces the compiler to inline even when the optimizer isn't sure it'll be a good idea.\n\nSometimes it still won't be able to though, like when you call an object method that might be overridden, so consider using final with @inline(always) for methods of classes.\n\nAnother important optimization is specialization cloning a generic function for a specific concrete type, eliminating generic overhead and often enabling further optimization. Specializing a function only helps if the optimizer knows that you're going to use it with that type, but sometimes, especially in libraries, the compiler can't see how the function will be used.\n\nSwift 6.3 introduces the @specialized attribute to give you direct control over this.\n\nInside the attribute, you write a where clause constraining some or all of the generic parameters and Swift will generate a specialized version of the function with those constraints.\n\nSo if you have some slow generic code that gets used a lot with one or two specific types, you can let Swift know that it needs to prioritize them. But the biggest improvements we've made for performance tuning are to the ownership system.\n\nTo understand what we've done, let's start by reviewing what we already have.\n\nA lot of performance problems in Swift boil down to unnecessarily copying data. You have a piece of data in one place, you need it in another, so you copy the data across to new storage. Sometimes these components are big, like the model and view layers of your app, or sometimes they're small, like the two variables on either side of a for loops in keyword, but the basic pattern is the same.\n\nThe way to solve slow-downs due to copying is to recognize that, in certain situations, a copy is unnecessary. If you know that the storage will stay allocated, and that both of the components using it will follow Swift's exclusivity rules so they don't mutate data they both have access to, then you don't need to copy the data you just need to grant access to the existing storage. The simplest way to avoid copying is to put the data in an object and pass that object to the other component.\n\nThis is often good enough, but it doesn't entirely eliminate the problem. After all, objects are reference-counted, and passing an object changes its reference count. Releasing and retaining an object has less overhead than copying a large value, but it still might be too slow for the most performance-sensitive code.\n\nWhen objects aren't an option, you've traditionally had to pass an UnsafePointer to the storage instead. But the problem with doing that is right in its name, UnsafePointers are unsafe. Sharing storage like this is only safe because both components are following certain rules, but the compiler has no idea about those rules and can't make sure that each side will hold up its end of the bargain.\n\nComponent 1 could mutate data that Component 2 expected to remain unchanged. Or it could deallocate the storage while Component 2 is still using it.\n\nYou're back to a world with no memory safety, like the C languages had.\n\nSo a few years ago, we started working on a better solution. We codified the set of guarantees needed to safely share storage as a borrow. As long as Component 2 is borrowing the storage, both Components can only read it, not write it. Component 2 has to finish using the storage first, and when it does, Component 1 regains full control.\n\nMutation is similar, except the other component is completely blocked from accessing the storage. This ensures that it doesn't read half-updated data or behave differently depending on when the other component chooses to write back its changes. And whether it's borrowing or mutating, Swift can verify at compile time that both components are following the rules.\n\nBut our goal has always been to support these advanced use cases without affecting how you write ordinary Swift code. That's required careful, incremental design of ownership features. This has been a multi-year process and it's still not over, but this year we've taken a few more steps forward. For example, the Equatable, Comparable, and Hashable protocols can now be used on noncopyable types.\n\nAnd Equatable and Comparable can also be used with non-escapable types.\n\nThis lets types tuned for performance and safety take advantage of some of the most universal capabilities that ordinary Swift types have. Associated types can also now be non-copyable or non-escapable.\n\nThis opens up powerful new capabilities for high-performance protocol-driven development, like the Iterable protocol seen here.\n\nIts element type is non-copyable and its iterator type is both non-copyable and non-escapable. Of course, that doesn't mean you can't use a copyable or escapable type for these, it just means that the protocol doesn't require it. Gosh, though, that Iterable type looks really handy. Don't you wish it were real? Well, good news, it is! In Swift 6.4, for loops support a new Iterable protocol. The Sequence protocol we all know and love works by copying the elements out of the sequence, the Iterable protocol allows the for loop to borrow them instead.\n\nThis means it works with non-copyable elements, and also that it doesn't need to perform reference counting when it's working with objects or copy-on-write types. Plus, it can optionally throw an error during the loop, just like an ASYNC Sequence can. However, like any borrow, exclusivity checking prohibits you from mutating the Iterable while you're looping over it which isn't necessarily a bad thing, since this was often a performance trap with Sequences. Because of this behavior difference, the for loop will prefer the Sequence protocol if available, and fall back to Iterable otherwise.\n\nMuch like a Sequence, an Iterable works by creating an iterator for the for loop to retrieve the elements from.\n\nBut unlike a sequence, it retrieves elements in batches. The for loop will ask the iterator for a span of elements and go through them one-by-one. Then ask for another span and visit those, too.\n\nWhen it runs out of elements, it returns an empty span, which terminates the for loop. This batching design makes the loop a lot more efficient, especially for types that can just return everything in one big span.\n\nSwift 6.4 also makes a major improvement to accessors.\n\nTo understand why it's important, consider this UniqueBox struct.\n\nIt automatically manages a pointer to a large value and provides a computed property to access it. Unfortunately, as it's written right now, this property has a serious performance problem. Let's see why.\n\nSuppose you put an InlineArray of 256 Ints in a UniqueBox. InlineArrays are, well, inline, so on a 64-bit device, that's gonna be a two-kilobyte struct. Not something you want to be copying around all the time! Which is a problem, because get and set work by copying the data. To change one Int in the array, you have to copy the whole thing out and then back again.\n\nThat's not going to help your performance very much! Fortunately, there's now a better option, you can switch from get and set to borrow and mutate. The new borrow accessor gives you read-only access to shared storage without copying it, while the mutate accessor gives you exclusive access to modify it in place. Once you've switched, Swift can just mutate the element in the original array without copying anything.\n\nAnd, as a nice little bonus, the new accessors mean UniqueBox can also handle non-copyable values. The borrow and mutate accessors are a big help for both performance and expressivity, and we're excited they're ready for everyone to use. Gosh, though, that UniqueBox type looks really handy. Don't you wish it... Okay, okay, you know how that ends, I won't do that spiel again. Yes, UniqueBox is a real type that's now in the standard library. There are also some other new APIs that save you from writing unsafe code, \"Unique Array\" is a lot like an ordinary Swift Array, except that it's non-copyable. That means you can store non-copyable elements and avoid reference counting overhead without limiting yourself to a fixed size.\n\nThe withTemporaryAllocation function uses an OutputSpan instead of an UnsafeMutableBufferPointer to ensure that temporary memory is handled safely.\n\nThe Continuation type checks at compile time that you only resume it once, making it even safer than a CheckedContinuation but just as efficient as an UnsafeContinuation. And there's also one more pair of types I want to tell you about, but they're not improved versions of an existing API, they bring new capabilities to the language. A Ref is a bit like a Span, but for one value instead of many. It's sort of like a container for a borrow or mutation, that can be stored in variables, passed and returned from functions, and used in generic types. You can make an instance of the Ref type from a read access by borrowing storage, or an instance of the MutableRef type from a write access by using the prefix ampersand. These types can be used to create new APIs that were impossible to write before, like a method that returns a Mutable Ref for one of its properties, but they can also be used to address performance issues.\n\nFor example, consider this updateCount function. It looks up a dictionary key every time it needs to increment it, even though the key is always the same. You usually want to hoist repeated work like that out of the loop so you only have to do it once, but until now, the only way to do a dictionary lookup and just sort of hold it open for a while, was by moving the loop into a function and passing the lookup as an inout parameter. A pretty obscure trick! MutableRef gives you a better way. Now you can make a MutableRef from the dictionary lookup once, before the loop starts, and then use it when you want to mutate the dictionary entry. Refs are non-escapable, so Swift knows the access ends when the variable goes out of scope. All these new ownership features combine to make speeding up your most performance sensitive code safer and easier than ever. The ownership system isn't the only work that's still in progress. Let's hand it back to Evan to tell you about the future of Swift. The features we covered today were developed in open source, improving the overall experience across the Apple OS's, Linux, Windows, and beyond. Here are some more future developments happening in the open source community that you can get involved with.\n\nLast year, we announced that we were open sourcing Swift Build, the build system in Xcode. Swift Build is now the default build system backend for Swift Package Manager, improving the consistency between Swift Package builds and what you see from Xcode. Joining the growing list of workgroups are the build and packaging workgroup, addressing the build and packaging needs of the Swift community. The networking workgroup, designing the next generation of cross-platform networking APIs. And the Windows workgroup, improving the Swift experience on Windows. This year, the Android work group released the first Swift SDK for Android as part of Swift 6.3. This means it is now possible to share Swift code between Android and iOS apps. If you're interested in participating in the development of the Swift ecosystem, please join us on the Swift forums at forums.swift.org. We would love to have your unique feedback.\n\nThank you for following along with us today to learn more about what's new in Swift. Whether you've submitted a bug report, posted a pull request, joined us at an event, or participated on the forums, your contributions help shape the future of Swift making programming safer and more approachable for everyone. Thank you!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "1:12",
+ "title": "Better Swift Concurrency diagnostics (catching in the task)",
+ "language": "swift",
+ "code": "Task {\n do {\n try lander.fly(to: moon)\n }\n catch {\n lander.abort()\n }\n}"
+ },
+ {
+ "timestamp": "1:21",
+ "title": "Better Swift Concurrency diagnostics (saving the task for later)",
+ "language": "swift",
+ "code": "let landingTask = Task {\n try lander.fly(to: moon)\n}\n\ndefer {\n await orbiter.rendezvous(with: lander)\n}\n\ntry await orbiter.justHangOut(waitingFor: landingTask)"
+ },
+ {
+ "timestamp": "1:27",
+ "title": "Better 'Sendable' conformances",
+ "language": "swift",
+ "code": "final class Spacecraft: Sendable {\n ...\n weak let dockedAt: SpaceStation?\n ...\n}\n\nclass Mission: ~Sendable { ... }\n\nclass CrewedMission: Mission, @unchecked Sendable { ... }"
+ },
+ {
+ "timestamp": "1:48",
+ "title": "More accessible memberwise initializers",
+ "language": "swift",
+ "code": "struct Briefing {\n internal var topic: String\n internal var scheduledAt: Date\n private var attendees: [Person] = []\n}\n\n// Generated memberwise initializers:\n// extension Briefing {\n// private init(topic: String, scheduledAt: Date, attendees: [Person] = []) { \n// self.topic = topic\n// self.scheduledAt = scheduledAt\n// self.attendees = attendees\n// }\n// \n// internal init(topic: String, scheduledAt: Date) {\n// self.topic = topic\n// self.scheduledAt = scheduledAt\n// self.attendees = []\n// }\n// }"
+ },
+ {
+ "timestamp": "2:03",
+ "title": "'anyAppleOS' availability (before)",
+ "language": "swift",
+ "code": "extension Mission {\n @available(macOS 27, iOS 27, watchOS 27, tvOS 27, visionOS 27, *)\n func showStatus() { ... }\n\n @available(macOS 27, iOS 27, watchOS 27, visionOS 27, *)\n @available(tvOS, unavailable)\n func launch() { ... }\n \n #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS)\n func makeLiveActivityWidget() -> some Widget { ... }\n #endif\n}"
+ },
+ {
+ "timestamp": "2:17",
+ "title": "'anyAppleOS' availability (after)",
+ "language": "swift",
+ "code": "extension Mission {\n @available(anyAppleOS 27, *)\n func showStatus() { ... }\n\n @available(anyAppleOS 27, *)\n @available(tvOS, unavailable)\n func launch() { ... }\n \n #if os(anyAppleOS)\n func makeLiveActivityWidget() -> some Widget { ... }\n #endif\n}"
+ },
+ {
+ "timestamp": "2:40",
+ "title": "Controlling warnings with '@diagnose'",
+ "language": "swift",
+ "code": "@diagnose(DeprecatedDeclaration, as: ignored, reason: \"Flying with surplus hardware\")\nfunc makeApolloSoyuzMission() -> Mission {\n CrewedMission(\n rocket: makeSaturnIRocket(),\n payload: makeApolloCSM(),\n crew: [.daniellePoole, .nathanMorrison]\n )\n}\n\n@diagnose(StrictMemorySafety, as: warning)\nfunc uplinkCommand(from receiver: inout Receiver, to computer: inout Computer) {\n let commandSize = receiver.receiveInt()\n receiver.withReceivedData(byteCount: commandSize) {\n computer.receiveUplinkedCommand($0)\n }\n}\n\n@diagnose(ErrorInFutureSwiftVersion, as: error)\nfunc fetchPosition() -> (x: Double, y: Double, z: Double) {\n return self.rotation\n}"
+ },
+ {
+ "timestamp": "3:47",
+ "title": "Clarifying code with module selectors",
+ "language": "swift",
+ "code": "import Rocket\nimport GiftShopToys\n\nlet rocket1 = SaturnV() // could mean `Rocket::SaturnV` or `GiftShopToys::SaturnV`\nlet rocket2 = Rocket.SaturnV() // prefers `Rocket::Rocket.SaturnV`\nlet rocket3 = Rocket::SaturnV() // correctly finds `Rocket::SaturnV`"
+ },
+ {
+ "timestamp": "5:00",
+ "title": "Clarifying code with module selectors (module selectors work on members, too)",
+ "language": "swift",
+ "code": "//\n// Module Chemistry\n//\n\npublic protocol Flammable { ... }\n\nextension Flammable {\n /// Set `self` on fire.\n public func fire() { ... }\n}\n\n//\n// Module HumanResources\n//\n\nimport Chemistry\n\npublic protocol Employee { ... }\n\nextension Employee {\n /// Remove `self` from job.\n public func fire() { ... }\n}\n\npublic class LaunchPadTechnician: Employee, Flammable { ... }\n\n//\n// Module main\n//\n\nimport HumanResources\nimport Chemistry\n\nlet launchPadTechnician = LaunchPadTechnician(...)\n\nlaunchPadTechnician.HumanResources::fire()"
+ },
+ {
+ "timestamp": "6:26",
+ "title": "Task cancellation",
+ "language": "swift",
+ "code": "// Radio for help\n\nextension Radio {\n func send(_ data: [UInt8] {\n if Task.isCancelled { return }\n // ...\n }\n}\n \nextension EmergencyTransponder {\n func sendSOS() {\n radio.send(makeSOSPacket())\n }\n}"
+ },
+ {
+ "timestamp": "6:40",
+ "title": "Task cancellation shield",
+ "language": "swift",
+ "code": "// Radio for help\n\nextension Radio {\n func send(_ data: [UInt8] {\n if Task.isCancelled { return }\n // ...\n }\n}\n \nextension EmergencyTransponder {\n func sendSOS() {\n withTaskCancellationShield {\n \tradio.send(makeSOSPacket())\n }\n }\n}"
+ },
+ {
+ "timestamp": "6:53",
+ "title": "Constructing a new dictionary",
+ "language": "swift",
+ "code": "// Map values with keys\n\nfunc makeCalendarDisplayNames(for missions: [Mission: LaunchWindow]) -> [Mission: String] {\n let new: [Mission: String] = .init(\n uniqueKeysWithValues: missions.lazy.map { mission, launchWindow in\n (mission, makeDisplayName(for: mission, in: launchWindow))\n }\n )\n return new\n}"
+ },
+ {
+ "timestamp": "7:06",
+ "title": "Dictionary.mapKeyedValues",
+ "language": "swift",
+ "code": "// Map values with keys\n\nfunc makeCalendarDisplayNames(for missions: [Mission: LaunchWindow]) -> [Mission: String] {\n missions.mapKeyedValues { mission, launchWindow in\n makeDisplayName(for: mission, in: launchWindow)\n }\n}"
+ },
+ {
+ "timestamp": "7:14",
+ "title": "The new FilePath type",
+ "language": "swift",
+ "code": "// FilePath handling macOS-named resources\n\nvar path: FilePath = \"/var/www/static\"\npath.components.append(\"WWDC\")\nprint(path.components)\n// [ \"var\", \"www\", \"static\", \"WWDC\" ]\n\nvar path: FilePath = \"/var/www/static/..namedresource/rsrc\"\nprint(path.components)\n// [ \"var\", \"www\", \"static\" ]"
+ },
+ {
+ "timestamp": "7:41",
+ "title": "Issue Severity",
+ "language": "swift",
+ "code": "// Issue severity\n\n@Test(arguments: allRockets)\nfunc testBurn(rocket: Rocket) throws {\n rocket.burn(for: .seconds(150))\n let remaining = rocket.propellantKg / rocket.totalPropellantKg\n\n if remaining < 0.10 {\n Issue.record(\n \"\\(rocket.name) remaining fuel is below 10% reserve target\",\n severity: .warning\n )\n }\n\n #expect(remaining > 0.02, \"\\(rocket.name) propellant critically low - abort\")\n}"
+ },
+ {
+ "timestamp": "7:52",
+ "title": "Test Cancellation",
+ "language": "swift",
+ "code": "// Test Cancellation\n\n@Test(arguments: allRockets)\nfunc testBurn(rocket: Rocket) throws {\n // solid-fuel rocket engines can't be stopped\n if rocket.engineType == .solid {\n try Test.cancel(\"\\(rocket.name) has solid fuel\")\n }\n \n rocket.burn(for: .seconds(150))\n let remaining = rocket.propellantKg / rocket.totalPropellantKg\n\n if remaining < 0.10 {\n Issue.record(\n \"\\(rocket.name) remaining fuel is below 10% reserve target\",\n severity: .warning\n )\n }\n\n #expect(remaining > 0.02, \"\\(rocket.name) propellant critically low - abort\")\n}"
+ },
+ {
+ "timestamp": "8:34",
+ "title": "XCTest interoperability: Using XCTest from Swift Testing",
+ "language": "swift",
+ "code": "// XCTest interoperability: Using XCTest from Swift Testing\n\nfunc checkedTransmitAndReceive(on radio: Radio,\n packet: Packet,\n expectedByteCount: Int) throws -> [UInt8] {\n try radio.transmit(bytes: packet.data)\n let bytes = try radio.receive()\n XCTAssertEqual(bytes.count, expectedByteCount)\n return bytes\n}\n\n@Test\nfunc pingTest() throws {\n let radio = Radio()\n let bytes = try checkedTransmitAndReceive(on: radio, packet: .ping, expectedByteCount: 8)\n #expect(bytes == [0x00, 0x00, 0xf0, 0x37, 0x0f, 0xc7, 0x00, 0x01])\n}"
+ },
+ {
+ "timestamp": "8:48",
+ "title": "XCTest interoperability: Using Swift Testing from XCTest",
+ "language": "swift",
+ "code": "// XCTest interoperability: Using Swift Testing from XCTest\n\nclass RadioTests: XCTestCase {\n func testPingPacketTransmission() {\n let radio = Radio()\n let bytes = try checkedTransmitAndReceive(on: radio,\n packet: .ping,\n expectedByteCount: 8)\n\n #expect(bytes == [0x00, 0x00, 0xf0, 0x36, 0x0f, 0xc7, 0x00, 0x02])\n }\n}"
+ },
+ {
+ "timestamp": "10:01",
+ "title": "Subprocess Output Stream",
+ "language": "swift",
+ "code": "// Subprocess output streaming\n\nlet result = try await Subprocess.run(.name(\"ls\"),\n input: .none,\n output: .sequence,\n error: .string(limit:4096)) { execution in\n\t\texecution.standardOtput.strings().filter { $0.hasSuffix(\".obj\") }\n}\n\nfor try await objectFiles in result.closureOutput {\n \tprint(\"Object file: \\(objectFile)\")\n}"
+ },
+ {
+ "timestamp": "10:37",
+ "title": "Progress Manager - Concurrency",
+ "language": "swift",
+ "code": "// Progress reporting - Concurrency\n\nlet manager = ProgressManager(totalCount: 100)\ntry await rocket.launch(mission.subprogress(assigningCount: 100))\n\nextension Rocket {\n func launch(_ progress: consuming Subprogress? = nil) async throws {\n let stage = progress?.start(totalCount: 3)\n try await ignite(); stage?.complete(count: 1)\n try await liftoff(); stage?.complete(count: 1)\n try await stageSeparation(); stage?.complete(count: 1)\n }\n}"
+ },
+ {
+ "timestamp": "10:37",
+ "title": "Progress Manager - progress reporting",
+ "language": "swift",
+ "code": "// Progress reporting - progress reporting\n\nlet manager = ProgressManager(totalCount: 100)\ntry await rocket.launch(mission.subprogress(assigningCount: 100))\n\nTask {\n for await update in Observations({ mission.fractionCompleted }) {\n print(\"🚀 Mission \\(Int(update * 100))%\")\n }\n}"
+ },
+ {
+ "timestamp": "10:37",
+ "title": "Progress reporting - metadata",
+ "language": "swift",
+ "code": "// Progress reporting - metadata\n\nextension Rocket {\n func ascend(_ progress: consuming Subprogress) async throws {\n let stage = progress.start(totalCount: 3)\n stage.detlaV = 3_400; try await burn(); stage.complete(count: 1)\n stage.detlaV = 2_100; try await stageSeparation(); stage.complete(count: 1)\n stage.detlaV = 1_800; try await coast(); stage.complete(count: 1)\n }\n}\n\nprint(\"Δv to orbit: \\(mission.summary(of: \\.deltaV)) m/s\")"
+ },
+ {
+ "timestamp": "20:56",
+ "title": "Directly control inlining (source code)",
+ "language": "swift",
+ "code": "func histogram(of values: Values) -> [256 of Int] where Values: Sequence {\n var result = makeInts(randomized: false)\n \n for value in values {\n result[Int(value)] += 1\n }\n \n return result\n}\n\nfunc makeInts(randomized: Bool) -> [256 of Int] {\n if randomized {\n InlineArray { _ in Int.random(in: (.min)...(.max)) }\n } else {\n InlineArray(repeating: 0)\n }\n}"
+ },
+ {
+ "timestamp": "21:01",
+ "title": "Directly control inlining (inlined, but not optimized)",
+ "language": "swift",
+ "code": "func histogram(of values: Values) -> [256 of Int] where Values: Sequence {\n var result = if false { //\n InlineArray { _ in Int.random(in: (.min)...(.max)) } //\n } else { // Inlined code\n InlineArray(repeating: 0) //\n } //\n\n for value in values {\n result[Int(value)] += 1\n }\n return result\n}\n\nfunc makeInts(randomized: Bool) -> [256 of Int] {\n if randomized {\n InlineArray { _ in Int.random(in: (.min)...(.max)) }\n } else {\n InlineArray(repeating: 0)\n }\n}"
+ },
+ {
+ "timestamp": "21:07",
+ "title": "Directly control inlining (inlined and optimized)",
+ "language": "swift",
+ "code": "func histogram(of values: Values) -> [256 of Int] where Values: Sequence {\n var result = InlineArray(repeating: 0) // Inlined and optimized code\n\n for value in values {\n result[Int(value)] += 1\n }\n return result\n}\n\nfunc makeInts(randomized: Bool) -> [256 of Int] {\n if randomized {\n InlineArray { _ in Int.random(in: (.min)...(.max)) }\n } else {\n InlineArray(repeating: 0)\n }\n}"
+ },
+ {
+ "timestamp": "21:30",
+ "title": "Directly control inlining (preventing inlining)",
+ "language": "swift",
+ "code": "@inline(never)\nfunc makeInts(randomized: Bool) -> [256 of Int] {\n if randomized {\n InlineArray { _ in Int.random(in: (.min)...(.max)) }\n } else {\n InlineArray(repeating: 0)\n }\n}"
+ },
+ {
+ "timestamp": "21:39",
+ "title": "Directly control inlining (forcing inlining)",
+ "language": "swift",
+ "code": "@inline(always)\nfunc makeInts(randomized: Bool) -> [256 of Int] {\n if randomized {\n InlineArray { _ in Int.random(in: (.min)...(.max)) }\n } else {\n InlineArray(repeating: 0)\n }\n}"
+ },
+ {
+ "timestamp": "21:55",
+ "title": "Making generic functions faster with '@specialized'",
+ "language": "swift",
+ "code": "func histogram(of values: Values) -> [256 of Int] where Values: Sequence {\n var result = makeInts(randomized: false)\n \n for value in values {\n result[Int(value)] += 1\n }\n \n return result\n}\n\n// Note: Specialized function doesn't actually have a directly callable name.\nfunc `histogram of [UInt8]`(of values: [UInt8]) -> [256 of Int] { //\n var result = makeInts(randomized: false) //\n //\n for value in values { //\n result[Int(value)] += 1 // Specialized code\n } //\n //\n return result //\n} //"
+ },
+ {
+ "timestamp": "22:17",
+ "title": "Making generic functions faster with '@specialized' (explicitly requesting specialization)",
+ "language": "swift",
+ "code": "@specialized(where Values == [UInt8])\nfunc histogram(of values: Values) -> [256 of Int] where Values: Sequence {\n var result = makeInts(randomized: false)\n \n for value in values {\n result[Int(value)] += 1\n }\n \n return result\n}\n\n// Note: Specialized function doesn't actually have a directly callable name.\nfunc `histogram of [UInt8]`(of values: [UInt8]) -> [256 of Int] { //\n var result = makeInts(randomized: false) //\n //\n for value in values { //\n result[Int(value)] += 1 // Specialized code\n } //\n //\n return result //\n} //"
+ },
+ {
+ "timestamp": "25:46",
+ "title": "Associated types can be '~Copyable' and '~Escapable'",
+ "language": "swift",
+ "code": "protocol Iterable: ~Copyable, ~Escapable {\n associatedtype Element: ~Copyable\n associatedtype IterableIterator: IterableIteratorProtocol, ~Copyable, ~Escapable\n associatedtype Failure: Error = Never\n\n func makeIterableIterator() -> IterableIterator\n \n var underestimatedCount: Int { get }\n}\n\nprotocol IterableIteratorProtocol: ~Copyable, ~Escapable {\n associatedtype Element: ~Copyable\n associatedtype Failure: Error = Never\n\n mutating func nextSpan(maximumCount: Int) throws(Failure) -> Span\n \n mutating func skip(by maximumOffset: Int) throws(Failure) -> Int\n}"
+ },
+ {
+ "timestamp": "27:28",
+ "title": "The problem with existing accessors",
+ "language": "swift",
+ "code": "@safe public struct UniqueBox: ~Copyable {\n private let valuePointer: UnsafeMutablePointer\n\n public init(_ value: consuming Value) {\n valuePointer = UnsafeMutablePointer.allocate(capacity: 1)\n valuePointer.initialize(to: value)\n }\n\n public var value: Value {\n get { valuePointer.pointee }\n set { valuePointer.pointee = newValue }\n }\n\n deinit {\n valuePointer.deinitialize(count: 1)\n valuePointer.deallocate()\n }\n}"
+ },
+ {
+ "timestamp": "28:19",
+ "title": "'borrow' and 'mutate' accessors",
+ "language": "swift",
+ "code": "@safe public struct UniqueBox: ~Copyable {\n private let valuePointer: UnsafeMutablePointer\n\n public init(_ value: consuming Value) {\n valuePointer = UnsafeMutablePointer.allocate(capacity: 1)\n valuePointer.initialize(to: value)\n }\n\n public var value: Value {\n borrow { valuePointer.pointee }\n mutate { &valuePointer.pointee }\n }\n\n deinit {\n valuePointer.deinitialize(count: 1)\n valuePointer.deallocate()\n }\n}"
+ },
+ {
+ "timestamp": "30:14",
+ "title": "Using 'MutableRef' to eliminate repeated accesses (with un-hoisted access)",
+ "language": "swift",
+ "code": "func updateCount(\n for key: Key,\n from sets: [Set],\n in counts: inout [Key: Int]\n) {\n for set in sets {\n if set.contains(key) {\n counts[key, default: 0] += 1\n }\n }\n}"
+ },
+ {
+ "timestamp": "30:34",
+ "title": "Using 'MutableRef' to eliminate repeated accesses (hoisted by 'inout' parameter)",
+ "language": "swift",
+ "code": "func updateCount(\n for key: Key,\n from sets: [Set],\n in counts: inout [Key: Int]\n) {\n func updateCountImpl(count: inout Int) {\n for set in sets {\n if set.contains(key) {\n count += 1\n }\n }\n }\n \n updateCountImpl(count: &counts[key, default: 0])\n}"
+ },
+ {
+ "timestamp": "30:41",
+ "title": "Using 'MutableRef' to eliminate repeated accesses (hoisted by 'MutableRef')",
+ "language": "swift",
+ "code": "func updateCount(\n for key: Key,\n from sets: [Set],\n in counts: inout [Key: Int]\n) {\n var countRef = MutableRef(&counts[key, default: 0])\n\n for set in sets {\n if set.contains(key) {\n countRef.value += 1\n }\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Swift Blog",
+ "url": "https://www.swift.org/blog/"
+ },
+ {
+ "title": "Explore documentation on swift.org",
+ "url": "https://www.swift.org/documentation/"
+ },
+ {
+ "title": "Swift Forums",
+ "url": "https://forums.swift.org"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/262/5/d430e425-34fc-4ed5-b590-507ac593453a/downloads/wwdc2026-262_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/262/5/d430e425-34fc-4ed5-b590-507ac593453a/downloads/wwdc2026-262_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "265",
+ "year": "2026",
+ "title": "Build real-time apps and services with gRPC and Swift",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/265"
+ },
+ {
+ "id": "267",
+ "year": "2026",
+ "title": "Migrate to Swift Testing",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/267"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:16.708Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-265.json b/data/wwdc/videos/2026-265.json
new file mode 100644
index 0000000..10c7505
--- /dev/null
+++ b/data/wwdc/videos/2026-265.json
@@ -0,0 +1,203 @@
+{
+ "id": "265",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/265/",
+ "title": "Build real-time apps and services with gRPC and Swift",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Developer Tools",
+ "Swift"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm George from the Swift Server team. In this video, I'll show you how you can build real time experiences in your apps and services with gRPC Swift. Dynamic app experiences usually depend on fetching data from a server, but working with services can be challenging. Hand crafting networking code to interact with a service can be time consuming. You start with some documentation, spend time crafting great APIs and end up with something that seems to work. But the documentation isn't always up-to-date, and maybe you made some mistakes along the way and the result can be something that doesn't always work as you expect. Fortunately, there's a better way. Many service APIs are defined separately in a specification which acts as the source of truth for the service. This allows you to generate the code required to interact with it, saving you time and eliminating errors. These benefits scale across all of the APIs that you need to interact with. A great option for HTTP based APIs is OpenAPI. It's widely used and has great support in Swift which my teammate Si talked about in the session named \"Meet Swift OpenAPI Generator\". We'll look at an alternative called gRPC. Then I'll show you how you can use it in your app to make simple requests and build real-time experiences using streaming RPCs. Then, we'll see how to implement a gRPC service and deploy it to the cloud. First though, let's talk about what gRPC is.\n\ngRPC is a framework for making remote procedure calls. It's a CNCF project and widely adopted industry standard. Like with OpenAPI you work with code that's generated from a specification, allowing you to start working with services quickly. But in gRPC your APIs are defined in terms of functions with input and outputs, rather than in terms of HTTP. Let's see how this works in practice. There's a new go karting league starting nearby. They've got a system which tracks everything from the schedule to all of the live race data, but they need a way to make that information available. I've been working on integrating an iOS app with their backend via a gRPC service. I've prepared a few views in the app and populated them with some example data. I can list the upcoming races. And then tap in to each to get more information. But it would be great to use gRPC to get this content from the server instead. A function to return the race schedule might be called list races. It could be called with the number of races to request and it would return the list of races. As a remote procedure call, the request message is sent by the client to the server, which executes the function and sends back the list of races as the response. Let's put this theory into practice and see how to use gRPC Swift in my app.\n\nI'll start by defining the service API. Then I'll add the required dependencies to my Xcode project, configure the gRPC build plugin to generate the code I need to call my service. And then, update my app to make a call to the server. The most common format for specifying gRPC services is called Protocol Buffers, or Protobuf for short. In a .proto file, I'll define the service with one RPC called ListRaces. It has a ListRacesRequest as the input and a ListRacesResponse as the output. The request message has one field called limit which is an integer representing the maximum number of races to include in the response. I've given it a default value of 100. Every field in a message is also assigned a unique field number. The response message contains a repeated Race field. Race is defined as a separate message and contains information such as its name, location, and championship as strings, the number of laps as an integer and the start time as a timestamp. The timestamp type is one of Protobuf's Well Known Types and defined elsewhere so I need to import its definition. Now that I've defined the service I can switch to Xcode to add the gRPC dependencies and configure the code generator. To get started, I need to add some dependencies to my project. I'll navigate to the Project Editor.\n\nThen, I'll select the Package Dependencies tab and then click plus.\n\nFirst, I'll add a dependency on grpc-swift-nio-transport which provides the high performance networking code built on top of the open source SwiftNIO library.\n\nThen I'll add a dependency on grpc-swift-protobuf which provides a build plugin for generating gRPC code from my proto file.\n\nNow that I've setup the dependencies, I can configure the target to use the build plugin. I'll select the app Target. Then, I'll select the Build Phases tab, and expand the section named Run Build Tool Plug-ins. Then I'll click the plus icon, select GRPCProtobufGenerator and click Add. The plugin scans the target directory for proto files and can be configured using a JSON config file. I'll add those to my target now.\n\nThe JSON file configures what code is generated. Since this is an app I only need messages and clients, I don't need the server code. Now I can recompile the app to generate the code. As a security measure, you'll be asked to trust the plug-in the first time you use it.\n\nThat's everything setup, I'm now ready to call my service. I'll open up the RaceScheduleView.\n\nAnd import the modules I need.\n\nThe core module provides common gRPC runtime components, the HTTP module provides the networking code, and SwiftProtobuf lets us interact with our Protobuf messages. Next, I'll add a task modifier to the view in which I'll make the request. Inside the task, I'm going to use the withGRPCClient function to create a client. I'll do this inside a do catch block and just print the errors for now. gRPC Swift lets you configure the implementation used for networking. I'll use a SwiftNIO based one to connect to a server running locally on my Mac.\n\nThe client passed to the closure only knows about the server. It doesn't know anything about the service. This is where the generated code comes in: I'll create a SwiftKart client, and initialize it with the gRPC client, then I'll create the request, call the list races RPC and await the response.\n\nFinally, I'll update the view with the new data by mapping the response from the server to the data model used by the view.\n\nAnd just like that we've fetched the race schedule from our local server. Finite Loops sounds fun, and it's starting soon! Before I go any further I need to make one important change. At the moment, my app creates a new gRPC client every time the view appears. This means each view needs to establish its own connection to the server, adding unnecessary latency. Instead, my app should create a client and share it between views so that connections can be reused.\n\nI can propagate the client via the app's environment. Clients should also be disconnected when the app enters the background to free up resources. In Xcode I'll add some code for a client manager I wrote earlier.\n\nThen I'll open the app entry point and create an instance of the manager.\n\nI'll make it available to child views via the environment modifier. I should also disconnect the client when the scene enters the background phase. To do that, I'll create a scene phase property and then watch it for changes.\n\nMy manager class connects lazily when asked for a client so I don't need to do anything when the scene enters the active state. Now I'll use the manager in the RaceScheduleView. I'll add it to the view, and make it available in the preview.\n\nFinally I'll replace the withGRPCClient call with a call to the manager.\n\nThat's my app setup and talking to my service using code generated from a service API defined in Protobuf. In addition to the Service API, Protobuf provides a message interchange format. SwiftProtobuf has a code generator so that you can work directly with Swift types that represent your messages. For example I can create a race message and populate the fields with the relevant information. When gRPC sends messages between the client and server, it serializes them to a binary representation. It uses the unique field number rather than name to identify each field. As a result, the Protobuf message is roughly half the size of the equivalent JSON message. Reducing message size is great for mobile apps where minimizing data transfer helps improve the performance of your network calls. This is especially important when network conditions are poor. This efficiency is great for other environments too, like service-to-service communication. And interprocess communication like in Apple's open source Containerization framework. It uses gRPC Swift to communicate over virtual sockets between the host operating system and a Linux Virtual Machine. gRPC Swift is also a key component in cloud services like Private Cloud Compute, iCloud Keychain and Photos, and SharePlay file sharing. But our use doesn't just power external facing services, gRPC runs deep into our internal infrastructure, such as in our OS build and release systems. One of gRPC's standout features is its first class support for streaming Many RPCs, like list races, simply send a single request message to the server which replies with a single response message. This is called a unary RPC. But RPCs can stream request and response messages, meaning there are three other types of RPC to explore. A client streaming RPC is when the client sends any number of messages to the server which replies with a single response message. Imagine each go kart streaming its telemetry data to the server. In server streaming RPCs the client sends a single request message to the server which replies with any number of response messages. Think about real-time updates, like a live text commentary feed. The final type is bidirectional streaming where the client and server can send each other any number of messages. I've got a great idea for how I can use this in my app to provide live race updates. The request messages will tell the server what type of events the client has subscribed to, and the response messages will contain the relevant events. The client can send more messages to the server when they change what events they're interested in receiving.\n\nMy app has been making requests to a server running on my Mac which is also written in Swift. Let's take a look. Setting up the server is straightforward. I create a server object initialized with a transport, and the services it should offer. To start the server, I just call serve. The service is just a type that implements a protocol generated by the build plugin. You can see the list races RPC I implemented earlier: it's an async function which takes a request and returns a response. Implementing it is just a matter of querying the database for races, populating the message and then returning it. To incorporate the streaming RPC, I'll update the service definition, then I'll switch to the server and regenerate the code so I can implement the new RPC. And once that's done, I'll update the app to call it. I'll start by adding a FollowRace RPC to the service definition. Because the RPC streams request and response messages, I need to add the stream keyword before the input and output. Then I need to define the messages. The request message includes the name of the race to follow and a list of event types to subscribe to which is represented as an enum. The response type has a oneof field which is just like a Swift enum with associated values. The message can either hold the locations of each kart or the current race standings, which are defined as separate messages. Now that the service definition has been updated, I'll switch to the server in Xcode so I can work on implementing the new RPC. I'll build the project to regenerate the code.\n\nI have a build error now because the protocol has a new requirement that I haven't implemented yet, so I'll fill out a stub.\n\nThis looks a bit different to the list races RPC because of the streaming. The request parameter is an async sequence of request messages and the response parameter is an object for writing response messages to the client. I know that I need to handle two streams of data simultaneously so I'll need a task group.\n\nI need to wait for the first request message so that I know the name of the race to track and which events the caller is interested in. I'll create an async iterator and await the first message. I'll store the events in a set protected by a mutex because two different tasks will need to access it concurrently. Then I'll add a task to the task group which calls the live race tracker with the name of the race to follow.\n\nThis gives me an async sequence of events which I can then filter to only include the ones the client is currently interested in.\n\nI'll iterate the filtered events and create an empty response message.\n\nThen, I'll switch over the event and populate the message. First, I'll map the array of kart locations from the tracker to the data type used by the RPC. Then, I'll do the same for the standings.\n\nNow I'll write the message to the client.\n\nThere are a few things left to do: the first is to continue consuming the request messages as the caller might change what events they're interested in. We'll use the end of the request stream as a signal that the client no longer wants any more events, so we can cancel the tasks running in the task group to stop sending back messages. Finally I'll restart my server so that the client can call the new RPC.\n\nThat's the RPC implemented in the service, now I can update the app. I'll open the Xcode project for the app, and update the proto file to include the new RPC and messages.\n\nI'll build the project to regenerate the gRPC code.\n\nThen I'll navigate to the RaceInfoView. and add a NavigationLink to a LiveStreamView that I created earlier. Then I'll open up the live stream view.\n\nIt displays a map that will draw annotations representing the positions of each kart in the race. There's also a toolbar button which opens a sheet to display a live leaderboard. The showLeaderboard property tracks whether this is displayed or not. The view already has properties to store the various bits of state I'm interested in, I just need to call the RPC and wire up the data received from the server. First, I'll add the imports I used earlier. Then I'll inject the client via the environment.\n\nLike before I'll create a task, and call manager.withClient.\n\nThen I'll create a kart client and call the FollowRace RPC.\n\nIts structure is different to the unary list races RPC. It has two closures, one for writing request messages and another for handling response messages. I need to send a request message every time the value of showLeaderboard changes. I'll use an AsyncStream to track it over time and store its continuation as a property.\n\nWhen showLeaderboard changes, I'll yield the new value to the continuation. I'll create the AsyncStream and its continuation in the task.\n\nI'll need to yield the current value of showLeaderboard to the stream as the initial value. In the first closure of the RPC I can iterate the stream, and send a message to the server for each value. If the leaderboard is being shown I'll add the standings event.\n\nAnd then I'll write the message to the server.\n\nIn the response closure, I'll iterate the messages and update the view state for each event. I'll use a helper method for handling the events.\n\nI'll switch over the event and map each to the data type used by the view.\n\nI'll start with the kart locations. And then I'll do the same for the standings Finally, I'll iterate the response messages and call the helper for each event.\n\nLet's check it out.\n\nIt looks like a race is about to start at Apple Park. They're heading down towards the Rainbow Arches and now it looks like they're turning right towards the Duck Pond. Monty's in the lead, followed closely by Pepper and Bo. That's great, but I still haven't achieved my goal of making the information available to spectators because the service is running locally. Let's deploy it to the cloud so that it's available to everyone using the app. I'm using Google Cloud Platform to host my service, but you could use another platform like AWS or Fly.io. The approach will be similar but the exact steps will be different. Most servers run Linux and that's what I'll deploy to today. I don't need to make any code changes but I do need to package up the server executable into a container image with its runtime dependencies. Then I'll publish the image to my cloud provider's image registry. After that I'll create a deployment. Finally I'll update the app to target the deployed service. I'll start by creating a Containerfile which describes the steps required to build the container image. I'll use swift:latest as the base image. Next, I'll set the working directory, and copy over the package manifest and source files. Then I'll build the server in release mode and copy it into a known location. At this point, I have the server executable in my image, but it also contains the whole Swift toolchain. I don't need all of that to run my server and it makes the image much larger than it needs to be.\n\nI'll use a multi-stage build and copy the binary into a swift:slim runtime image. Finally, I expose a port and set the entry point to be the server. That's the Containerfile written, at this point I would build and publish the image to a container registry but that will take a few minutes, so I'll use one I published earlier. In the terminal I can use the gcloud run deploy command.\n\nI'll provide the name of the deployment, the image name and the region. Then I need to specify that my service uses http2 and allows unauthenticated requests. When the deployment finishes, it prints out the URL for the service which I'll need when I update the client, so I'll copy it now.\n\nI'll switch back to the app and open the ClientManager. I'll update the connect target to the DNS name of the service from the deploy command. Then I'll enable TLS by changing the transport security option from plaintext to TLS.\n\nLet's test it out.\n\nIt looks like we just made the start of the Finite Loops race.\n\nOh what a disastrous start for Pepper, as Monty takes first position, followed closely by Mycroft and Kiko.\n\nThe drivers are turning into the Infinite Loop campus. Pepper's gained back a few positions. It looks like we're in for a great race here.\n\nI've shown you how to use gRPC Swift to build great live experiences in your apps, and how it can simplify app-to-server communication from defining a service, generating code, all the way through to implementing and deploying a service to the cloud. And that's just the start. gRPC Swift has plenty of built in features to help you take your application from prototype to production Whether that's integration with other Swift packages like Swift OTel or Swift service lifecycle. Or advanced connection management features like custom transports and name resolvers, and client side load balancing. You're now ready to use gRPC in your app: why not prototype part of the app to server interactions and see how simple gRPC Swift makes the workflow. Or you could try out one of the tutorials and examples in the project's repository available on GitHub. Because the project is open source, you can also contribute, whether that's asking questions, improving documentation, or proposing and implementing new features. Thank you for watching and see you on track!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:38",
+ "title": "ListRaces RPC definition",
+ "language": "swift",
+ "code": "edition = \"2024\";\n\nimport \"google/protobuf/timestamp.proto\";\n\nservice SwiftKartService {\n rpc ListRaces(ListRacesRequest) returns (ListRacesResponse);\n}\n\nmessage ListRacesRequest {\n int32 limit = 1 [default = 100];\n}\n\nmessage ListRacesResponse {\n repeated Race races = 1;\n}\n\nmessage Race {\n string name = 1;\n string location = 2;\n google.protobuf.Timestamp start_time = 3;\n int32 laps = 4;\n string championship = 5;\n}"
+ },
+ {
+ "timestamp": "5:55",
+ "title": "grpc-swift-proto-generator-config.json",
+ "language": "swift",
+ "code": "{\n \"generate\": {\n \"clients\": true,\n \"servers\": false,\n \"messages\": true\n }\n}"
+ },
+ {
+ "timestamp": "6:24",
+ "title": "Add gRPC imports",
+ "language": "swift",
+ "code": "import GRPCCore\nimport GRPCNIOTransportHTTP2\nimport SwiftProtobuf"
+ },
+ {
+ "timestamp": "6:38",
+ "title": "Create a gRPC client connected to a local server",
+ "language": "swift",
+ "code": ".task {\n do {\n try await withGRPCClient(\n transport: .http2NIOTS(\n address: .ipv4(host: \"127.0.0.1\", port: 8080),\n transportSecurity: .tls\n )\n ) { client in\n <#code#>\n }\n } catch {\n print(\"gRPC error: \\(error)\")\n }\n}"
+ },
+ {
+ "timestamp": "7:14",
+ "title": "Call the ListRaces RPC and update the view",
+ "language": "swift",
+ "code": ".task {\n do {\n try await withGRPCClient(\n transport: .http2NIOTS(\n address: .ipv4(host: \"127.0.0.1\", port: 8080),\n transportSecurity: .tls\n )\n ) { client in\n let kart = SwiftKartService.Client(wrapping: client)\n let request = ListRacesRequest()\n let response = try await kart.listRaces(request)\n self.races = response.races.map { race in\n RaceInfo(\n name: race.name,\n location: race.location,\n startTime: race.startTime.date,\n championship: race.championship,\n laps: Int(race.laps),\n drivers: race.drivers\n )\n }\n }\n } catch {\n print(\"gRPC error: \\(error)\")\n }\n}"
+ },
+ {
+ "timestamp": "8:30",
+ "title": "ClientManager.swift",
+ "language": "swift",
+ "code": "import GRPCCore\nimport GRPCNIOTransportHTTP2\nimport Synchronization\nimport SwiftUI\n\n@Observable\nfinal class ClientManager: Sendable {\n fileprivate let state = Mutex(State.disconnected)\n\n static func makeTransport() throws -> HTTP2ClientTransport.TransportServices {\n try .http2NIOTS(\n target: .ipv4(address: \"127.0.0.1\", port: 8080),\n transportSecurity: .plaintext\n )\n }\n\n func withClient(\n body: (_ client: GRPCClient) async throws -> Void\n ) async throws {\n let client = try connectIfNecessary()\n try await body(client)\n }\n\n private func connectIfNecessary() throws -> GRPCClient {\n try self.state.withLock { state in\n try state.connectIfNecessary()\n }\n }\n\n func disconnect() {\n let client = self.state.withLock { state in\n state.disconnect()\n }\n\n client?.beginGracefulShutdown()\n }\n}\n\nextension ClientManager {\n enum State {\n case connected(GRPCClient, Task)\n case disconnected\n }\n}\n\nextension ClientManager.State {\n mutating func connectIfNecessary() throws -> GRPCClient {\n switch self {\n case .connected(let client, _):\n return client\n\n case .disconnected:\n let client = try GRPCClient(transport: ClientManager.makeTransport())\n let task = Task { try await client.runConnections() }\n self = .connected(client, task)\n return client\n }\n }\n\n mutating func disconnect() -> GRPCClient? {\n switch self {\n case .connected(let client, _):\n self = .disconnected\n return client\n case .disconnected:\n return nil\n }\n }\n}"
+ },
+ {
+ "timestamp": "8:39",
+ "title": "Propagate ClientManager to child views",
+ "language": "swift",
+ "code": "import SwiftUI\n\n@main\nstruct SwiftKartApp: App {\n let manager = ClientManager()\n\n var body: some Scene {\n WindowGroup {\n RaceScheduleView()\n .environment(manager)\n }\n }\n}"
+ },
+ {
+ "timestamp": "8:52",
+ "title": "Disconnect ClientManager when the scene enters the background phase",
+ "language": "swift",
+ "code": "import SwiftUI\n\n@main\nstruct SwiftKartApp: App {\n let manager = ClientManager()\n @Environment(\\.scenePhase) private var scenePhase\n\n var body: some Scene {\n WindowGroup {\n RaceScheduleView()\n .environment(manager)\n }\n .onChange(of: scenePhase) { _, newPhase in\n switch newPhase {\n case .background :\n manager.disconnect()\n case .inactive, .active:\n break\n @unknown default:\n break\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "9:12",
+ "title": "Inject ClientManager into the view via @Environment",
+ "language": "swift",
+ "code": "@Environment(ClientManager.self) var manager"
+ },
+ {
+ "timestamp": "9:21",
+ "title": "Replace withGRPCClient with manager.withClient",
+ "language": "swift",
+ "code": ".task {\n do {\n try await manager.withClient { client in\n let kart = SwiftKartService.Client(wrapping: client)\n let request = ListRacesRequest()\n let response = try await kart.listRaces(request)\n self.races = response.races.map { race in\n RaceInfo(\n name: race.name,\n location: race.location,\n startTime: race.startTime.date,\n championship: race.championship,\n laps: Int(race.laps),\n drivers: race.drivers\n )\n }\n }\n } catch {\n print(\"gRPC error: \\(error)\")\n }\n}"
+ },
+ {
+ "timestamp": "9:41",
+ "title": "Using SwiftProtobuf",
+ "language": "swift",
+ "code": "var race = Race()\nrace.name = \"Duck Pond Dash\"\nrace.location = \"Apple Park, Cupertino\"\nrace.startTime = .init(roundingTimeIntervalSince1970: 1_781_198_600)\nrace.laps = 6\nrace.championship = \"Corporate Cup\"\nrace.drivers = [\"Monty\", \"Pepper\", \"Mycroft\", \"Pancakes\", \"Duke\", \"Kiko\", \"Sissi\", \"Bo\"]\n\ntry race.serializedBytes()"
+ },
+ {
+ "timestamp": "12:32",
+ "title": "Server",
+ "language": "swift",
+ "code": "let server = GRPCServer(\n transport: .http2NIOPosix(\n address: .ipv4(host: \"127.0.0.1\", port: 8080),\n transportSecurity: .plaintext\n ),\n services: [Service()]\n)\ntry await server.serve()"
+ },
+ {
+ "timestamp": "12:45",
+ "title": "Service",
+ "language": "swift",
+ "code": "struct Service: SwiftKartService.SimpleServiceProtocol {\n private let database = RaceDB()\n\n func listRaces(\n request: ListRacesRequest,\n context: ServerContext\n ) async throws -> ListRacesResponse {\n var response = ListRacesResponse()\n response.races = await database.listRaces(atMost: request.limit)\n return response\n }\n}"
+ },
+ {
+ "timestamp": "13:20",
+ "title": "swift_kart_service.proto",
+ "language": "swift",
+ "code": "edition = \"2024\";\n\nimport \"google/protobuf/duration.proto\";\nimport \"google/protobuf/timestamp.proto\";\n\nservice SwiftKartService {\n rpc ListRaces(ListRacesRequest) returns (ListRacesResponse);\n rpc FollowRace(stream FollowRaceRequest) returns (stream FollowRaceResponse);\n}\n\nmessage ListRacesRequest {\n int32 limit = 1 [default = 100];\n}\n\nmessage ListRacesResponse {\n repeated Race races = 1;\n}\n\nmessage Race {\n string name = 1;\n string location = 2;\n google.protobuf.Timestamp start_time = 3;\n int32 laps = 4;\n string championship = 5;\n repeated string drivers = 6;\n}\n\nmessage FollowRaceRequest {\n string race_name = 1;\n repeated RaceEventType event_types = 2;\n}\n\nenum RaceEventType {\n RACE_EVENT_TYPE_UNSPECIFIED = 0;\n RACE_EVENT_TYPE_KART_LOCATIONS = 1;\n RACE_EVENT_TYPE_STANDINGS = 2;\n}\n\nmessage FollowRaceResponse {\n oneof event {\n KartLocations locations = 1;\n Standings standings = 2;\n }\n}\n\nmessage KartLocations {\n message Kart {\n int32 number = 1;\n double latitude = 2;\n double longitude = 3;\n google.protobuf.Timestamp recorded_at = 4;\n }\n repeated Kart karts = 1;\n}\n\nmessage Standings {\n message Entry {\n int32 kart_number = 1;\n google.protobuf.Duration gap_to_leader = 2;\n int32 position = 3;\n int32 lap = 4;\n }\n\n repeated Entry entries = 1;\n}"
+ },
+ {
+ "timestamp": "14:16",
+ "title": "FollowRace stub",
+ "language": "swift",
+ "code": "func followRace(\n request: RPCAsyncSequence,\n response: RPCWriter,\n context: ServerContext\n) async throws {\n throw RPCError(code: .unimplemented, message: \"FollowRace is unimplemented\")\n}"
+ },
+ {
+ "timestamp": "14:38",
+ "title": "Implement the FollowRace RPC",
+ "language": "swift",
+ "code": "func followRace(\n request: RPCAsyncSequence,\n response: RPCWriter,\n context: ServerContext\n) async throws {\n try await withThrowingTaskGroup { group in\n var iterator = request.makeAsyncIterator()\n guard let first = try await iterator.next() else { return }\n let eventTypes = Mutex(Set(first.eventTypes))\n\n group.addTask {\n let events = tracker.events(forRace: first.raceName).filter { event in\n eventTypes.withLock { $0.contains(event.type) }\n }\n\n for await event in events {\n var message = FollowRaceResponse()\n switch event {\n case .locations(let locations):\n message.locations.karts = locations.map { location in\n var kart = KartLocations.Kart()\n kart.number = Int32(location.number)\n kart.latitude = location.latitude\n kart.longitude = location.longitude\n return kart\n }\n case .standings(let standings):\n message.standings.entries = standings.map { standing in\n var entry = Standings.Entry()\n entry.gapToLeader = .init(rounding: standing.delta, rule: .towardZero)\n entry.kartNumber = Int32(standing.kartNumber)\n entry.lap = Int32(standing.lap)\n entry.position = Int32(standing.position)\n return entry\n }\n }\n\n try await response.write(message)\n }\n }\n\n while let next = try await iterator.next() {\n eventTypes.withLock { $0 = Set(next.eventTypes) }\n }\n\n group.cancelAll()\n }\n}"
+ },
+ {
+ "timestamp": "16:40",
+ "title": "swift_kart_service.proto",
+ "language": "swift",
+ "code": "edition = \"2024\";\n\nimport \"google/protobuf/timestamp.proto\";\n\nservice SwiftKartService {\n rpc ListRaces(ListRacesRequest) returns (ListRacesResponse);\n}\n\nmessage ListRacesRequest {\n int32 limit = 1 [default = 100];\n}\n\nmessage ListRacesResponse {\n repeated Race races = 1;\n}\n\nmessage Race {\n string name = 1;\n string location = 2;\n google.protobuf.Timestamp start_time = 3;\n int32 laps = 4;\n string championship = 5;\n repeated string drivers = 6;\n}"
+ },
+ {
+ "timestamp": "16:56",
+ "title": "Navigation link to LiveStreamView",
+ "language": "swift",
+ "code": "NavigationLink(destination: LiveStreamView(race: race)) {\n Text(\"Live stream\")\n}"
+ },
+ {
+ "timestamp": "17:32",
+ "title": "Call the FollowRace RPC in the LiveStreamView",
+ "language": "swift",
+ "code": "import SwiftUI\nimport GRPCCore\nimport GRPCNIOTransportHTTP2\nimport SwiftProtobuf\n\nstruct LiveStreamView: View {\n private let race: RaceInfo\n\n @Environment(ClientManager.self) var manager\n @State private var tracking: KartTrackingViewModel\n @State private var standings: [StandingsEntry] = []\n @State private var showLeaderboard = false\n @State private var continuation: AsyncStream.Continuation?\n\n init(race: RaceInfo) {\n self.race = race\n self.tracking = KartTrackingViewModel(race: race)\n }\n\n var body: some View {\n VStack {\n KartTrackingMapView(viewModel: tracking)\n .ignoresSafeArea()\n .onAppear { tracking.start() }\n .onDisappear { tracking.stop() }\n }\n .onChange(of: showLeaderboard) { _, newValue in\n continuation?.yield(newValue)\n }\n .sheet(isPresented: $showLeaderboard) {\n LeaderboardView(race: race, standings: standings)\n .presentationDetents([.fraction(0.3), .medium, .large])\n .presentationBackgroundInteraction(.enabled)\n }\n .toolbar {\n Toggle(isOn: $showLeaderboard) {\n Label(\"Leaderboard\", systemImage: \"list.number\")\n }\n }\n .toolbarBackgroundVisibility(.visible, for: .navigationBar)\n .task {\n do {\n let (stream, continuation) = AsyncStream.makeStream(of: Bool.self)\n self.continuation = continuation\n continuation.yield(showLeaderboard)\n\n try await manager.withClient { client in\n let kart = SwiftKartService.Client(wrapping: client)\n try await kart.followRace { requestStream in\n for await showLeaderboard in stream {\n var message = FollowRaceRequest()\n message.raceName = race.name\n message.eventTypes = [.kartLocations]\n if showLeaderboard {\n message.eventTypes.append(.standings)\n }\n try await requestStream.write(message)\n }\n } onResponse: { responseStream in\n for try await message in responseStream.messages {\n if let event = message.event {\n await handleEvent(event)\n }\n }\n }\n\n }\n } catch {\n print(\"gRPC error: \\(error)\")\n }\n }\n }\n\n @MainActor\n private func handleEvent(_ event: FollowRaceResponse.OneOf_Event) {\n switch event {\n case .locations(let locations):\n self.tracking.updateKartCoordinates(\n locations.karts.map {\n TrackedKart(number: $0.number, latitude: $0.latitude, longitude: $0.longitude)\n }\n )\n case .standings(let standings):\n self.standings = standings.entries.map {\n StandingsEntry(\n kartNumber: $0.kartNumber,\n secondsToLeader: $0.gapToLeader.timeInterval,\n position: $0.position,\n lap: $0.lap\n )\n }\n }\n }\n}\n\n#Preview {\n NavigationStack {\n LiveStreamView(race: .example4)\n .environment(ClientManager())\n }\n}"
+ },
+ {
+ "timestamp": "20:55",
+ "title": "Containerfile",
+ "language": "swift",
+ "code": "FROM swift:latest AS builder\n\n# Copy sources into /app\nWORKDIR /app\nCOPY Package.swift Package.resolved .\nCOPY Sources/ Sources/\n\n# Build the server\nRUN swift build -c release --product server\nRUN cp \"$(swift build -c release --show-bin-path)/server\" /usr/bin/server\n\n# Copy the binary from the builder into a smaller runtime image.\nFROM swift:slim\nCOPY --from=builder /usr/bin/server /usr/bin/server\n\nEXPOSE 8080\nENTRYPOINT [\"/usr/bin/server\"]"
+ },
+ {
+ "timestamp": "21:56",
+ "title": "Deploy service",
+ "language": "swift",
+ "code": "gcloud run deploy wwdc-demo-server \\\n --image us-central1-docker.pkg.dev/wwdc26/wwdc-demo-server/wwdc-demo-server:latest \\\n --region us-central1 \\\n --use-http2 \\\n --allow-unauthenticated"
+ },
+ {
+ "timestamp": "22:22",
+ "title": "Target deployed service",
+ "language": "swift",
+ "code": "static func makeTransport() throws -> HTTP2ClientTransport.TransportServices {\n try .http2NIOTS(\n target: .dns(host: \"wwdc-demo-server-863666503339.us-central1.run.app\"),\n transportSecurity: .tls\n )\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "About gRPC",
+ "url": "https://grpc.io/"
+ },
+ {
+ "title": "gRPC Swift Extras",
+ "url": "https://github.com/grpc/grpc-swift-extras"
+ },
+ {
+ "title": "gRPC Swift Protobuf",
+ "url": "https://github.com/grpc/grpc-swift-protobuf"
+ },
+ {
+ "title": "gRPC Swift NIO Transport",
+ "url": "https://github.com/grpc/grpc-swift-nio-transport"
+ },
+ {
+ "title": "gRPC Swift",
+ "url": "https://github.com/grpc/grpc-swift-2"
+ },
+ {
+ "title": "Swift on Server",
+ "url": "https://www.swift.org/server/"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/265/4/05249c6d-4136-4164-a8d0-5db0bbb22c7f/downloads/wwdc2026-265_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/265/4/05249c6d-4136-4164-a8d0-5db0bbb22c7f/downloads/wwdc2026-265_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "346",
+ "year": "2025",
+ "title": "Meet Containerization",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/346"
+ },
+ {
+ "id": "10216",
+ "year": "2024",
+ "title": "Explore the Swift on Server ecosystem",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10216"
+ },
+ {
+ "id": "10171",
+ "year": "2023",
+ "title": "Meet Swift OpenAPI Generator",
+ "url": "https://developer.apple.com/videos/play/wwdc2023/10171"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:16.849Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-267.json b/data/wwdc/videos/2026-267.json
new file mode 100644
index 0000000..6691057
--- /dev/null
+++ b/data/wwdc/videos/2026-267.json
@@ -0,0 +1,107 @@
+{
+ "id": "267",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/267/",
+ "title": "Migrate to Swift Testing",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi! My name is Jerry, and I'm an engineer on the Swift Testing team. Today, I'll share how it's easier than ever to migrate from XCTest to Swift Testing.\n\nWe introduced Swift Testing in Xcode 16. It's a modern testing library with an expressive and concise interface for writing tests. It also fits right in with the Swift ecosystem. For example, it was built with Swift concurrency in mind, so it runs test cases in parallel and delivers results super quickly. I'll begin with a review of some Swift Testing building blocks and compare those to similar concepts from XCTest. Next, I'll add Swift Testing to an XCTest project. Along the way, I'll demonstrate how to reuse existing code with the test framework interoperability feature, and even if you don't use XCTest, please stick around! I'll share how to use Swift Testing features to boldly go where no XCTest has gone before! Let's get started with the basics of Swift Testing.\n\nI begin a test file by importing the Swift Testing framework along with a testable import to access internal types. Then, I create a new function and annotate it with the @Test macro to declare it as a test.\n\nSwift supports raw identifiers, which are indicated by these backticks. They let me mix in spaces and punctuation to help me read longer test names. In the body of the test, I use the #expect macro to create this expectation that fruits have a tropical climate. That's all it takes to create a new test! The @Test and #expect macros are the core building blocks for most tests. If you're totally new to Swift Testing, or you just want to jog your memory, \"Meet Swift Testing\" is a fantastic resource. It covers additional building blocks and testing workflows.\n\nThe #expect macro is super flexible and replaces many XCTest assertions. To replace XCTFail, which is an unconditional failure, use Issue.record instead.\n\nWith Swift Testing, you'll enjoy using less code to write more powerful and expressive tests. However, keep using XCTest for a few scenarios. UI automation and performance testing APIs are only available in XCTest, and when testing code that throws Objective-C exceptions, you have to use XCTests also written in Objective-C. That's because Swift code, including XCTests in Swift, can't safely handle these exceptions. All right, I know you're eager to write some tests now! So, I'll demonstrate how to migrate fearlessly to Swift Testing.\n\nI'll explain my strategy for migrating test code in small chunks.\n\nNext, I'll introduce test framework interoperability, a powerful tool that will allow me to reuse existing test code.\n\nAnd to help your own migration go smoothly, I'll share a few common patterns to follow. Ok. Let's talk migration strategy.\n\nWhen I modify old tests, I introduce risk, even for small changes. So, I'm going to leave most XCTests in their place. When I'm ready, I'll modify tests a few at a time and focus on the ones that I update most often. My existing test targets can include tests from both frameworks. So, I can already use Swift Testing for my new tests, although they can't go inside XCTest classes. Ok! Now that I have a migration strategy, I'm ready to put it to the test. I made an app to train birds in my neighbourhood to do fruit delivery. Just like how birds migrate with the changing of the season, I think it's the season for me to also migrate to Swift Testing.\n\nHere's my XCTest suite for my Fruit data structure. I recently added climate information to the fruit but haven't tested it yet. I think I want to add some new tests using Swift Testing, and I can do that without even leaving this file! First, I'll import the Swift Testing framework.\n\nThen, I'll add my test at the end of this file.\n\nBy the way, I love that Swift Testing lets me put this outside of a test suite. I can always create a parent suite later, once I have more tests. In the Test navigator, I'm noticing the new test is already included. Now, I'll go to the Product menu and select Test.\n\nGreat! My new test ran successfully.\n\nSome of my tests require more setup. This test checks in an array of fruit has all unique names. I originally wrote this test with multiple lines of code to set up the assertion.\n\nWhen I ran the test later, I found it difficult to understand why it failed. This test was trying to tell a story, but it got lost in all this code. So, I extracted this multi-step assertion into a helper function. Now, it's clearer what I'm testing and how I'm testing it. As a finishing touch, I provided file and line number parameters and passed them to XCTFail.\n\nXcode now attributes the failure to the line where I call the helper.\n\nI want to create new tests with Swift Testing which also call this helper, and, ultimately, XCTFail. However, XCTFail isn't part of Swift Testing. This is where test framework interoperability can help! It's a feature that lets you safely call API from one test framework, while in the body of a test, from the other.\n\nWhen considering interoperability, there's two directions to think about. You can call XCTest API which reports an issue in a Swift Testing test. This is what happens when I reuse my assertUnique helper which wraps XCTFail. In the other direction, you can call Swift Testing API which reports an issue in an XCTest. I'll explain this one later.\n\nIn either case, you end up creating a cross-framework issue, with the issue-reporting API and the test it's called in belong to different frameworks. Xcode enables interoperability by default to handle these issues. Let me show you how.\n\nI call the assertUnique helper in testUniqueFruitNames. The helper reports failures using XCTFail. I've created another Swift Testing test with the same body. This helper should report the same failure, but in this test, it's a cross-framework issue.\n\nLet's compare the result after running this test.\n\nHmm, I'm noticing the test passes, but I actually thought it would fail, like the XCTest above. A test failure can be a good thing, because it means my helper is working to catch bugs. There's some messages here, though. I'll click to view them.\n\nInteroperability created two warnings, indicated by the purple triangles. Because these aren't errors, the test still passes. The first warning tells me that Lychee is a duplicate name, and the second warning instructs me to replace XCTFail with Issue.record from Swift Testing.\n\nInteroperability comes with different modes for handling cross-framework issues. I've just explained the limited mode. In this mode, cross-framework issues from XCTest are warnings. Test plans created before Xcode 27 inherit limited mode. For new projects, Xcode uses the complete mode. In this mode, those same issues stay as errors. Change the mode at any time using your Test Plan Settings. You can find it under the Test Execution section. Let's find out how the complete mode changes my test results. I'll go edit my test plan.\n\nHere, I'll filter on interoperability, and I'll change that mode to Complete.\n\nNow I'll go back to my \"Unique fruit names\" test, and run it again.\n\nNow, my test fails. Let's check the messages.\n\nThe complete mode preserves the error created by XCTFail. It also keeps the warning that instructs me to migrate. Think of complete mode as a step above limited mode. It elevates cross-framework issues from warnings to errors, so you're less likely to miss them. One more step above complete mode is strict mode. For cross-framework issues from XCTest, strict mode stops the test in its tracks with a fatal error. This helps you find places to replace XCTest API with Swift Testing API. I've already updated the mode to strict. Now, I'll run my test again.\n\nWhoa! The test stopped exactly where I call XCTFail in my helper function! Let's check out this message.\n\nOnce again, I have instructions to replace XCTFail, but I have to keep in mind I still have XCTests that call this helper. That's why interoperability also supports cross-framework issues from Swift Testing. In all modes, those issues remain errors. So, you're empowered to call Swift Testing API within tests from both frameworks. Replacing XCTFail just takes a few steps. I'll start by replacing the XCTest import with a Swift Testing import.\n\nThen, replace XCTFail with Issue.record, and sourceLocation replaces the original file and line parameters.\n\nAfter updating my helper, I'll run all my test cases again. I'll use CMD+U, which is the shortcut for product test.\n\nWith the updated helper, my new test and my original XCTest both fail as expected. Thanks to test framework interoperability, I replaced XCTest API with Swift Testing API without changing the meaning of my tests. There's one more mode I haven't introduced yet. You can set the mode to none to opt-out of interoperability. After opting-out, Xcode won't report cross-framework issues in either direction, but, those issues can highlight bugs in your app. You won't catch those bugs if you disable interoperability. So, if you have to use this mode, only use it temporarily. Instead, prefer complete or strict mode. Complete mode maintains cross-framework issues as errors, and it's a worthy steup up from the limited mode. The strict mode, is, well, strict! It's a great fit if you want to completely prevent cross-framework issues from XCTest. You can also use interoperability and its different modes in Swift Package projects. Here's an example of a project created with swift-tools-version: 6.3, and it has a test that produces a cross-framework issue from XCTest. By default, the Swift 6.4 toolchain enables limited mode. When I run this project with the swift test command, I notice that it reports the cross-framework issue as a warning.\n\nTo use complete mode, update your package to swift-tools-version 6.4 or newer.\n\nAfter bumping the tools version, the earlier issue is now an error.\n\nOverride the default mode at any time using an environment variable: SWIFT_TESTING_XCTEST_INTEROP_MODE For the value, provide the name of the mode in lowercase.\n\nFinally, interoperability supports a limited set of APIs from both testing frameworks. I just demonstrated the XCTFail and Issue.record API. In XCTest, it also supports all other test assertions, and in Swift Testing, it supports both expectation macros: #expect and #require. The known issue API in Swift Testing can mark XCTest assertion failures as known, and Test.cancel can skip test cases in XCTest.\n\nOn your own migration journey, you might encounter some common patterns. I'll share some tips for how to address those.\n\nThe first pattern is skipping tests. In XCTest, you skip tests using the XCTSkip API.\n\nTo replace it with Swift Testing API, use Test.cancel. If you're writing new tests in Swift Testing, Test.cancel will work there as well. But, Swift Testing has traits, which are annotations you can attach to test functions and suites. Move test enablement logic out of the test body with the enabled or disabled trait.\n\nAnother common pattern is halting on failure. In XCTest, you assign the continueAfterFailure property to false. This halts tests on their first failed assertion. To replace with Swift Testing API, use the #require macro. It throws an error upon failure, halting the test, and you don't need to set continueAfterFailure anymore. With Swift Testing, you also get to choose which expectations halt the test and which don't.\n\nFor more information, check out \"Migrating a test from XCTest\" in the developer documentation. It covers many more scenarios and will be a great reference in your migration journey. Xcode's Coding Assistant is also aware of this documentation. It can help formulate a migration strategy or review your work. It even has a skill to automate parts of your migration. Wow, we covered a lot! So, in summary, here's how you can migrate fearlessly to Swift Testing.\n\nDon't feel pressured to modify your XCTests until you're ready. Focus on writing new tests using Swift Testing. By relying on interoperability, you can even reuse your helper code which wraps XCTest API.\n\nIn the process, you'll turn up cross-framework issues from XCTest. Interoperability allows you to replace XCTest API with Swift Testing API to address these, and Xcode 27 enables interoperability by default. Make sure you handle all future cross-framework issues by upgrading to the complete or strict mode.\n\nNow, it's time to turn the spotlight on Swift Testing.\n\nAfter you migrate to Swift Testing, you get access to new tools to supercharge your tests. Let's start with parameterized tests.\n\nThese are tests that repeat with different arguments. Each argument becomes a separate test case. All Swift Testing tests, including parameterized test cases, run in parallel by default. This can be faster than running serially. Check out this example.\n\nI've migrated my project's bird test to Swift Testing. This checks that every bird can flap its wings from forty to a hundred times. I can't write a separate test for each of those combinations though, there's just too many of them. In the body of my original XCTest, I used a nested loop to generate all the combinations. I'll go ahead and run the test.\n\nA few things stuck out to me. The test took a while to finish, probably because there's lots of combinations. Also, the test fails, but I don't even know which bird failed.\n\nIf I were still using XCTest, I'd have to catch these errors or run the test with a debugger attached. I have a better idea. Let's make this a parameterised test. I'll begin by removing these loops from the test body, and instead, I'll define bird and count as test function parameters.\n\nI'll provide the input for birds and counts as arguments in the test macro.\n\nSwift Testing will pair each bird from the first argument and each count from the second. I'll run the test again.\n\nHey! This time, the test finished almost instantly, because Swift Testing ran all my combinations in parallel. In the Test navigator, my parameterised test now has a disclosure arrow to its left. I'll click it to show all the combinations I'm testing.\n\nWow, that's a lot of test cases. Ok, I still have to find the failing combinations. In the Test navigator, I'll filter the failing test results.\n\nAh, there it is! The swallow can't flap its wings less than 43 times. Using a parameterised test supercharged my test execution. Not only did I get results faster, I could clearly tell which test inputs failed.\n\nNext, I want to show how to supercharge test coverage with exit tests. Note that exit tests are only supported on macOS, Linux, FreeBSD, and Windows. I'm looking for code that needs test coverage, and I found something in my Bird initialiser.\n\nIf I get an empty name for new Birds, I halt the program with a precondition failure.\n\nI can verify if I've already tested this by going to the \"Editor\" menu and showing \"Code Coverage.\" The coverage annotation shows the precondition in red, which means that none of my tests run this code. An exit test is the perfect tool to add this coverage.\n\nYou define an exit test with the #expect macro. Provide an expected process exit condition, along with the body of the exit test. When you start the test, Swift Testing runs the exit test body in a child process. Because that code is isolated, it can crash without disrupting other tests. The exit test waits for the child process to finish and checks the exit status to determine test success or failure. I can add a new test in this extension of my test suite. I'll use the #expect macro to create an exit test and specify the process should exit with failure.\n\nInside the body of the exit test, I'll create a Bird with an empty name. That should crash, but it'll be isolated, because Swift Testing will run this code in a separate process. Now, I'll run all my tests.\n\nGreat, my exit test passes! Let's check the coverage one more time. I'll right click and select \"Jump to Definition\" on the Bird initializer. Now that we're back, let's examine the coverage again. The coverage annotation now highlights the precondition in green, which means it's now tested! That was a quick preview of two ways to supercharge your tests, and if you have ideas for how Swift Testing can improve, I encourage you to contribute! That's because Swift Testing is open source. It's part of the SwiftLang organization on GitHub. It's available on more platforms than ever before, with full support starting this year for FreeBSD.\n\nThe Testing Workgroup governs the project and runs regular meetings that any Swift community member can attend. New features are guided by Swift Evolution. In fact, interoperability was one of those! Share your opinions and ideas by joining us on the Swift Forums. Now, it's your turn to make like a bird, and migrate to Swift Testing! Interoperability in Xcode 27 makes this transition easier than ever. Try it out in your project and explore its different modes. Along the way, you can adopt powerful tools, such as parameterized tests and exit tests. To, go further, check out \"Go further with Swift Testing\" from WWDC 2024. Have fun renovating your tests!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "1:12",
+ "title": "Name a test using a raw identifier",
+ "language": "swift",
+ "code": "import Testing\n\n@testable import DemoApp\n\n@Test func `Default climate: tropical`() async throws {\n let fruit = Fruit(name: \"Coconut\")\n #expect(fruit.climate == .tropical)\n}"
+ },
+ {
+ "timestamp": "5:03",
+ "title": "Wrap XCTFail in a test helper function",
+ "language": "swift",
+ "code": "func testUniqueFruitNames() async throws {\n assertUnique(Market.fruits + [Fruit.lychee])\n}\n\n// TestHelpers.swift\n\nfunc assertUnique(_ fruits: [Fruit], file: StaticString = #filePath, line: UInt = #line) {\n var uniqueNames = Set()\n for name in fruits.map(\\.name) {\n if !uniqueNames.insert(name).inserted {\n XCTFail(\"Duplicate name: \\(name)\", file: file, line: line)\n }\n }\n}"
+ },
+ {
+ "timestamp": "10:12",
+ "title": "Replace XCTFail with Issue.record in the test helper",
+ "language": "swift",
+ "code": "import Testing\n\nfunc assertUnique(_ fruits: [Fruit], sourceLocation: SourceLocation = ...) {\n var uniqueNames = Set()\n for name in fruits.map(\\.name) {\n if !uniqueNames.insert(name).inserted {\n Issue.record(\"Duplicate name: \\(name)\", sourceLocation: sourceLocation)\n }\n }\n}"
+ },
+ {
+ "timestamp": "12:15",
+ "title": "Run Swift Package tests with the strict interoperability mode from Terminal",
+ "language": "swift",
+ "code": "> SWIFT_TESTING_XCTEST_INTEROP_MODE=strict swift test"
+ },
+ {
+ "timestamp": "13:10",
+ "title": "Common migration: skipping tests",
+ "language": "swift",
+ "code": "let isFall = false\n\n// XCTest\nfunc testSwallowFallMigration() async throws {\n try XCTSkipIf(!isFall, \"Wrong season for migration\")\n // ...\n}\n\n// Test.cancel interoperability from Swift Testing\nfunc testSwallowFallMigration() async throws {\n if !isFall {\n try Test.cancel(\"Wrong season for migration\")\n }\n // ...\n}\n\n// ✅ Prefer test trait in Swift Testing\n@Test(.enabled(if: isFall, \"Wrong season for migration\"))\nfunc `Swallow fall migration`() async throws {\n // ...\n}"
+ },
+ {
+ "timestamp": "13:41",
+ "title": "Common migration: halting after test failures",
+ "language": "swift",
+ "code": "func testExample() async throws {\n #expect(Fruit.banana.climate == .temperate)\n\n try #require(Fruit.banana == Fruit.plantain)\n XCTFail(\"This is never reached\")\n}"
+ },
+ {
+ "timestamp": "15:57",
+ "title": "Example of nested loops which can be converted into a parameterized @Test function",
+ "language": "swift",
+ "code": "struct BirdTests {\n\n @Test func `Birds flap wings successfully`() async throws {\n for bird in Aviary.birds {\n for count in (40...100) {\n try await bird.flapWings(count: count)\n }\n }\n }\n\n}"
+ },
+ {
+ "timestamp": "16:47",
+ "title": "Refactor nested loops into a parameterized @Test function",
+ "language": "swift",
+ "code": "struct BirdTests {\n\n @Test(arguments: Aviary.birds, 40...100)\n func `Birds flap wings successfully`(bird: Bird, count: Int) async throws {\n try await bird.flapWings(count: count)\n }\n\n}"
+ },
+ {
+ "timestamp": "18:21",
+ "title": "Precondition check on empty input name in an initializer",
+ "language": "swift",
+ "code": "// In `Bird.init(...)`\nif name.isEmpty {\n preconditionFailure(\"Bird name cannot be empty\")\n}"
+ },
+ {
+ "timestamp": "19:27",
+ "title": "Add coverage for precondition failure with exit test",
+ "language": "swift",
+ "code": "extension BirdTests {\n\n @Test func `Bird with empty name crashes`() async throws {\n await #expect(processExitsWith: .failure) {\n _ = Bird(name: \"\")\n }\n }\n\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/267/4/d54e4861-10d9-4d4d-9952-3fe311cd2dc4/downloads/wwdc2026-267_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/267/4/d54e4861-10d9-4d4d-9952-3fe311cd2dc4/downloads/wwdc2026-267_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "262",
+ "year": "2026",
+ "title": "What’s new in Swift",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/262"
+ },
+ {
+ "id": "10195",
+ "year": "2024",
+ "title": "Go further with Swift Testing",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10195"
+ },
+ {
+ "id": "10179",
+ "year": "2024",
+ "title": "Meet Swift Testing",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10179"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:16.768Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-268.json b/data/wwdc/videos/2026-268.json
new file mode 100644
index 0000000..eff409e
--- /dev/null
+++ b/data/wwdc/videos/2026-268.json
@@ -0,0 +1,112 @@
+{
+ "id": "268",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/268/",
+ "title": "Profile, fix, and verify: Improve app responsiveness with Instruments",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Developer Tools",
+ "Machine Learning & AI",
+ "Swift"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Art, I work on Xcode. And I am Harjas from the Instruments team. Delivering a fluid user interface is the baseline for a great app. But there is a lot happening behind the scenes. Navigating the layers of the software stack can be confusing. Instruments 27 makes it easier than ever to understand how to make your apps feel fast and responsive.\n\nIt starts with app foundation. The Swift code you write is fast and expressive. But this code does not execute in isolation. Even simple code is hiding a lot of complexity.\n\nThe compiler and runtime inject dynamic dispatch, safety checks, and reference counting behind the scenes. Then, it relies on the Operating System for memory allocation, process scheduling, and I/O routing.\n\nAfter that, OS delegates to the underlaying platform. This finally routes execution directly to the physical hardware. It is, also, worth noticing while I've highlighted a few common components here, there are many more compiler and system features at play. To help you triage performance issues, we will share a mental model to understand and optimize your app responsiveness. First, we will look at CPU saturation, and show you how to investigate periods of high system utilization. Next, we will discuss sampling data visualization, exploring how to interpret your profiling data. Then, we will look at execution contention, and examine what happens when tasks are starved for resources. And finally, we analyze system blocking, uncovering the root causes behind why your application stops processing. But before we dive in, let's establish the diagnostic flow. When an application drops frames or hangs. The first step is the Time Profiler. It provides high-level overview needed to orient yourself. From there, the question is: what is the CPU doing during the hang? If CPU usage is high, then the thread is busy and the work is taking too long. This points to a code performance bottleneck. When you hit a performance issue on the main thread, there are two ways to fix it. The first is code optimization: refactoring algorithms to execute faster. But, if the heavy workload is unavoidable, resolve it by offloading the work to a background task so the user interface remains responsive. If the application hangs but the processor is idle, optimizing algorithms won't be helpful. This, usually, suggest that the main thread is stuck waiting for a resource to free up. This block can fall into many categories, but some common ones are: waiting for the File I/O, waiting on a synchronization lock, or waiting for Inter-Process Communication. Because Time Profiler only monitors active CPU cycles, it provides no visibility into these events. We will demonstrate this workflow using a note-taking application, that we have been prototyping. This app allows me to draw, add images, and use a lasso tool to move elements around. But we've noticed three issues while testing the app.\n\nLet's capture a profile to pin these issues down. We start right here in Xcode. To begin profiling, I will open the Product menu and select Profile.\n\nThis will create a release build of my application. A debug build trades off runtime performance for debug ability, so profiling data from debug builds can be misleading. Profiling a release build is crucial to get the most actionable data. Because my codebase utilizes Swift Concurrency, I will choose the Swift Concurrency template from the picker.\n\nWithin this template, we still have access to the Time Profiler instrument.\n\nI'll start the recording, and switch over to iPad to record the workflows that were exhibiting performance issues during my testing.\n\nWhen I hit save on a note, the pencil is not instantly responsive.\n\nWhen I scroll through my notes, the UI is not smooth.\n\nAnd when use the lasso tool, I noticed that it hangs.\n\nNow, I stop the recording at the top left corner.\n\nAnd we have a trace containing all the information about these three hangs.\n\nHey Harjas, the trace is ready. I'll AirDrop you the file, so we can start the investigation. Thanks, Art.\n\nInstruments 27 makes it easy to read and interpret your data. The timeline at the top displays horizontal tracks for your tasks, actors, and executors. These tracks provide a high-level view of resource usage and events over time. Below the timeline is the detail area. This detail view is based on the selected track in the timeline, and each track provides its own set of details. You can switch between them by changing the selection with the pop-up button in the middle bar. On the right-hand side is a brand new Inspector panel. It surfaces additional details and actions based on selection in the timeline or the detail view. First, let's investigate why the lasso tool wasn't able to keep up with the pencil. To make this easier I am going to use a tool called OSSignpost. OSLog and OSSignpost offer a set of APIs to enable logging and tracing. For our use case, I added an os.signpost interval around lasso selection. I achieved this by using the OSSignposter type which has APIs to start and stop tracing an interval. When creating this signposter, I set the subsystem to \"Demo App\" so that I know it's related to my App. And, I set the category to points of interest, so that instruments will automatically surface this data in the points of interest track. Now, we can find the lasso selection interval in the timeline.\n\nAnd use the context menu on the interval to filter the trace to this time period.\n\nThe hangs instrument shows that there were several hangs during lasso selection, which lines up with what Art was experiencing. As Art said earlier, we should check the CPU usage on the main thread during this hang. We can find that by expanding the process track, which reveals all the threads for this process. The main thread shows that CPU usage is high, staying around a 100% during this time period. Which suggests our code is executing, but it takes too long. So we should investigate using Time Profiler. Hey Art, can you walk through the ways we can visualize sampling data in Instruments. Absolutely. When your application is running, the CPU is generating thousands of samples every single second. Instruments is built to handle that scale, providing visualizations for different categories of performance analysis. Let's break down how the Time Profiler translates raw execution into a call tree.\n\nIt uses a hardware timer to sample the state of your app's execution at regular intervals. The default sampling rate is one millisecond.\n\nWhen the timer fires, it records the current call stack on every core. In this first sample, main calls saveNote. In the call tree, each of these functions receives a weight of 1. Because saveNote is the function actively executing at the bottom of the stack, it receives a self-weight of 1.\n\nA millisecond later, the second sample fires. This time, we sample main calling renderCanvas, which calls drawStroke function. Their weights are, also, added to the tree.\n\nOn the third sample, drawStroke has finished, so we sample main and renderCanvas. Main increments to a total weight of 3, and renderCanvas increments to a weight of 2.\n\nNotice the fast swift_retain call. Because it started and finished entirely between the samples, it is never recorded, but it is important to note that the more frequent it called the more likely it will be sampled.\n\nThis call tree is the raw data driving your investigation. But, reading it can be difficult to parse at a glance. To make this data even more intuitive, Instruments can render it visually as a flame graph. The flame graph maps the all tree structure into spacial blocks. Main, with a weight of 3, forms the top level. The functions it called, renderCanvas and saveNote, cascade downwards proportionally to their weights, with drawStroke node sitting at the very bottom.\n\nIn this visualization, the vertical axis represents the call stack, with callers on top and callees growing downward. The horizontal axis represents total CPU time, but instead of a chronological timeline, it's an aggregated view. The wider the bar, the more samples that function appeared in, allowing you to instantly spot expensive code paths.\n\nHowever, for code that is called from a lot of places, like Swift runtime functions and various helper utilities. The structural view of a flame graph distributes their total cost. The execution time is fractured into small pieces across every distinct branch that calls them. This distribution makes it difficult to answer which specific functions burned the most overall cycles. To answer that question, Instruments introduces new analysis mode Top Functions. This new mode discards the call hierarchy. Instead, it extracts every single scattered node and merges them together to form one block.\n\nThis is evaluated using the self metric, which calculates the amount of time spent executing instructions directly inside that specific function. Harjas, I think we are ready look at the data, take it away. Thanks Art. When viewing a profile in Instruments, the default view is an outline call tree display, this is great when looking at sample counts. But, as Art mentioned a flame graph can be easier to scan through and visually spot issues. Using the segmented control in the bar above the call tree, we can switch the detail view to display the flame graph. Doing a quick scan through it reveals that the time is split across different codepaths in the rendering code for our canvas. There is no single obvious issue to address. Rather, these different codepaths sum together to be costly enough to cause hangs. I am going to try the new top functions mode in Instruments, as it might surface something that would be harder to find in the call tree or the flame graph. This can be found in the same control we used before.\n\nOn the left hand side is a list of all the Top Functions sorted by self weight. The right hand side displays a flame graph of all the code paths that called into the selected function. The Top Function during lasso selection is swift_project_boxed_opaque_existential.\n\nThis runtime function is responsible for unwrapping an existential so our code can operate over it. I will ask the coding assistant in Xcode to rewrite our drawing code to use concrete types and generics instead of existentials.\n\nWhile this is running, I want to explain what is an existential and why I asked the coding assistant to make this change. In Swift, we often want to have a variable that can hold any type conforming to a protocol without having to know the specific type at compile time. One way of achieving this is by using the any keyword before the protocol name, this is called an existential. Because the possible types can vary in size, existentials may require additional work to access and operate over the underlying value. This is proving to be too expensive for our use case. There are several alternatives to existentials when performance is paramount including concrete types, generics, and in some cases, enums. These approaches give the compiler more information, allowing for better optimizations. To learn more about generics in Swift, watch \"Embrace Swift generics\" from WWDC22. Let's check back in with the assistant.\n\nIt appears that the coding assistant has finished making those changes. I will send an updated trace over to you, Art. Thank you Harjas. To verify the fix we made, I could open them up side-by-side in different windows and compare the Top Functions data. However, Instruments now allows you to directly compare profiling data across runs in a single document, to help speedup the work of confirming your changes.\n\nNew in Instruments we're excited to introduce Run Comparisons. It computes the exact performance delta by cross-referencing all of samples from the baseline trace and optimized trace. It evaluates every node in the stack. To do this, Instruments matches the old version of a function from your baseline run directly to the new version in your optimized one.\n\nOnce matched, it calculates the delta and sorts them based on their performance difference. A red block indicates a performance regression. A green block indicates a performance improvement, or run time optimization. So, let's try it out. To ensure an accurate comparison without noise, we first filter both runs to the exact same os_signpost interval for the lasso selection.\n\nThen, to compare sampling data on the main thread, I will select the main thread track.\n\nAnd, by clicking the compare button in the middle bar, I can select my baseline run from the dropdown menu.\n\nThis will add a comparison tab in the sidebar. You can create multiple comparisons, and they are saved to the document to make collaboration easier. A comparison tree can be visualized as a textual call tree.\n\nHere we can see that overall execution time for lasso selection has decreased.\n\nSwitching to a flame graph, we can see code paths that improved in green and the ones that regressed in red. When the coding assistant adopted generics it introduced new functions, which Run Comparisons will mark as regressions. And in Top Functions view we can now see what improved the most and what regressed the most. By default, regressions in Run Comparisons are sorted to the top. These regressions are new functions added by the coding assistant as it worked to eliminate the usage of existentials.\n\nWe can flip the sort order to see the improvements.\n\nHere, the swift_project_boxed_opaque_existential call has been removed completely. And overall, the improvements out weight the regressions. This confirms that adopting concrete types and generics successfully eliminated this specific runtime overhead. To learn more about optimizing CPU work, watch the \"Optimize CPU performance with Instruments\" session from WWDC25. Harjas, 1 hang down, 2 to go. That's right. Now that we've optimized the drawing code, we can return to our baseline run and keep optimizing other aspects of the app. There are still several hangs that need to be investigated.\n\nUnfortunately, these don't have any logs in the points of interest track to help contextualize what was happening at the time. Another way to contextualize this would be to see what tasks are on the Main Actor during these hangs. Instruments 27 has a new Swift executors instrument.\n\nThis instrument visualizes the Main Actor, the global concurrent executor, and any custom executors in your process. For each of these hangs, the Main Actor track shows a corresponding Swift task called renderThumbnail. We can select this track and a get a summary of all the tasks running on the Main Actor.\n\nIt appears that we have several render thumbnail tasks on the Main Actor taking a few hundred ms to run.\n\nThis could help explain why scrolling the list of notes wasn't smooth. Let's follow our diagnostic flow, and check the CPU usage. I will filter the trace to one of these hangs.\n\nAnd use the inspector to pin the main thread.\n\nIn this period, time profiler reports that CPU usage of the main thread is around a 100%. So we aren't waiting on other system resources, these tasks are just taking too long to run on the Main Actor. So why is this a problem? Well, first let's review what the Main Actor is and then discuss how we can resolve this hang. The Main Actor is responsible for handling all user interface updates and interactions. The application renders all the thumbnails asynchronously. But because this code was called from SwiftUI, it inherited the Main Actor context. These tasks will compete with critical UI updates for the Main Actor, preventing the app from delivering a smooth experience. To resolve this, we must route the thumbnail rendering to the thread pool. This frees up the Main Actor, allowing the pending UI events to execute smoothly.\n\nThis is the code responsible for generating our thumbnails. We can refactor it by adding the @concurrent attribute to the task initializer, this will move the thumbnail rendering task off the Main Actor, and onto the global executor. The swift compiler will check that this code change does't introduce any race conditions. In the updated trace, the Swift executors instrument shows that the thumbnail rendering tasks have moved from the Main Actor track to the global executor track. Not only did moving this to the global concurrent executor prevent hangs in the UI, but it also allowed us to render these thumbnails in parallel.\n\nTo learn more about using Swift Concurrency, watch \"Embracing Swift Concurrency\" from WWDC25. Alright Art, one more hang left! Let's bring it home.\n\nNow that the concurrency contention is resolved, let's return to our baseline run for the final hang. When we hit save, the user interface hangs for a short time. I am going to use the Write to File interval in the points of interest track to find this hang.\n\nAnd I am going to set the inspector range and zoom in by clicking on appropriate option in the contextual menu.\n\nThere is a reported micro hang during this step. Again, let's check the CPU usage.\n\nIn this case, it's actually quite low, hovering around 20%.\n\nThe diagnostic points us to low CPU usage. When the interface freezes under these conditions, it tells us the main thread is blocked waiting on a system resource. To understand exactly why that happens, let's look at how threads transition between states. A low CPU utilization can be deceptive, because it doesn't mean your code is executing slowly, it means the thread has stopped running. System Trace template is built to visualize exactly when and why the operating system pauses your application. Here, the main thread is actively running on a CPU core. The operating system provides whole host of system calls to do work on your behalf and control the hardware. But when a resource isn't immediately available a thread enters the blocked state. When this occurs, the kernel evicts the thread from the processor. Only when the resource is finally ready, the thread becomes runnable, and returns to the core.\n\nTo be more specific, when the hardware eventually finishes the job, the thread doesn't immediately start executing. It first enters the runnable state, which means the resource is ready, but the thread has to wait in line for the OS scheduler to assign it a free CPU core.\n\nNow, zooming back. Notice this blocked state highlighted here. It spends the vast majority of its time blocked waiting for that external dependency to resolve. When it is resolved the thread wakes up briefly to coordinate the next stage of the request. These brief moments of execution are exactly what cause that twenty percent of CPU utilization.\n\nDuring this phase, the thread is completely idle. A single system call like this often relies on multiple underlying dependencies, forcing the main thread to wait until the OS resolves every single one of them. It finally returns to the core to finish the work. Optimizing algorithms yields no improvement here, because there is no code running to optimize. Let's find out together what is happening with the Write to File interval.\n\nHere is a profile of this hang using the System Trace template. With System Trace, we can see exactly what the thread was doing, including key OS concepts like system calls. We can pin the main thread using the inspector, and zoom in.\n\nIt reveals that while saving the file, the activity lane shows a large amount of blank space. This indicates that the thread was blocked, preventing the UI from updating. The purple intervals you see indicate that a system call is running. However, just because a syscall is active does not mean the thread is actually executing the application code. I am going to select one of these intervals.\n\nNotice, when I do so, more than just the segment I clicked is highlighted. This visualizes one continuous write system call that spans both on and off-core time. The opaque segments represent the time spent actively running on-core, while the translucent segments are the periods it was blocked off-core.\n\nTo understand why it was blocked for so long, we can look at the Inspector. The Inspector gives us the exact arguments passed to this system call. We can see the target file descriptor, the memory address of the buffer, and most importantly, the size.\n\nIt is trying to write over 1.7 gigabytes of data on the main thread. We can also see the performance cost. This single operation took over 500 milliseconds, and almost 300 of those milliseconds were spent off-core waiting for the disk. Because the file write was initiated synchronously on the main thread, the application freezes waiting for the storage to respond. To fix this, I will refactor the code to move the file I/O in the background. This is the snippet responsible for that workflow. We're using the PropertyListEncoder class to serialize our data, and the bottleneck is this line right here. We are calling the data.write method synchronously. Because this atomic write is executing directly on the main thread, it's exactly what caused that block. We, again, can wrap this code inside a Swift task. By doing this, we push the encoding and file writing onto concurrent thread pool and unblock the Main Actor. Here's another profile to verify that we no longer block the main thread during file saving. I have already navigated myself using the Writing to File signpost interval. The main thread no longer shows the write system call.\n\nWe can now find the same syscall on a background thread.\n\nWhich mean using the Apple Pencil should now feel fluid. Let's open the app and confirm the wins. With the file I/O properly routed, saving the document is now happening in the background, allowing interacting with the application.\n\nAnd because we also optimized the CPU saturation and resource contention, using the lasso tool and scrolling my notes both remain responsive.\n\nBy following the data, we have successfully eliminated all three types of hangs from our baseline run. Consistently engineering responsive applications requires matching your profiling tool to the specific performance symptom.\n\nWhen the CPU is overloaded, utilize Top Functions to isolate scattered software overhead, and use Run Comparison to verify your improvements. When the tasks fight for resources, use the Swift Concurrency instrument to identify actor congestion.\n\nAnd when a thread is idle, leverage System Trace and the Inspector panel to uncover synchronous blocking behaviors like file I/O.\n\nAs you apply these workflows to your own codebases, ensure your profiling is accurate. Always profile a release build. And leverage os_signpost to make sure your intervals for run comparisons are reliable. To go even deeper into these topics, we highly recommend checking out \"Analyze hangs with Instruments\" session from WWDC 2023. With Instruments you never have to guess where to look. Profile often, and let the data tell the story. Thanks for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "5:41",
+ "title": "Add signpost interval around Lasso Selection",
+ "language": "swift",
+ "code": "// Add signpost interval around Lasso Selection\n\nimport os.signpost\n\nlet signposter = OSSignposter(subsystem: “Demo App\", category: .pointsOfInterest)\nvar lassoIntervalState: OSSignpostIntervalState? = nil\n\nfunc lassoSelectionUpdated() {\n lassoIntervalState = signposter.beginInterval(\"Lasso Selection\")\n // Update selection in canvas…\n}\n\nfunc lassoSelectionEnded() {\n // Finalize lasso selection...\n signposter.endInterval(\"Lasso Selection\", lassoIntervalState!)\n}"
+ },
+ {
+ "timestamp": "12:11",
+ "title": "Existentials",
+ "language": "swift",
+ "code": "// Existentials\n\nprotocol Foo { }\n\nstruct TypeA: Foo { }\nstruct TypeB: Foo { }\n\nfunc bar(_ foo: any Foo) {\n\n}"
+ },
+ {
+ "timestamp": "12:39",
+ "title": "Concrete Types",
+ "language": "swift",
+ "code": "// Concrete types\n\nprotocol Foo { }\n\nstruct TypeA: Foo { }\nstruct TypeB: Foo { }\n\nfunc bar(_ a: TypeA) {\n\n}\n\nfunc bar(_ b: TypeB) {\n\n}"
+ },
+ {
+ "timestamp": "12:46",
+ "title": "Concrete Types + Generics",
+ "language": "swift",
+ "code": "// Concrete types\n\nprotocol Foo { }\n\nstruct TypeA: Foo { }\nstruct TypeB: Foo { }\n\nfunc bar(_ a: TypeA) {\n\n}\n\nfunc bar(_ b: TypeB) {\n\n}\n\n// Generics\n\nprotocol Foo { }\n\nstruct TypeA: Foo { }\nstruct TypeB: Foo { }\n\nfunc bar(_ generic: T) {\n\n}"
+ },
+ {
+ "timestamp": "12:49",
+ "title": "Concrete Types + Generics + Enums",
+ "language": "swift",
+ "code": "// Concrete types\n\nprotocol Foo { }\n\nstruct TypeA: Foo { }\nstruct TypeB: Foo { }\n\nfunc bar(_ a: TypeA) {\n\n}\n\nfunc bar(_ b: TypeB) {\n\n}\n\n// Generics\n\nprotocol Foo { }\n\nstruct TypeA: Foo { }\nstruct TypeB: Foo { }\n\nfunc bar(_ generic: T) {\n\n}\n\n// Enums\n\nenum Foo {\n case a(TypeA)\n case b(TypeB)\n}\n\nstruct TypeA { }\nstruct TypeB { }\n\nfunc bar(_ enum: Foo) {\n\n}"
+ },
+ {
+ "timestamp": "18:24",
+ "title": "Thumbnail Rendering",
+ "language": "swift",
+ "code": "// Thumbnail rendering\n\nlet drawingData = note.drawingData\nlet canvasImages = note.decodeCanvas()\nthumbnail = await Task(name: \"Render Thumbnail\") {\n await renderThumbnail(drawingData: drawingData, canvasImages: canvasImages, size: CGSize(width: 300, height: 240))\n}.value"
+ },
+ {
+ "timestamp": "18:29",
+ "title": "Thumbnail Rendering Off Main Actor",
+ "language": "swift",
+ "code": "// Thumbnail rendering off Main Actor\n\nlet drawingData = note.drawingData\nlet canvasImages = note.decodeCanvas()\nthumbnail = await Task(name: \"Render Thumbnail\") { @concurrent in\n await renderThumbnail(drawingData: drawingData, canvasImages: canvasImages, size: CGSize(width: 300, height: 240))\n}.value"
+ },
+ {
+ "timestamp": "24:12",
+ "title": "File Saving",
+ "language": "swift",
+ "code": "// File saving\n\nlet encoder = PropertyListEncoder()\nencoder.outputFormat = .binary\nguard let data = try? encoder.encode(snapshots) else { return }\nlet id = signposter.beginInterval(\"Writing To File\")\ntry? data.write(to: fileURL, options: .atomic)\nsignposter.endInterval(\"Writing To File\", id)"
+ },
+ {
+ "timestamp": "24:25",
+ "title": "File Saving off Main thread",
+ "language": "swift",
+ "code": "// File saving\n\nTask { @concurrent in\n\tlet encoder = PropertyListEncoder()\n\tencoder.outputFormat = .binary\n\tguard let data = try? encoder.encode(snapshots) else { return }\n\tlet id = signposter.beginInterval(\"Writing To File\")\n\ttry? data.write(to: fileURL, options: .atomic)\n\tsignposter.endInterval(\"Writing To File\", id)\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Analyzing CPU profiles with call tree views",
+ "url": "https://developer.apple.com/documentation/Xcode/analyzing-cpu-profiles-with-call-tree-views"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/268/4/7d94575d-e65b-4033-811f-199586ac587a/downloads/wwdc2026-268_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/268/4/7d94575d-e65b-4033-811f-199586ac587a/downloads/wwdc2026-268_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "258",
+ "year": "2026",
+ "title": "What’s new in Xcode 27",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/258"
+ },
+ {
+ "id": "308",
+ "year": "2025",
+ "title": "Optimize CPU performance with Instruments",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/308"
+ },
+ {
+ "id": "10248",
+ "year": "2023",
+ "title": "Analyze hangs with Instruments",
+ "url": "https://developer.apple.com/videos/play/wwdc2023/10248"
+ },
+ {
+ "id": "110352",
+ "year": "2022",
+ "title": "Embrace Swift generics",
+ "url": "https://developer.apple.com/videos/play/wwdc2022/110352"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:17.171Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-269.json b/data/wwdc/videos/2026-269.json
new file mode 100644
index 0000000..941578d
--- /dev/null
+++ b/data/wwdc/videos/2026-269.json
@@ -0,0 +1,256 @@
+{
+ "id": "269",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/269/",
+ "title": "What’s new in SwiftUI",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "Design",
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Steven, and I work on UI Frameworks! My name is Julia, and I'm also a UI Frameworks engineer. We're excited to talk to you about what's new in SwiftUI! SwiftUI has gained some major upgrades. From a refined look and feel, to performance improvements, new ways to interact with your apps, and a powerful new document API, we have a lot of great new things to share.\n\nBut first… I love stickers. I have my laptop covered with them. I love stickers too! But there are only so many places in the real world where we're allowed to stick them. So we came up with a way to unlock unlimited sticker potential: an app! Meet our sticker app! I'll start by picking a photo… I really like this photo of Steven and me at Apple Park! Now… I wish we could bring our pets to work. They would absolutely love it here! So I'm gonna drag this sticker of my dog Pretzel into the scene... and resize her to match the size of her personality. And I'll add my cat, Kishka! She's got an even bigger personality! Now this is what I call an ideal workday! I wish we could do this all day, but there's a lot to cover, so it's important we stick… to the script! Our app takes advantage of lots of new enhancements to SwiftUI that have helped us build a first class user experience that looks great and also has great performance. I'll start by taking you through the beautiful new look apps gain on the 2027 releases, along with ways to optimize toolbar content for resizability. Julia will tell you about new APIs that unlock powerful document features in your apps along with improvements to presentation and interaction. And finally, I'll talk about how to keep your apps running smoothly with enhancements to performance, and data flow. Let's get started with the refreshed look and feel for apps on the 2027 releases. When I build and run our app, the Liquid Glass design automatically takes on its updated appearance. Apps gain this look without having to change a single line of code! Liquid Glass has a refined look and automatically responds to the new Liquid Glass slider to adjust its tint.\n\nOn macOS, like on iOS, you can mark Liquid Glass custom elements as \"interactive\" so they respond more fluidly to user's clicks. And this is optimized to work great with the mouse pointer, so it feels right at home on the Mac.\n\nAnd just like Mac, our iPad app automatically takes on a distinct appearance when inactive, with the icons and text dimming to reinforce which window is active; like here, when I tap to switch between our app and the Files app.\n\nI love how these improvements look in our app. And even more so since the app's look was refreshed without having to make any code changes! But there are also ways to fine tune our app's appearance to get things… just right.\n\nOur custom account button in the sidebar dims along with the rest of the tab labels, using the appearsActive environment value to conditionally reduce the button's opacity when the window isn't active. iPad and Mac Menu Bars now have a minimal set of icons by default, reserving them for key actions. However, I can apply the labelStyle titleAndIcon modifier to our Store menu item to show its icon, so that one will stand out.\n\nAs I accumulate more stickers, I really appreciate the resizability of our app on Mac and iPad. And on iOS 27, our iPhone app becomes resizable too.\n\nIn Xcode 27, Live Previews now have resize handles that allow you to test how your app responds to being interactively resized! This allows you to instantly preview how an app will behave when using iPhone Mirroring, or when running it as an iPhone app on iPad.\n\nThis is already working great, especially since we made sure our app resizes well on iPad and Mac.\n\nApps built with SwiftUI gain a lot of this functionality automatically, but if your app uses both UIKit and SwiftUI, there may be some additional things to consider. Things like how to correctly determine screen geometry, using size classes instead of idiom for sizing your views, and responding to interface orientation changes. To learn more about getting ready for resizability for apps that use both UIKit and SwiftUI, check out \"Modernize your UIKit app\".\n\nOur app also has a full store experience that allows people to download new sticker packs! My personal favorite is the WWDC26 sticker pack! The store view has a few tabs, including one for the shopping cart. The shopping cart tab is displayed on the bottom trailing edge of the screen, distinguishing it from the other tabs, which contain store content. To enable this special tab placement, I'm using the new prominent tab role to make it stand out.\n\nWith all of the features our app has, the toolbar is a great way to provide quick access to the most important actions. As I add more features to the app, the list of toolbar items will probably grow even bigger! And this becomes especially important when resizing the app.\n\nWhen I resize the app window, the toolbar items are automatically adjusted by the system.\n\nAnd some of the items that don't fit, end up becoming hidden! On iPhone, where there's even less horizontal space to work with, there isn't enough room for all my toolbar buttons. Important actions like Undo, Redo, and Share are hidden in the overflow menu! And that's where the new toolbar APIs come in! They allow me to specify which buttons stay visible when toolbar space is limited.\n\nThe most important ToolbarItemGroup contains buttons for editing: Undo and Redo. But they're currently hidden! I want the system to know that it's important to keep them visible. I can do this by adding the new visibilityPriority modifier, and setting the priority to high. Now, the Undo and Redo buttons become visible! Some of the actions I'd prefer to keep in the overflow menu, since they aren't used as often, like the buttons for swapping out the photo, exporting the page as an image, and clearing the stickers. I choose to always place these buttons in the overflow menu by grouping them in the new ToolbarOverflowMenu container. And there they are in the menu, if I need them! Lastly, for my Share button, I want to make sure it's never hidden, so I don't forget to send these sticker pages to everyone I know. I can use the new topBarPinnedTrailing placement to make the Share button always visible in the trailing position.\n\nAnd now, the toolbar is set up perfectly, to access all the app's important features, no matter the window or screen size.\n\nThere's one more toolbar enhancement I want to show you. I have lots of stickers in my collection, and when I'm scrolling through them, I want to have as much space as possible for all of them. So I add the new toolbarMinimizeBehavior modifier, and set it to \"onScrollDown\" for the navigationBar placement. Now, the system automatically moves the navigation bar out of the way when I scroll.\n\nI think the app looks and feels great, but there's a lot more going on under the surface. Our app can open and save sticker pages, and even has support for exporting the pages as images. This is all thanks to some powerful new features Julia will tell you about. The app has all these capabilities, and even more, thanks to the new SwiftUI Document API. I'll start by giving an overview of the new Document APIs, and how they serve as a base for building an app.\n\nFor quite some time now, SwiftUI has had support for document-based apps via the FILE_DOCUMENT and REFERENCE_FILE_DOCUMENT protocols.\n\nIn the 2027 releases, I'm happy to share expanded APIs that build on that foundation.\n\nYou might be familiar with document-based apps like PixelMator PRO, or Pages, or the one we spend our days in: Xcode.\n\nThey get a lot of functionality out of the box, including things like keyboard shortcuts, Command+N for new documents, and Command-O for opening documents, the edited indicator that tells you when a document has changes, a smart autosaving mechanism, and much more. And the Document API unlocks a number of improvements an app can make both under-the-hood, and in the UI. I'll cover three of these, including the document creation context, disk reading and writing performance improvements, and first-class support for direct document URL access.\n\nHere's how people create documents in our app.\n\nBy default, the app lets you start with a blank sticker page. But I want to help people get going more quickly. So there's also a button to create a page from a photo! I use the new DocumentCreationSource API to declare two sources, blank and photo, and add a NewDocumentButton for each one to the launch scene. When choosing one of these buttons, SwiftUI passes the source to my document creation closure via the context parameter. I check the context in my initializer, and if the source is \"photo\", the document opens with the photo picker already presented! Now, I'm only one tap away from putting stickers on my photo.\n\nDocument-based apps read and write a lot of data. They can also have complex UI that needs to update frequently. The new API provides great ways to optimize these operations and keep your app running smoothly.\n\nI opt our app into the document architecture by declaring a DocumentGroup as the first Scene in the app's body. My StickerDocument class describes the document type. It provides data to the views and describes how to read data from disk and write it back. The Document API works in conjunction with the modern Observation framework, so I am using the Observable macro. This alone gives me a performance boost: the views will update only when a property they depend on changes. My goal is to make reading and writing as fast and efficient as possible. Let me take you through the optimization points in the new API.\n\nFor writing, I conform the document to the writable document protocol. It has three requirements. First, a list of formats the app can write.\n\nOur application supports a custom package format with a photo, and stickers inside. Second, the snapshot method that returns the current document content for writing. To represent the content, I use a custom PageSnapshot struct. It contains everything I need for writing — the background image, coordinates for the stickers, and the stickers themselves.\n\nIt acts as a snapshot of the document at a single point in time.\n\nTo satisfy the third requirement, I provide a Writer. The Writer conforms to the DocumentWriter protocol and knows how to write a document to disk in a specified format. I give it the requested content type, which for my app is \"stickerDocument\". The DocumentWriter protocol has a notion of Snapshot. And the PageSnapshot type fits perfectly here! DocumentWriter's only requirement is a method for writing. It offers multiple opportunities to optimize performance. First, the write method is nonisolated and asynchronous.\n\nThis lets me perform expensive disk writing operations in the background, so the app stays responsive.\n\nI write only the parts of the package that actually need updating, by comparing the current and the previous snapshots. Even with all the optimizations, disk operations can take noticeable time, so SwiftUI provides a progress parameter that lets me report writing progress using the Foundation Subprogress API.\n\nTo teach our document type how to read from disk, I conform the StickerDocument class to the ReadableDocument protocol. ReadableDocument is a twin to WritableDocument. Here's how they compare. Each protocol requires a list of supported content types. WritableDocument provides a snapshot and ReadableDocument knows how to apply it.\n\nWritableDocument has a friend protocol: DocumentWriter.\n\nAnd ReadableDocument's friend is DocumentReader, which does all the disk-related heavy lifting.\n\nNow, the Sticker app is ready to read and write files, like this page! I think I make a pretty good pirate! There's one more feature to add: saving pages as images, so I can share them with people who don't have the app.\n\nThe Document API makes it possible to extend my writer to save pages in another format, like PNG, using Core Graphics.\n\nI return to the writable content types definition and add .PNG to the list. Now, to support an additional format, I revisit the write method I implemented earlier.\n\nSince the app can now handle multiple formats, I am adding content type checks for each type the app supports.\n\nFor PNG, I use Core Graphics to flatten the stickers and background photo into a single image and write it to the URL.\n\nTo add even more types, I could write the document in any format, using any framework, just by adding another content type! That's how our app is set up to save documents.\n\nNow, it's time to get to my sticker collection, where there are some great enhancements to presentation and interaction. Sometimes, our massive collection of stickers can feel a little chaotic. And that's where the reorderable container APIs come in! There are two different ways to browse the sticker collection in the app's inspector. The first is a \"List\" that displays each sticker along with its name. I'd like to keep these stickers organized by dragging them to rearrange their order. So I use the Reorderable API.\n\nI add the Reorderable modifier to ForEach and add a reorderContainer modifier to the List.\n\nThen, in the closure, I call this helper function I wrote, difference.apply, to update my array of stickers! Under the hood, my apply function uses the open source swift-collections package to commit the ordering changes.\n\nVisit swift.org for more details.\n\nSwiftUI automatically handles the drag interaction and animation for me! I can also organize my stickers in a grid, which makes it easier to browse more at a time. And the reorderable API works with any container, not just List! This means I can take the code from my list and repurpose it to use a Lazy_V_Grid! The code for reordering stays exactly the same.\n\nI get the same interactive reordering behavior on a completely different container, using the same code! And now these APIs also bring reordering capabilities to watchOS for the first time! When it comes to reordering, that's just the beginning! For a deep dive into all of the features of reorderable containers, check out the code-along session: \"Build powerful drag and drop in SwiftUI\".\n\nWhen I'm on the go, I love to customize my sticker pages on my iPhone! The sheet at the bottom of the UI keeps all my stickers right where I need them.\n\nI've even set up swipe actions for removing stickers from the list, by adding the swipeActions modifier to my list item, along with this Delete button.\n\nBut I want some more flexibility to customize my list, so I've decided to switch to using a Lazy V Stack. And now, SwiftUI supports swipe actions on any view, not just List! I move my ForEach out of the List into a Lazy V Stack with my updated item style and add the swipeActionsContainer modifier, which coordinates the swipe actions across the items in this scroll view.\n\nWell, as I said before, I love stickers! So, it's no surprise that when I start decorating a photo, I'll admit it, I can get a little carried away. And sometimes that means I need to make space by deleting some stickers, even if I love them all.\n\nI've added a context menu to each sticker placed on the photo, giving me quick access to delete it with this Delete button.\n\nTapping the button sets the stickerToDelete State variable to the current sticker.\n\nI've also added a confirmationDialog modifier. Now, confirmation dialogs support the same item-binding pattern that sheets use.\n\nI pass a stickerToDelete binding to the modifier.\n\nWhen I set stickerToDelete to a value by tapping the Delete button, the confirmation dialog appears.\n\nAnd this also works with alert! I've taken you through some great improvements to presentation and interaction, but that's not all! There are even more improvements in SwiftUI in the 2027 releases.\n\nMany of these improvements make the APIs you're already using better without any changes to your apps.\n\nGreat performance is important for making an app feel responsive and polished. Being thoughtful about the way data flows through your app is one of the best ways to keep your app's performance in top shape.\n\nSteven is going to tell you about some big improvements to data flow and performance. Thanks, Julia! The sticker store you added to the app is really cool. I love how the sticker packs are downloadable, because that allows me to take advantage of improvements to AsyncImage! AsyncImage is a great way to load image assets from the Internet as they appear on screen.\n\nThis works great when scrolling to reveal more of these adorable stickers of Kishka and Pretzel! But up until now, AsyncImage hasn't kept images in memory. When scrolling back up, they would reload when reappearing on screen. I'd prefer that these images show immediately when scrolling back to the top. And in the 2027 releases, they do! AsyncImage now supports standard HTTP caching, so images are cached by default, respecting the server's cache headers, without any changes to your code. This is enabled automatically for every app. And apps built with Xcode 27 can take advantage of new APIs to customize how downloads happen.\n\nFor more control over how an image is downloaded I can construct my own URLRequest and pass it to AsyncImage. This allows a wide variety of per-request customizations, like specifying a cache policy, for instance. And if I need a longer-lived configuration, like a bigger cache, let's say, I can instantiate my own custom URLSession and configure a URLCache with whatever capacity I need and then use my session by passing it to the asyncImageURLSession modifier.\n\nNow, when I scroll back to the top, the images are automatically loaded from the cache! SwiftUI provides a variety of ways for apps to model and store their data, and to pass that data to views. One great way to store an app's data is using Observable classes, like I do here with the StickerStore class! When StickerStoreView is initialized, a new instance of my StickerStore class is created and assigned to the state variable. This instance stays around for the lifetime of the view. But what happens when the parent view updates, causing StickerStoreView to be initialized again? In prior releases, a new instance of StickerStore would be created on every initialization. But the original instance is still the one being stored in the State variable. And so the new one was just discarded. This would happen again for every reinitialization of the view even though the main stored instance of the class remained stable.\n\nIn the 2027 releases, for the first time, classes initialized and stored using State properties are now lazy, which means they will only be initialized once. This is thanks to the conversion of State from a Dynamic Property to a macro! Now, when StickerStoreView is initialized for the first time, a new instance of my StickerStore class is created like before, but on future initializations, no new class instances are created.\n\nAnd this behavior has been back ported to the releases where @Observable was first introduced, starting with iOS 17, macOS 14, and aligned releases.\n\nIn some cases, the introduction of the state macro can be a source-breaking change.\n\nFor example, if you specify a default value for your @State variable, and then you assign a value to the same @State variable inside your init, Xcode will show an error about use before initialization. To resolve this error, remove the unnecessary default value assignment. For additional details about how the State macro may impact your code, check out the documentation.\n\nMaintaining good runtime performance is critical for keeping your app working smoothly. But there's another type of performance that can have a significant impact on your app development experience.\n\nIf your app has complex, deeply nested views, you may have encountered this error: \"The compiler is unable to type-check this expression in reasonable time\". But why does this happen? This view has a Section, a Group, and a ForEach wrapping its content. To type check this expression, first the compiler has to select which overload of Section to use. Section can be initialized with a builder that produces either a View, or TableRowContent. To know which one to use, the compiler has to try both options. In my code, the Section builder returns a Group. The compiler can't know what type of content section produces until it figures out the type of the nested group's content. This time, there are even more options, and for the nested ForEach the compiler will have to try each one. And then, ForEach's builder has its own set of options that will also need to be checked.\n\nAnd we haven't even gotten to the content yet! Trying each of these paths makes type checking increasingly expensive. But I already know that Section, Group, and ForEach in my code are building views, so there's really only one valid path through this decision tree.\n\nInstead of this complex set of choices, what if these builders weren't constrained by the types they produce, and instead just assembled their content? In the 2027 releases, that's exactly what SwiftUI starts doing. The most common set of builders now share a single initializer, leaving just one, straightforward path.\n\nThis is possible because multiple different builder types have been unified under a single builder: ContentBuilder! This is a step towards enabling unified builders across all of SwiftUI's APIs. And ContentBuilder can be used with any minimum deployment target, because under the hood, it's an evolution of the existing ViewBuilder.\n\nContentBuilder provides a substantial improvement in type checking performance in SwiftUI when building using Xcode 27; whether you're targeting the 2027 releases, or previous releases as well. We're also excited to introduce new agent skills included with Xcode 27 to help you adopt the new features from the 2027 releases in your apps, and improve your app's performance and code correctness.\n\nThe SwiftUI Specialist Skill can help you follow SwiftUI best practices in your apps. The What's New In SwiftUI Skill can guide you through adopting new APIs from the 2027 releases. Both of these skills can be accessed in the Coding Assistant in Xcode 27. And to use these skills with other tools, you can export them with the \"xcrun agent skills export\" command. This will create markdown files you can import in your workflows. We've covered some exciting new enhancements to SwiftUI. Now, it's your turn! Start by building your project in Xcode 27 for the 2027 releases and then check out your app's updated look and feel! If you have a document-based app, investigate how the new Document APIs can make it even better.\n\nAnd try out the SwiftUI agent skills in Xcode 27 to adopt new APIs and best practices.\n\nWell… as sad as we are to peel ourselves away, we have to adhere to the schedule. We hope you have as much fun adopting these improvements in your apps as we had with ours! Thanks for sticking with us!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:20",
+ "title": "appearsActive environment value",
+ "language": "swift",
+ "code": "struct SidebarFooterView: View {\n @Environment(\\.appearsActive) private var appearsActive\n\n var body: some View {\n MyAccountView()\n .opacity(appearsActive ? 1 : 0.5)\n }\n}"
+ },
+ {
+ "timestamp": "3:34",
+ "title": "Menu icon visibility",
+ "language": "swift",
+ "code": "CommandMenu(\"Stickers\") {\n Button { openStore() } label: {\n Label(\"Store\", systemImage: \"bag.fill\")\n .labelStyle(.titleAndIcon)\n }\n }\n // Other menu items\n}"
+ },
+ {
+ "timestamp": "5:12",
+ "title": "Prominent tab role",
+ "language": "swift",
+ "code": "TabView {\n Tab { EventsTab() }\n Tab { HolidaysTab() }\n Tab { FunTab() }\n\n Tab(role: .prominent) {\n CartTab()\n }\n}"
+ },
+ {
+ "timestamp": "6:15",
+ "title": "Toolbar item visibility and overflow menu",
+ "language": "swift",
+ "code": "// Toolbar item visibility priority\n\nStickerPageView()\n .toolbar {\n ToolbarItemGroup {\n UndoButton()\n RedoButton()\n }\n .visibilityPriority(.high)\n ToolbarOverflowMenu {\n ChoosePhotoButton()\n ExportAsImageButton()\n ClearAllStickersButton()\n }\n ToolbarItem(placement: .topBarPinnedTrailing) {\n ShareButton()\n }\n }"
+ },
+ {
+ "timestamp": "7:37",
+ "title": "Minimize toolbar on scroll with toolbarMinimizeBehavior",
+ "language": "swift",
+ "code": "// Minimize toolbar when scrolling\n\nScrollView {\n StickerListView()\n}\n.toolbarMinimizeBehavior(.onScrollDown, for: .navigationBar)"
+ },
+ {
+ "timestamp": "9:47",
+ "title": "Document creation sources with context parameter",
+ "language": "swift",
+ "code": "// Use the context to create a document\n\n@main\nstruct Stickers: App {\n var body: some Scene {\n DocumentGroupLaunchScene(\"Create a Sticker Page\") {\n NewDocumentButton(\"New Sticker Page\", source: .blank)\n NewDocumentButton(\"Sticker Page from Photo…\", source: .photo)\n }\n \n DocumentGroup { /* ... */ }\n }\n}\n \nextension DocumentCreationSource {\n static let blank = Self(id: \"blank\")\n static let photo = Self(id: \"photo\")\n}"
+ },
+ {
+ "timestamp": "10:01",
+ "title": "Use the context to create a document",
+ "language": "swift",
+ "code": "@main\nstruct Stickers: App {\n var body: some Scene {\n DocumentGroupLaunchScene(\"Create a Sticker Page\") {\n NewDocumentButton(\"New Sticker Page\", source: .blank)\n NewDocumentButton(\"Sticker Page from Photo…\", source: .photo)\n }\n \n DocumentGroup { document in\n StickerPageDocumentView(document)\n } { configuration, context in\n StickerPageDocument(configuration: configuration, context: context)\n }\n }\n}"
+ },
+ {
+ "timestamp": "10:43",
+ "title": "Document app declaration",
+ "language": "swift",
+ "code": "@main\nstruct Stickers: App {\n var body: some Scene {\n DocumentGroup { /* ... */ }\n WindowGroup { /* ... */ }\n }\n}"
+ },
+ {
+ "timestamp": "11:25",
+ "title": "Implement document writing",
+ "language": "swift",
+ "code": "@Observable\nfinal class StickerDocument {\n // ...\n}"
+ },
+ {
+ "timestamp": "11:34",
+ "title": "Implement document writing: list writable formats",
+ "language": "swift",
+ "code": "@Observable\nfinal class StickerDocument {\n \n static let writableDocumentTypes: [UTType] = [.stickerDocument]\n \n // ...\n}\n\nimport UniformTypeIdentifiers\n\nextension UTType {\n static let stickerDocument = UTType(exportedAs: \"stickerdocument\")\n}"
+ },
+ {
+ "timestamp": "11:45",
+ "title": "Implement document writing: provide snapshot",
+ "language": "swift",
+ "code": "@Observable\nfinal class StickerDocument {\n \n static let writableDocumentTypes: [UTType] = [.stickerDocument]\n \n @MainActor\n func snapshot(contentType: UTType) async throws -> sending PageSnapshot { /* ... */ }\n \n // ...\n}"
+ },
+ {
+ "timestamp": "11:54",
+ "title": "Implement document writing: represent the snapshot",
+ "language": "swift",
+ "code": "struct PageSnapshot {\n var background: Image\n var metadata: StickerPlacements\n var stickers: [Image]\n}\n\nstruct StickerPlacements { /* ... */ }"
+ },
+ {
+ "timestamp": "12:13",
+ "title": "Implement document writing: provide a DocumentWriter",
+ "language": "swift",
+ "code": "@Observable\nfinal class StickerDocument {\n \n static let writableDocumentTypes: [UTType] = [.stickerDocument]\n \n @MainActor\n func snapshot(contentType: UTType) async throws -> sending PageSnapshot {\n makeSnapshot()\n }\n \n func writer(configuration: sending WriteConfiguration) -> sending Writer {\n Writer(contentType: configuration.contentType)\n }\n}"
+ },
+ {
+ "timestamp": "12:33",
+ "title": "DocumentWriter: Snapshot",
+ "language": "swift",
+ "code": "struct Writer: DocumentWriter {\n typealias Snapshot = PageSnapshot\n \n // ...\n}"
+ },
+ {
+ "timestamp": "12:36",
+ "title": "DocumentWriter: PageSnapshot as Snapshot",
+ "language": "swift",
+ "code": "struct Writer: DocumentWriter {\n typealias Snapshot = PageSnapshot\n \n let contentType: UTType\n \n // ...\n}"
+ },
+ {
+ "timestamp": "12:42",
+ "title": "DocumentWriter protocol implementation",
+ "language": "swift",
+ "code": "struct Writer: DocumentWriter {\n typealias Snapshot = PageSnapshot\n \n let contentType: UTType\n \n nonisolated func write(\n snapshot: sending PageSnapshot, to destination: URL,\n previous: sending PageSnapshot?, progress: consuming Subprogress\n ) async throws {\n // write .stickerDocument\n }\n}"
+ },
+ {
+ "timestamp": "13:18",
+ "title": "Progress reporting during writing",
+ "language": "swift",
+ "code": "struct Writer: DocumentWriter {\n typealias Snapshot = PageSnapshot\n \n let contentType: UTType\n \n nonisolated func write(\n snapshot: sending PageSnapshot, to destination: URL,\n previous: sending PageSnapshot?, progress: consuming Subprogress\n ) async throws {\n // report progress…\n // write .stickerDocument\n }\n}"
+ },
+ {
+ "timestamp": "13:27",
+ "title": "Implement document reading with ReadableDocument protocol",
+ "language": "swift",
+ "code": "extension StickerDocument: ReadableDocument {\n \n}"
+ },
+ {
+ "timestamp": "14:35",
+ "title": "Add PNG to supported formats list",
+ "language": "swift",
+ "code": "@Observable\nfinal class StickerDocument: WritableDocument {\n \n static let writableContentTypes: [UTType] = [.stickerDocument, .png]\n}"
+ },
+ {
+ "timestamp": "14:48",
+ "title": "Add content type checks",
+ "language": "swift",
+ "code": "struct Writer: DocumentWriter {\n typealias Snapshot = PageSnapshot\n \n let contentType: UTType\n \n nonisolated func write(\n snapshot: sending PageSnapshot, to destination: URL,\n previous: sending PageSnapshot?, progress: consuming Subprogress\n ) async throws {\n if contentType.conforms(to: .stickerDocument) {\n // write .stickerDocument\n } else if contentType.conforms(to: .png)\n \n }\n}"
+ },
+ {
+ "timestamp": "14:56",
+ "title": "Writing multiple formats including PNG",
+ "language": "swift",
+ "code": "struct Writer: DocumentWriter {\n typealias Snapshot = PageSnapshot\n\n let contentType: UTType\n\n nonisolated func write(\n snapshot: sending PageSnapshot, to destination: URL, \n previous: sending PageSnapshot?, progress: consuming Subprogress\n ) async throws {\n if contentType.conforms(to: .stickerDocument) { \n // write .stickerDocument\n } else if contentType.conforms(to: .png) {\n let context = CGContext(/* ... */) \n context.draw(/* ... */)\n }\n }\n}"
+ },
+ {
+ "timestamp": "15:58",
+ "title": "Reorderable list with reorderContainer",
+ "language": "swift",
+ "code": "List {\n ForEach(stickers) { sticker in\n StickerListItemView(sticker: sticker)\n }\n .reorderable()\n}\n.reorderContainer(for: Sticker.self) { difference in\n difference.apply(to: &stickers)\n}"
+ },
+ {
+ "timestamp": "16:14",
+ "title": "Apply changes to a reorderable list's data source",
+ "language": "swift",
+ "code": "import OrderedCollections // from https://github.com/apple/swift-collections\n\nextension ReorderDifference where CollectionID == ReorderableSingleCollectionIdentifier {\n func apply(to values: inout [some Identifiable]) {\n var dictionary = OrderedDictionary(uniqueKeys: values.map { $0.id }, values: values)\n let destinationOffset: Int? = switch destination.position {\n case .before(let destination):\n dictionary.keys.firstIndex(of: destination)\n case .end:\n nil\n }\n dictionary.move(keys: sources, to: destinationOffset ?? values.endIndex)\n values = dictionary.values.elements\n }\n}"
+ },
+ {
+ "timestamp": "16:48",
+ "title": "Reorderable grid with LazyVGrid",
+ "language": "swift",
+ "code": "LazyVGrid {\n ForEach(stickers) { sticker in\n StickerListItemView(sticker: sticker)\n }\n .reorderable()\n}\n.reorderContainer(for: Sticker.self) { difference in\n difference.apply(to: &stickers)\n}"
+ },
+ {
+ "timestamp": "18:12",
+ "title": "Swipe actions on List",
+ "language": "swift",
+ "code": "List {\n ForEach(stickers) { sticker in\n StickerListItemView(sticker: sticker)\n .swipeActions {\n DeleteButton(sticker: sticker)\n }\n }\n}"
+ },
+ {
+ "timestamp": "18:15",
+ "title": "Swipe actions on any view",
+ "language": "swift",
+ "code": "ScrollView {\n LazyVStack {\n ForEach(stickers) { sticker in\n StickerListItemView(sticker: sticker)\n .swipeActions {\n DeleteButton(sticker: sticker)\n }\n }\n }\n}\n.swipeActionsContainer()"
+ },
+ {
+ "timestamp": "18:54",
+ "title": "Confirmation dialog with item binding",
+ "language": "swift",
+ "code": "struct StickerCanvasView: View {\n var stickers: [Sticker]\n @State private var stickerToDelete: Sticker?\n\n var body: some View {\n ZStack {\n ForEach(stickers) { sticker in\n PlacedStickerView(sticker: sticker)\n .contextMenu {\n // ...\n }\n }\n }\n .confirmationDialog(\n \"Delete?\", item: $stickerToDelete\n ) { sticker in\n DeleteStickerButton(sticker)\n } \n }\n}"
+ },
+ {
+ "timestamp": "19:35",
+ "title": "Alert with item binding",
+ "language": "swift",
+ "code": "struct StickerCanvasView: View {\n var stickers: [Sticker]\n @State private var stickerToDelete: Sticker?\n\n var body: some View {\n ZStack {\n ForEach(stickers) { sticker in\n PlacedStickerView(sticker: sticker)\n .contextMenu {\n // ...\n }\n }\n }\n .alert(\n \"Delete?\", item: $stickerToDelete\n ) { sticker in\n DeleteStickerButton(sticker)\n } \n }\n}"
+ },
+ {
+ "timestamp": "21:18",
+ "title": "AsyncImage with URLRequest and custom URLSession",
+ "language": "swift",
+ "code": "@Observable class StickerStore {\n static let imageSession: URLSession = {\n let config = URLSessionConfiguration.default\n config.urlCache = URLCache(\n memoryCapacity: 64 * 1024 * 1024,\n diskCapacity: 256 * 1024 * 1024)\n return URLSession(configuration: config)\n }()\n}\n\nForEach(pets) { pet in\n AsyncImage(request: URLRequest(\n url: pet.imageURL,\n cachePolicy: .returnCacheDataElseLoad)\n )\n}\n.asyncImageURLSession(StickerStore.imageSession)"
+ },
+ {
+ "timestamp": "23:08",
+ "title": "@State converted to macro for lazy initialization",
+ "language": "swift",
+ "code": "@Observable class StickerStore { }\n\nstruct StickerStoreView: View {\n // store is now lazily initialized, only\n // created once for the lifetime of the view\n @State private var store = StickerStore()\n\n var body: some View {\n // ...\n }\n}"
+ },
+ {
+ "timestamp": "23:48",
+ "title": "@State macro init assignment error",
+ "language": "swift",
+ "code": "struct StickerPageView: View {\n @State private var page = StickerPage()\n let title: String\n \n init(title: String) {\n self.page = StickerPage(title: title) // Variable 'self.title' used before being initialized\n self.title = title\n }\n \n var body: some View {\n // ...\n }\n}"
+ },
+ {
+ "timestamp": "24:02",
+ "title": "Fixed @State macro init assignment error",
+ "language": "swift",
+ "code": "struct StickerPageView: View {\n @State private var page: StickerPage // Removed default value to fix error\n let title: String\n \n init(title: String) {\n self.page = StickerPage(title: title)\n self.title = title\n }\n \n var body: some View {\n // ...\n }\n}"
+ },
+ {
+ "timestamp": "26:07",
+ "title": "@ContentBuilder",
+ "language": "swift",
+ "code": "@ContentBuilder\nfunc stickerLibraryView() -> some View {\n // ...\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "TN3211: Resolving SwiftUI source incompatibilities for State and ContentBuilder",
+ "url": "https://developer.apple.com/documentation/Technotes/tn3211-resolving-swiftUI-source-incompatibilities-for-state-and-contentbuilder"
+ },
+ {
+ "title": "State()",
+ "url": "https://developer.apple.com/documentation/SwiftUI/State()"
+ },
+ {
+ "title": "ContentBuilder",
+ "url": "https://developer.apple.com/documentation/SwiftUI/ContentBuilder"
+ },
+ {
+ "title": "Swift Collections on GitHub",
+ "url": "https://github.com/apple/swift-collections"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/269/4/9215cf93-1308-4706-91e8-34d4e40939d1/downloads/wwdc2026-269_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/269/4/9215cf93-1308-4706-91e8-34d4e40939d1/downloads/wwdc2026-269_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "271",
+ "year": "2026",
+ "title": "Code-along: Build powerful drag and drop in SwiftUI",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/271"
+ },
+ {
+ "id": "278",
+ "year": "2026",
+ "title": "Modernize your UIKit app",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/278"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:16.915Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-271.json b/data/wwdc/videos/2026-271.json
new file mode 100644
index 0000000..3c83037
--- /dev/null
+++ b/data/wwdc/videos/2026-271.json
@@ -0,0 +1,91 @@
+{
+ "id": "271",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/271/",
+ "title": "Code-along: Build powerful drag and drop in SwiftUI",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Jack, an engineer on the UI Frameworks team. In this video, I'd like to share with you some of the new drag and drop APIs available in the 2027 releases.\n\nSince iOS 16, SwiftUI has provided drag and drop through the draggable and dropDestination modifiers. The draggable modifier allows people to move content, like photos and text, throughout the system by using a drag gesture. You can conform your data to the Transferable protocol, giving it a transfer representation that allows it to be dragged through the system and accepted by other apps. For example, I can conform my card type to Transferable and then pass an instance of it to the draggable modifier. The dropDestination modifier allows your app and views to accept all kinds of content. You decide what data your view can handle by specifying a Transferable type to accept.\n\nIf I want my view to accept card instances, for example, I can provide the card type to the dropDestination modifier. For more information about how to make your content Transferable, I'd recommend watching the video \"Meet Transferable\" from WWDC 2022. SwiftUI has expanded the capabilities of drag and drop in three major ways. There's a new reordering API, that allows people to rearrange content using drag and drop.\n\nYou can enable people to drag multiple items at a time using the Drag Container APIs. And you can now configure how data is transferred in drags and at dropDestinations. I'll be implementing the drag and drop interactions for a game of Solitaire. The game will look similar to how I've arranged these playing cards. I can move cards between the piles, and I can pull a card from the deck of the remaining cards.\n\nYou don't need to be familiar with Solitaire to understand the API I'll be using. If you'd like to follow along, you can download the sample project. It contains the views and game logic needed to get started. I'll start by adopting the new reorderable API in my app. Just like the cards in my Solitaire game, your app's content likely exists in some order. But people might want to change that order to organize their content in the way that works best for them. When I make the cards in the Solitaire game reorderable, I'll be able to drag them individually. When I drag a view, it'll be lifted from its position in the view hierarchy and an empty placeholder will take its spot. From there, I can drag this card throughout my app. As I move the card over other cards, they make space for me to drop the one that I'm dragging.\n\nThe placeholder updates to reflect where the dragged card will go once I drop it. And when I drop the card, it will be moved to its new position. I'll add this capability to my Solitaire game. I'll first implement reordering without the game rules in place. Then, once everything's working, I'll refine the implementation to include the rules. I've opened the Xcode project for this app. Its contents are split into two main folders: Game and Views. The Game folder contains the SwiftData models and code for updating them. I'm going to be spending most of my time in the Views folder, where the SwiftUI is.\n\nI'll open to GameView, which contains the layout and views of the playing area. But before I add reordering to my app, I'm going to test it out in a Preview. At the bottom of this file, I have one that shows four cards on a green background. To enable reordering, I add the reorderable modifier to the ForEach that creates the cards.\n\nThen, I put a reorderContainer modifier on the HStack.\n\nI specify that the item type of the container is CardValue, which matches the type used in my ForEach view. In the closure, I'm provided a difference at the end of an operation, and I handle it by updating my cards array.\n\nNow, I can interact with the Preview and reorder cards. I can drag the ace of clubs from the left and drop it at the end. In Solitaire, my goal is to organize cards by moving them between the piles. So in my app, I want all of the piles to be in the same reorderContainer.\n\nI can accomplish this by adding the reorderContainer modifier to the HStack that contains the piles.\n\nI provide the same CardValue type for the container's item type. Because there are multiple piles, I need a way to uniquely refer to each of them. I use the Card.Group type to identify each pile, and, in the closure, I handle a difference across multiple piles.\n\nLastly, I need to go to PileView and add the reorderable modifier. Because I have multiple reorderable modifiers in the same container, I need to provide a unique identifier for each one. Now, I can drag the four of diamonds and drop it onto the pile with the five of spades.\n\nThis is great, but there's one more refinement I'd like to make right now. In Solitaire, you can't reorder the face down cards. But right now, I'm able to lift one from a pile. This is because I made the entire array of cards in PileView reorderable. While I could use some advanced drag and drop API to control this, there's an even easier way to solve this.\n\nI can add a second ForEach view without the reorderable modifier and slice the cards array between the two.\n\nThe first ForEach view contains all of the face down cards.\n\nThe second, with the reorderable modifier, contains all of the face up cards. With this change, I can still reorder the face up cards, but when I drag on a face down card, nothing happens.\n\nNow, I have the basic interactions of Solitaire in play.\n\nBecause of the reorderable modifier, I can now drag individual cards around to reorder them. The reorderContainer modifier allowed me to scope reordering to include all of the piles. And by moving the face down cards into a separate ForEach, I was able to keep them from being reordered. These APIs are newly available on all Apple platforms that support drag and drop.\n\nBut my Solitaire game is still missing a critical part of the gameplay, being able to move multiple cards at once. In Solitaire, when I drag a card in the middle of a pile, it brings the cards stacked on it. But in your app, the interaction model might not be as straightforward. One way to handle this is to add selection to your draggable items. In this example, I'll use a simple tap-to-select interaction model. As I tap each card, it's added to the selection.\n\nOnce I perform a drag gesture on one of the selected cards, all three cards lift together.\n\nI'll add the ability to drag multiple cards at once to my Solitaire game. I'll open back up to GameView, where I added the reorderContainer modifier.\n\nReorder containers implicitly provide their own dragContainer and dropDestination capabilities, but I can add my own to customize this behavior. I declare the dragContainer modifier below the reorderContainer modifier.\n\nFor them to work together, I need to make sure that they use the same type, CardValue.\n\nIn the closure, I'm given an item identifier, and I need to provide transferable data for the items that I want to move.\n\nI call out to my game logic to find the cards stacked above the one I'm trying to drag.\n\nWhen I drag this four of clubs, which is below a three of diamonds, I now get both cards in the same drag.\n\nWhen I dragged the stack of cards together, they collapsed into a pile, with the first card on top. This is the default preview, but I can configure it to several options, including pile, list, and stack. I can configure this with the dragPreviewsFormation modifier.\n\nI choose stack because it's compact and has the feel of stacked cards. When I drag the four of clubs now, the cards form into a neat stack. But when I drag over the piles, they revert back to the default appearance. Because they are above my reorderContainer's dropDestination, they're using its drop formation. To configure that, I can use the dropPreviewsFormation modifier. Because I want this to be consistent across all dropDestinations in my app, I declare this modifier on the root layout of my GameView. Now, the cards I drag maintain the same appearance throughout the playing area.\n\nI made it possible to drag multiple cards at a time in my Solitaire game. I used the Drag Container API to make it possible to lift more than one at a time. I added dragPreviewsFormation to customize the appearance of the lifted cards, and I ensured a consistent appearance by setting the same value for dropPreviewsFormation.\n\nThe dragContainer modifier is newly available on iOS, iPadOS, and visionOS 27. All three modifiers are available on macOS 26 and newer.\n\nTo implement the remaining parts of the game, I'll use the new Drag Configuration API. When adding a drag capability to my card view, I should think about how I want that card's value to move. In my Solitaire game, I'll be dragging new cards into the piles.\n\nBy default, SwiftUI will suggest that the data moves by copy. This works well when you're moving data between apps or inserting something new. But in my card game, dragging a card should not create a copy at the destination. Instead, I'd like the card to be moved from the deck into the piles. Unlike my reorderContainer, which is designed to handle moves, these Views are a separate drag source and dropDestination. I'll need to use the new Drag Configuration API to achieve this. I'll add the ability for cards to be moved into the piles without duplication by using the Drag Configuration API.\n\nIn the app, I have the deck of remaining cards on the top left of the playing area. I want to be able to drag this six of diamonds onto the pile with the seven of spades.\n\nTo get started, I open Xcode to RemainderView, which contains the views for this part of the game. The current face up card already has its own drag modifiers.\n\nBy default, this view will transfer the value by copy. I add the dragConfiguration modifier to this view and specify that my intent is for this card to be transferred by move.\n\nBut it's the dropDestination that decides how the data is transferred. In this case, my dropDestination is the reorderContainer in GameView. By default, the reorderContainer only accepts moves within the container. If I want to accept new items, I have to provide my own dropDestination modifier. I add one below the dragContainer, and I specify that I want to accept the same card type.\n\nI read the reorderContainer's destination value for the inserted items, and if it exists, I call into game logic to insert the newCards at the destination.\n\nMoves between piles are still handled by the closure on the reorderContainer modifier.\n\nWith the dropDestination in place, I can now configure how it receives items. I add the dropConfiguration modifier, which has the final say about how the data is transferred.\n\nI'm provided session information in the closure, and I can use that to return a dropConfiguration that tells the dropDestination how to accept the card.\n\nI do three things in this closure. First, I determine which pile should receive the cards by checking where the drag is. I create a destination value with that pile's identifier to tell SwiftUI where the cards should go.\n\nSecond, I express my intent to only support move-based transfers.\n\nNormally, you'd want to support copying as a fallback when move isn't available, but in a card game, copying doesn't make sense.\n\nAnd third, I validate that this destination value is allowed within the rules of the game. If I return a forbidden operation, SwiftUI will prevent the dropDestination from receiving the cards.\n\nWith all of those changes, now I'm ready to drag a card into play. I can drag this six of diamonds from the remainder deck onto the pile with the seven of spades, only because the rules allow me to. And when I do, the six of diamonds is moved from the remainder's deck to the pile. If I try to drop this queen of diamonds on the pile that has the seven of diamonds, the queen will return to the remainder deck because the move is not valid.\n\nI was able to drag cards into piles using the new configuration modifiers. I started by adding a dragConfiguration on my source card, where I specified my intent to transfer the card by move. Then, I used dropDestination to enable the reorderContainer to accept new cards.\n\nAnd I applied dropConfiguration to that destination so that it would take cards by move when the play was allowed.\n\nI started building this app with only a reorderable and reorderContainer modifier. But I was able to fully customize reordering by composing drag and drop modifiers on top of them. Now, I have a fully complete game of Solitaire. Even if you're not building Solitaire, you can use these APIs to make your apps better. Consider giving people the ability to reorder the content in your app. Remember that you can also give them the ability to drag multiple items at a time with the Drag Container API. And fine-tuning your app with drag and drop configurations will make it a delight to use. Now, if you'll excuse me, I have to get back to work! Thanks for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:40",
+ "title": "Add reorderable to the preview",
+ "language": "swift",
+ "code": "#Preview {\n @Previewable @State var cards = [\n CardValue(rank: .ace, suit: .clubs),\n CardValue(rank: .ace, suit: .diamonds),\n CardValue(rank: .ace, suit: .hearts),\n CardValue(rank: .ace, suit: .spades)\n ]\n\n HStack {\n ForEach(cards) { card in\n CardFaceView(card: card)\n }\n .reorderable()\n }\n .frame(maxWidth: .infinity, maxHeight: .infinity)\n .reorderContainer(for: CardValue.self) { difference in\n cards.apply(difference: difference)\n }\n .padding()\n .background(.green.gradient)\n}"
+ },
+ {
+ "timestamp": "4:40",
+ "title": "Add reorder container to the GameView",
+ "language": "swift",
+ "code": "struct GameView: View {\n var game: Game\n\n var body: some View {\n GeometryReader { proxy in\n let spacing: CGFloat = 10\n let cardWidth = (proxy.size.width - 6 * spacing) / 7\n VStack {\n HStack(alignment: .top, spacing: spacing) {\n Group {\n RemainderView(game: game)\n CardBackView()\n .hidden()\n ForEach(CardValue.Suit.allCases) { suit in\n DestinationView(game: game, suit: suit)\n }\n }\n .frame(width: cardWidth)\n }\n .padding(.bottom, 20)\n HStack(alignment: .top, spacing: spacing) {\n ForEach(0..<7) { index in\n PileView(game: game, index: index)\n .frame(width: cardWidth)\n }\n }\n .frame(maxHeight: .infinity, alignment: .top)\n \t// Add the reorder container modifier.\n .reorderContainer(for: CardValue.self, in: Card.Group.self) { difference in\n game.moveCards(difference: difference)\n }\n }\n }\n .padding()\n }\n}"
+ },
+ {
+ "timestamp": "5:58",
+ "title": "Add reorderable to PileView",
+ "language": "swift",
+ "code": "struct PileView: View {\n var game: Game\n var index: Int\n @Query var cards: [Card]\n\n var body: some View {\n ZStack(alignment: .topLeading) {\n CardPlaceholderView()\n PileLayout {\n let index = firstFaceUpIndex\n \t// Iterates over the face down cards.\n ForEach(cards[..\n .Destination(position: .end, collectionID: .pile(pile))\n // Check if the move is allowed.\n let allowed = session.suggestedOperations.contains(.move)\n && game.validateMove(session: session, destination: destination)\n let operation: DropOperation = allowed ? .move : .forbidden\n return DropConfiguration(operation: operation, destination: destination)\n }\n }\n .dropPreviewsFormation(.stack)\n }\n .padding()\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Making a card game with drag, drop, and reordering in SwiftUI",
+ "url": "https://developer.apple.com/documentation/SwiftUI/Making-a-card-game-with-drag-drop-and-reordering-in-swiftui"
+ },
+ {
+ "title": "Drag and drop",
+ "url": "https://developer.apple.com/documentation/UIKit/drag-and-drop"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/271/5/07f08d32-e28e-476f-8ebe-a3600b2e917c/downloads/wwdc2026-271_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/271/5/07f08d32-e28e-476f-8ebe-a3600b2e917c/downloads/wwdc2026-271_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "10062",
+ "year": "2022",
+ "title": "Meet Transferable",
+ "url": "https://developer.apple.com/videos/play/wwdc2022/10062"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:17.227Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-272.json b/data/wwdc/videos/2026-272.json
new file mode 100644
index 0000000..82db4ac
--- /dev/null
+++ b/data/wwdc/videos/2026-272.json
@@ -0,0 +1,109 @@
+{
+ "id": "272",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/272/",
+ "title": "Use SwiftUI with AppKit and UIKit",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "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.\n\nThis app can control lights, like this addressable ring lamp on my desk.\n\nIt has controls to change the color, and run animations.\n\nI'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.\n\nA 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.\n\nI 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.\n\nThe 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.\n\nThis 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.\n\nUIKit 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.\n\nAnd change the hue to red.\n\nGreat, all sliders update and the new color is sent to the light over the network.\n\nAdopting @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.\n\nHue starts with red, progresses through all the colors, and returns back to red. I would like to represent this as a circular slider.\n\nI 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.\n\nIf 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.\n\nBecause 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.\n\nI 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.\n\nhandleNSGestureRecognizerAction 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.\n\nThe 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%.\n\nWrapping the modification in withAnimation makes SwiftUI animate the change.\n\nTo give quick access, I am adding a keyboardShortcut. I have also added a Picker with the paletteStyle, to precisely select common colors.\n\nI now need to add this SwiftUI View to the main menu.\n\nI 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.\n\nAnd circle through all the hues to green.\n\nI will press the Keyboard shortcut to decrease the brightness a couple times.\n\nAnd then use the menu item to turn it off completely.\n\nWhen I force click, my NSGestureRecognizer resets the brightness.\n\nI incrementally added this custom SwiftUI control to my app.\n\nThe 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.\n\nA good place to add a scene is applicationWillFinishLaunching in your NSApplicationDelegate. Call addSceneRepresentation with your scenes, and SwiftUI will do the rest.\n\nIf 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.\n\nIt can be used from an @IBAction to open the settings window programmatically.\n\nI'm opening the settings from the apps main menu.\n\nAnd enable the menu bar extra item.\n\nLet me quickly open the color picker.\n\nAnd turn the light on one last time.\n\nTo 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.\n\nAll 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.\n\nAdd 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!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:39",
+ "title": "Observation in AppKit",
+ "language": "swift",
+ "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}"
+ },
+ {
+ "timestamp": "6:28",
+ "title": "Circular color picker",
+ "language": "swift",
+ "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}"
+ },
+ {
+ "timestamp": "7:21",
+ "title": "Hosting SwiftUI in AppKit",
+ "language": "swift",
+ "code": "// Hosting SwiftUI in AppKit\n\nNSHostingView(\n rootView: HSBColorPicker(model: model)\n)"
+ },
+ {
+ "timestamp": "8:14",
+ "title": "Mix NSGestureRecognizer with SwiftUI",
+ "language": "swift",
+ "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}"
+ },
+ {
+ "timestamp": "9:42",
+ "title": "Adding ColorMenu to the Main Menu",
+ "language": "swift",
+ "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}"
+ },
+ {
+ "timestamp": "11:36",
+ "title": "Adding SwiftUI scenes dynamically",
+ "language": "swift",
+ "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": {
+ "resourceLinks": [
+ {
+ "title": "Updating views automatically with observation tracking",
+ "url": "https://developer.apple.com/documentation/UIKit/updating-views-automatically-with-observation-tracking"
+ },
+ {
+ "title": "Updating views automatically with observation tracking",
+ "url": "https://developer.apple.com/documentation/AppKit/updating-views-automatically-with-observation-tracking"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/272/5/e1e4aa9a-cbe2-4f83-9cea-3dcaae19afd6/downloads/wwdc2026-272_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/272/5/e1e4aa9a-cbe2-4f83-9cea-3dcaae19afd6/downloads/wwdc2026-272_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "322",
+ "year": "2026",
+ "title": "Compose advanced graphics effects with SwiftUI",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/322"
+ },
+ {
+ "id": "243",
+ "year": "2025",
+ "title": "What’s new in UIKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/243"
+ },
+ {
+ "id": "10061",
+ "year": "2022",
+ "title": "Bring multiple windows to your SwiftUI app",
+ "url": "https://developer.apple.com/videos/play/wwdc2022/10061"
+ },
+ {
+ "id": "10075",
+ "year": "2022",
+ "title": "Use SwiftUI with AppKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2022/10075"
+ },
+ {
+ "id": "10072",
+ "year": "2022",
+ "title": "Use SwiftUI with UIKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2022/10072"
+ },
+ {
+ "id": "10021",
+ "year": "2021",
+ "title": "Add rich graphics to your SwiftUI app",
+ "url": "https://developer.apple.com/videos/play/wwdc2021/10021"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:17.105Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-274.json b/data/wwdc/videos/2026-274.json
new file mode 100644
index 0000000..ac21bb8
--- /dev/null
+++ b/data/wwdc/videos/2026-274.json
@@ -0,0 +1,75 @@
+{
+ "id": "274",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/274/",
+ "title": "What’s new in SwiftData",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "Swift",
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi I'm Thomas, an Engineer on the SwiftData team. In this session, I'm going to show you some of the new SwiftData features in Apple's 2027 releases.\n\nIf you're just starting on your SwiftData journey, make sure to watch the code-along \"Add persistence with SwiftData\" and learn how to adopt SwiftData in a new app.\n\nApple's 2027 releases bring exciting new features to SwiftData. First, I'm going to show how to use Query to fetch your data in sections in SwiftUI views. Next, I'm going to cover enhancements on how you can store custom types in your model.\n\nAnd finally, I'll cover some great new APIs for observing model and history changes in your SwiftData store. Let's have a look at sectioning.\n\nOver the last years, we've been working on an App \"SampleTrips\" to let me track all the adventures I have planned.\n\nSampleTrips uses SwiftData for Persistence and SwiftUI for the User Interface. SwiftUI and SwiftData work great together.\n\nReading from SwiftData in SwiftUI is as easy as adding a Query to the view. Here Query fetches all trips from SwiftData, sorted by the start date.\n\nThe results of this query can then be used in the view body to create a list of trips.\n\nIn SampleTrips I want to add the option to group trips by destination. So when I have several trips planned to the same place, I can see them next to each other.\n\nIn Apple's 2027 releases, Query adds support for sectioning.\n\nTo create a sectioned query, I can pass a KeyPath that starts from the root of the Trip model and leads to a string to the new sectionBy parameter of Query. Here I'm using a KeyPath to the Trips destination property to create a section for each destination.\n\nThe wrapped value of Query is still an array of Trip. So right now the list looks like before. To section the list, I'll wrap the current ForEach in a section and add a second ForEach.\n\nTo get to the sections, I access the query from the property wrapper, using the underscore-prefixed name \"_trips\". The query has a sections property that returns a list of sections. Then I can use SwiftUI's ForEach to iterate through all sections.\n\nEach section has an ID property. The ID is the value of my model from the KeyPath I passed to the SectionBy parameter of Query.\n\nSo here in my TripListView the ID will be the destination of all trips in the section.\n\nI use ID as the Section's header label.\n\nThe section itself is a collection of trips - so I change the inner ForEach to iterate though them and create a TripListItem for each trip in the section.\n\nNow, that I can see all my trips grouped by their destination, let's move to the next section.\n\nAnd have a closer look at some enhancements on storing custom types in your model.\n\nAt the center of the SampleTrips app is the Trip model. It stores information like name, destination, and the start and end dates. Right now the destination is stored as a String in the Trip Model. I want add the ability to find locations from MapKit. I've already built a picker in the App that lets me search for destinations using MapKits search API.\n\nFor each location, the MapKit API provides a lot of metadata, such as the name, geographic coordinates and a MKMapItem.Identifier. This MKMapItem.Identifier uniquely identifies the location in MapKit and can be used to look up more metadata about the location and open the location in the Maps app.\n\nI want to store the coordinates and the MKMapItem.Identifier in the trip model. So I'm adding them to the Trip class.\n\nBut now, when I launch the app, SwiftData raises a Fatal Error - \"Class property within Persisted Struct/Enum is not supported\" - pointing at MKMapItem.Identifier as the cause. When SwiftData loads, it automatically generates a schema for the models. The schema defines a mapping between the Model classes and the entities and properties a DataStore can persist. For most types this works automatically. But classes that are not annotated with the Model macro can't be automatically inspected and SwiftData fails to generate a schema.\n\nSince MKMapItem.Identifier comes from MapKit, I don't have access to its implementation. I can't modify it and mark it as @Model, and SwiftData can't inspect it because it's a class. But it does conform to Codable. Codable types know how to serialize themselves into data and instantiate themselves back from it.\n\nIn Apple's 2027 releases, you can mark a model attribute as .codable to tell SwiftData to delegate serialization to the type and store the serialized representation.\n\nLet's mark the mapItemIdentifier attribute as codable. Now, SampleTrips launches without any error. Great.\n\nThe codable attribute tells SwiftData to persist the encoded representation instead of inferring a schema for the type. Like transformable attributes, codable comes with a few things to keep in mind. The contents of codable attributes are opaque to SwiftData. This means they can't be used in Predicates to filter results or for sorting - using Sort Descriptors. Also, if the shape of the codable type changes, like adding or removing properties, they will not trigger a migration. The Codable implementation of the type must be able to encode and decode in a forward- and backward-compatible way.\n\nUsing Codable attributes can be thought of as an \"escape hatch\" to persist types that SwiftData does not support natively.\n\nYou should avoid using codable for Types that you define. Modeling them as SwiftData models or supported values types, allows you to harness the full power of SwiftData, like Sorting, Filtering, and Indexing. But for types that you don't own, codable lets you store them alongside your other attributes. Now I can pick locations from MapKit in SampleTrips and persist the destination's MKMapItem.Identifier alongside the other properties in the trip model. Now, let's look at how you can monitor your store and be notified when data changes.\n\nEarlier we used the query macro in the TripListView to make trips from the SwiftData store available to SwiftUI.\n\n@Query is a powerful tool. When the view appears, it fetches models from the SwiftData store so they can be used in the view body.\n\nThen, it continuously monitors the store for changes that would affect its results. For example, when a trip is removed from the store, Query will notice the change and the view re-renders to reflect the new results of the query.\n\nQuery is great and should be your first choice in a SwiftUI view. But what about parts of your app that are not SwiftUI? Maybe you have a state object that derives values from your SwiftData store and needs to recompute when the data changes. Or your app doesn't use SwiftUI at all like a game written in SceneKit? In Apple's 2027 releases, we're introducing ResultsObserver.\n\nLike Query in SwiftUI views, ResultsObserver fetches data from your SwiftData store and then observes your store for changes. But it works anywhere in your app - independent of SwiftUI views - using Swift Observation.\n\nIt supports the same query primitives that you already know - filtering, sorting, and the sectioning we covered earlier.\n\nHere's the data flow diagram I shared earlier, but with the Query replaced with ResultsObserver and the View replaced with any code you like. The ResultsObserver fetches data and observes your store for changes. Your code can use Swift Observation to react to these changes.\n\nEarlier I've added location information in the SampleTrips app. I want to add a map to see all the places I have trips planned to. I am using SwiftUIs MapKit integration. But I want to customize what area of the map is shown. To do this, I've added a new MapCameraController, which calculates a fitting MapCameraBounds for the map. It uses ResultsObserver to know when to recalculate the MapCameraBounds.\n\nTo do this, I create a ResultsObserver for trips. For the map, I always want to show all trips, so I'm not going to pass in a predicate to filter or a sectioning key path.\n\nThen, I'm using withContinuousObservation with the didSet option to get a callback every time a trip changes.\n\nAnd when the results are changing, I'm re-calculating the bounds of the map.\n\nwithContinuousObservation returns an ObservationTracking token. This token defines the lifetime of the observation. I'll store this token on my class to receive updates for the entire lifetime of my MapCameraController.\n\nHere it is in action. When I launch SampleTrips, MapCameraController picked the map camera bounds that fit all my trips into the view.\n\nUnfortunately, I won't be able to visit Toronto later this month, so I'm going to delete this trip.\n\nThe ResultsObserver will notice the change and the MapCameraController will recalculate the camera bounds of the map.\n\nResultsObserver is a powerful tool if your code needs to react to changes in your data store - anywhere in your app.\n\nBut we didn't stop there. In Apple's 2027 releases, we're also supporting observing history.\n\nFirst, a quick history lesson. When data in your store changes, SwiftData keeps a record of everything that changed. You can use this history to build features like syncing with external servers, or reacting to changes made outside of your app, like in an app extension.\n\nEvery time your data store is saved, SwiftData records a history transaction. The history transaction contains information about what changed, and where the change was coming from - and a token that uniquely identifies the transaction in the history. This token can be used with the ModelContext.fetchHistory API to fetch newer transactions. To learn more about persistent history, watch \"Track Model Changes with SwiftData History\" from WWDC 2024.\n\nNew in Apple's 2027 releases is HistoryObserver. HistoryObserver makes it easy to react to any changes in your store. Similar to ResultsObserver observing fetch results, HistoryObserver can observe the persistent history and let your code react when new transactions are added.\n\nAnd if you only need to know about certain kinds of changes, HistoryObserver lets you filter by model type and transaction author. Observing history is useful when your app needs to keep parts of the data store in sync with other systems, like an external server.\n\nHistoryObserver has a single observable property - eventCounter. When new transactions are available in the persistent history, the eventCounter increments.\n\nYour code can observe the eventCounter and when it increments, use ModelContext.fetchHistory API to fetch the latest changes.\n\nHere's an example of how you could use HistoryObserver to synchronize changes, made in the app with an external server.\n\nFirst, I set up a HistoryObserver for my modelContainer.\n\nIn this case, I'm only interested in changes made by the app so I'm passing \"App\" as authors. So that I'm not replaying changes that came from the server back to the server.\n\nNext, I'm using withContinuousObservation. Like the MapCameraController, I store the observation tracking token on the class so that tracking remains active for the lifetime of my class. Within the observation closure I need to access eventCounter so that Swift Observation knows what to track.\n\nAnd finally, I call my processChanges function. In processChanges I can use the ModelContext.fetchHistory API to fetch history and upload changes to my server.\n\nI hope you are as excited as I am about the new features in SwiftData in our 2027 releases.\n\nTailor SwiftData fetches with sections. Use codable attributes when storing types from other frameworks. Use ResultsObserver to react to changes of query results outside of SwiftUI views. And finally, use HistoryObserver to react to changes in SwiftData's persistent history.\n\nThanks for watching.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "0:01",
+ "title": "Sectioned fetching",
+ "language": "swift",
+ "code": "// Sectioned fetching\n\nstruct TripListView: View { \n @Query(sort: \\Trip.startDate,\n sectionBy: \\.destination)\n var trips: [Trip]\n\n var body: some View: {\n List(selection: $selection) {\n ForEach(_trips.sections) {section in\n Section(section.id) {\n ForEach(trips) {trip in\n TripListItem(trip: trip)\n }\n }\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "4:59",
+ "title": "Using Codable",
+ "language": "swift",
+ "code": "// Using Codable\n\nimport SwiftData\n\n@Model class Trip {\n\n struct Location: Codable {\n var latitude: Double\n var longitude: Double\n }\n\n var name: String\n var destination: String\n\n var startDate: Date\n var endDate: Date\n\n var location: Location?\n @Attribute(.codable) var mapItemIdentifier: MKMapItem.Identifier?\n}"
+ },
+ {
+ "timestamp": "8:20",
+ "title": "// Use observation to update map bounds",
+ "language": "swift",
+ "code": "// Use observation to update map bounds\n\n@Observable @MainActor final class MapCameraController {\n private let resultsObserver: ResultsObserver\n var bounds: MapCameraBounds?\n private var token: ObservationTracking.Token?\n\n init(modelContext: ModelContext) throws {\n resultsObserver = try ResultsObserver(modelContext: modelContext)\n\n token = withContinuousObservation(options: [.didSet]) {[weak self], event in\n self?.bounds = self?.calculateBounds(trips: resultsObserver.results)\n }\n }\n\n private func calculateBounds(trips: [Trip]) -> MapCameraBounds? { / * */ }\n}"
+ },
+ {
+ "timestamp": "8:21",
+ "title": "// Using HistoryObserver to sync with a server",
+ "language": "swift",
+ "code": "// Using HistoryObserver to sync with a server\n\n@SyncActor final class ServerSync {\n private let observer: HistoryObserver\n private var token: ObservationTracking.Token?\n\n func start() throws {\n self.observer = try HistoryObserver(authors: [\"App\"], modelContainer: modelContainer)\n token = withContinuousObservation(options: .didSet) {[weak self] _ in\n _ = self?.observer.eventCounter\n self?.processChanges()\n }\n }\n\n private func processChanges() {\n // Fetch and process history transactions\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "SwiftData",
+ "url": "https://developer.apple.com/documentation/SwiftData"
+ },
+ {
+ "title": "Adopting SwiftData for a Core Data app",
+ "url": "https://developer.apple.com/documentation/CoreData/adopting-swiftdata-for-a-core-data-app"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/274/4/87fb1efb-9956-414e-8c99-f2579fe86da2/downloads/wwdc2026-274_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/274/4/87fb1efb-9956-414e-8c99-f2579fe86da2/downloads/wwdc2026-274_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "275",
+ "year": "2026",
+ "title": "Code-along: Add persistence with SwiftData",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/275"
+ },
+ {
+ "id": "10075",
+ "year": "2024",
+ "title": "Track model changes with SwiftData history",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10075"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:17.381Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-275.json b/data/wwdc/videos/2026-275.json
new file mode 100644
index 0000000..02df84b
--- /dev/null
+++ b/data/wwdc/videos/2026-275.json
@@ -0,0 +1,111 @@
+{
+ "id": "275",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/275/",
+ "title": "Code-along: Add persistence with SwiftData",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI",
+ "Swift",
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hello, I'm Matthew Turk, an engineer on the SwiftData team. Today, I'd like to show you how to take an existing SwiftUI app's dynamic data and connect it to a modern persistence layer that works across all of Apple's platforms using the power of SwiftData. We'll start with the source code of a list-based app called Wishlist. Wishlist helps me organize travel plans on my phone by recording my ideas and grouping trips into seasonal collections. Feel free to download the sample app from developer.apple.com to follow along! In this video, we'll step through the project files to identify data types and variables to use for our SwiftData models and schemas, how those models should relate to one another in a database and how to update our SwiftUI views to present the new models with an eye toward performance, interoperability, and extensibility for more involved use cases.\n\nLet's preview what that data flow is like in action.\n\nHere's the Wishlist tab, with recent trips at the top, and several themed lists of more trips scrolling down.\n\nIn the Goals tab, I can see badges for various goals that I'd like to complete on those trips.\n\nAnd in the Search tab, I can filter through my trips and activities. Let's search for coastal trails. Top result.\n\nThis trip has five remaining activities. I already drove to Point Reyes once before and saw 11 elk there, so I'll check this one off.\n\nAnd back on the Wishlist tab, I'll tap the plus button to add a new trip. I'd like to see another aurora sometime. I'll call the trip \"Northern Lights,\" and choose a photo to go with it.\n\nPress Done, and that's my new trip.\n\nThe data flow you just saw is possible because views in Wishlist pull in a DataSource variable through the SwiftUI environment. The DataSource class manages and provides all of the preinstalled trip data. All trips, goals, and search results are filtered and sorted in memory, on demand. For a small example, this works fine. But in practice, optimizations will be necessary to keep the front-facing parts of the app lean. Additionally, Wishlist is relying on RAM while processing this data and for storing it, which is not a stable place where you can save new trips or activities for later.\n\nIf I close the app and rerun it, everything about my new trip is gone, reset to the preinstalled content.\n\nBut it doesn't have to be this way. This is exactly the kind of problem that SwiftData solves.\n\nSo far, we've identified the relevant state, namely trip collections, goal statuses, and search results. And SwiftData can connect that state to a persistent storage medium through a model context, and that's where we'll be by the end of this video.\n\nOur next move is to find where these data structures live in the code and then refactor them as models with SwiftData schemas. That will set us up to replace the in-memory DataSource with a persistent ModelContext, which will enable us to write efficient database queries to drive the views. Let's take a closer look at one such model, the Activity type.\n\nTo persist Activities, the first step is to import SwiftData and replace this Observable macro with the Model macro. SwiftData automatically generates observable conformance for us. Nice! On a side note, these didSet observers on name and isComplete set the activity's dateEdited value whenever either property changes.\n\nSuppose I have an activity for going paragliding. I've yet to complete it and last edited it on April 1 at 9:41 in the morning.\n\nHere's a timeline of events. Suppose that I then decide to change the activity from paragliding to swimming. The property observer for name fires and updates the dateEdited. Later, I take a trip to the beach and check off the activity. The property observer for isComplete fires and updates the dateEdited. Having an automatically updating dateEdited property is a great way to sort or filter in a list. However, property observers and computed properties are not always compatible, so I'll use a different technique to keep dateEdited up to date. But before I do that, I'll get the current project building again. I'll come back to this diagram. For now, just remove these didSet blocks and leave dateEdited as is. Okay, the Trip class is next. Same deal, import SwiftData, and swap the macros.\n\nNow let's inspect these build failures to get an idea of what's missing.\n\nThis one says that creationDate has to be mutable. So we'll adjust the declaration to use var instead of let.\n\nNow SwiftData can populate this property at runtime with a value loaded from the database. The next error says the TripCollection property, and all model properties, must be Codable. This requirement exists so that SwiftData can serialize properties into database columns. The trip collection stores the seasonal theme of the trip. It should be included in the schema and persisted. I'll command-click to jump to its declaration. And I'll add that Codable conformance explicitly. Several build failures remain. We'll fix these as we continue to build out the schema. Next, let's look at how goal tracking works in Wishlist. With this Goal type, the change is not as straightforward as converting an Observable to a Model.\n\nGoal is declared as an enumeration. Each goal has properties like its name, its kind—like whether it tracks activities completed or trips completed— and the target number of trips or activities to achieve the goal.\n\nThere are exactly 18 Goals, all defined ahead of time, because an enumeration defines a closed set of values. That's fine for the original interface demo, but what we need for a persistent model is a class. A class can store properties and can be instantiated any number of times.\n\nTo get the interface right, we'll need to rethink the design of Goal, so that it harmonizes with the logic of how Goals are processed and presented.\n\nAt a high level, each of these goals corresponds to a badge that will be displayed in Wishlist depending on whether certain criteria are met. To faithfully persist the statuses of these goals, we need some way to capture and store a minimal representation of whether a particular goal's criteria have been met.\n\nIn SwiftData, a class is the substrate we choose to express that minimal representation. I'll convert Goal into a class starting with the same three properties: name, kind, and target count.\n\nIn the original Wishlist app, the number of items completed was stored separately, since enumerations don't have stored properties. Now that we have a persistent class, let's also store the progress value in the goal itself, called completedCount, and a Boolean property called isComplete. We'll store a value of true when completedCount is greater than or equal to target. Because it's a stored property, it'll come in handy for separating completed goals from upcoming goals when we start querying in the view layer.\n\nNext we have to deal with the Kind property. Wishlist uses it to display different details based on whether the goal has to do with completing trips or completing activities. We can break off these finer differences into new subclasses using SwiftData's support for model inheritance. Inheritance is a software design pattern that tends to pay off when you have a well-defined hierarchy of classes, where each subclass represents the same idea as the superclass, but with more specific manifestations of a common set of properties. For example, a spiral galaxy and a lenticular galaxy are subclasses of galaxy, as they inherit several features that you'll find in any kind of galaxy, like stars and dust, while also having categorical differences in visible shape. Similarly, you could have a trip goal and an activity goal, which inherit some common properties from a superclass of goal.\n\nI'll model the different kinds of goals with inheritance by removing the Kind property from Goal. And introducing subclasses for TripGoals and ActivityGoals.\n\nTo learn more about when it makes sense to use model inheritance, check out the \"SwiftData: Dive into inheritance and schema migration\" session from WWDC 2025. We now have everything we need to start defining the relationships between all of these models. In Wishlist, each trip is associated with a set of activities. This is a to-many relationship. Each persistent Trip model has potentially many persistent Activity models.\n\nSeveral parts of Wishlist have been approximating to-many relationships by using dictionaries and functions that loop over arrays. For example, there's one that maps from Trips to Activities based on activity IDs.\n\nIn a moment, I'll convert these dictionaries to proper to-many relationships between types, where each Trip can have zero or more Activities. Declaring an array is the idiomatic way to tell SwiftData that one kind of model may reference another kind of model from the model context as needed. In Trip, I'll declare an array of activities. Here I'm also adding the relationship macro to explicitly mark the activities array of Trip as a relationship so that deleting a trip from the database also clears out the activity models that were in its itinerary. Lastly, this photoURL property needs to be adjusted. It's just a file path right now, which will lose all meaning if the file is renamed or moved to another directory.\n\nAnd, the full-resolution image should only be loaded when a view needs to show it in full, like in TripDetailView.\n\nWhen scrolling through a carousel of trips, we'll just show the thumbnail.\n\nSo add a new property here called thumbnailData. This will cache a low-resolution version of a selected photo and inline its raw bytes in the database. Then separately, instead of the URL, we'll store the full-resolution image using a persistent external file reference. In SwiftData, this is done by creating a new model just for the image. I already added that as the TripImage type. We won't cover those implementation details here, but I encourage you to read more in the sample code later. Since photoURL is no longer a URL, let's rename it to photo by right-clicking on the declaration and selecting Rename in this refactor menu.\n\nI'm using multi-cursor editing, so multiple files will be updated at once. And while I'm at it, I can click this comment to update it with the new variable name. Hit Return. Then refactor the labels of the initializer and adjust the body to set activities directly.\n\nBy setting up this relationship, we've replicated the ability from before for an activity-driven view to reference and display the name, season, and other details of its parent trip. And now that we've really started integrating these SwiftData features, the project has a few surplus files.\n\nThere's no need for TripEditModel anymore since SwiftUI views can bind directly to SwiftData models and propagate edits in real time. All the responsibilities of DataSource are handled automatically by ModelContext, queries, and relationships. Delete.\n\nIt's worth reflecting on that last part. We just removed hundreds of lines of code from the project. A good chunk of state management, storage logic, filtering, sorting, relationship traversal, and search will just work.\n\nOne last touch on the WindowGroup, and the model layer can be done. This modelContainer scene modifier here tells SwiftUI to use our new schema with the query macro.\n\nNext, we'll update the view layer. With the schemas and model container in place, automatic saving is enabled by default. Before seeing that in action, we're going to integrate these persistent models, starting with efficient, targeted queries for each subview that presents models. Then, we'll use SwiftUI view modifiers to capture and surface possible errors that could occur at runtime, like low disk capacity or unsupported predicates. Lastly, we'll add back any missing property observers so that UI events are propagating all of the right data and side effects that we expect. There are two key points to keep in mind when adding filtering to your SwiftData app.\n\nInside your queries, the FetchDescriptor is how you plan which models you want to load and show through your model context or query. And secondly, your models will be saved to a storage medium that exists outside of the address space of your app, such as a database in your local filesystem or a remote server. While this kind of storage is the bedrock of a persistence layer, it can be orders of magnitude slower than reading from memory, so when designing a persistence layer in your app, it's critical to think about which data need to be where and when for the best experience. I'll explain. Before, data about trips, activities, and goals would be in global variables like allGoals. They were loaded as part of the compiled binary for the app. Accessing data this way is fast, but if I hardcode lots of elements into allGoals, for example, the app will have a noticeably elevated memory footprint for its entire lifetime. And if I add a new goal, everything about it disappears when I close the app and relaunch it as you've seen. With SwiftData, we can insert goals or update existing goals and save them with our model context. Once they're saved, there are a few ways we can pull them back into the app.\n\nHere's one way. This code fetches all of the Goals and then discards the irrelevant ones. So it uses less ongoing memory, but it incurs more I/O. This approach is a little like asking a librarian to go grab every book from every shelf at a library, so that you can personally identify the ones by your favorite author. You could have asked that librarian for just the books by that author on their trip through the shelves.\n\nWhen you fetch using a predicate, it's like asking that librarian upfront for what you want. Here, I get only the goals that I asked for.\n\nIn GoalsView, we're going to use the query macro with a predicate. This is equivalent to calling fetch on a model context. The advantage is that the SwiftUI view will update automatically when the query result changes. Let's do that now. Import SwiftData and replace this dataSource environment property with a query for fetching achieved goals, sorted by when they were achieved.\n\nAnd this second query fetches the remaining relevant goals.\n\nSame idea in RecentTripsPageView.\n\nImport SwiftData and replace dataSource with a query.\n\nWe'll ask for trips in reverse chronological order and set a fetch limit of 5.\n\nThat gets us the five most recent trips, which will go right into this ForEach.\n\nIn TripCollectionView, we want to get all the trips and segment them by season. Each season is a trip collection, and there's going to be one instance of this view for each tripCollection. An individual TripCollectionView doesn't know which season it's going to display until it's initialized, so we'll declare the query and then dynamically construct it in the initializer.\n\nHere's the explicit query for all trips matching the desired Collection. Notice that the query receives a predicate, and inside of the predicate, the tripCollection parameter is captured directly from the initializer before heading to the database.\n\nAnd the results of the query will go into this ForEach.\n\nNext we have SearchResultsListView in the third tab in the app. Its super view, which is shown in the preview on the right, owns the search field and text, and passes the value to this view's initializer. So once again, replace dataSource with the query declarations, and then the parameters from the initializer will guide how the predicates are constructed for these queries.\n\nIf the search text is empty, fall back to fetching the three most recent trips. Otherwise, fetch all trips whose name matches the search text, sorted lexicographically. And then we'll do the same for activity search. Check if the text matches the name of any activities belonging to a trip, and again set the property wrapper like so.\n\nLastly, use the queried values in List.\n\nThis list also has an overlay view modifier, which displays a ContentUnavailableView on the top if no search results are found. Let's replace the original dataSource condition with direct checks on trips.isEmpty and activities.isEmpty.\n\nThat's enough to get the app running again. I'll add a trip in Wishlist and rerun it.\n\nThis time, my Trip is still there.\n\n\"Northern Lights.\" The transition to SwiftData is almost complete. We have just a few loose ends to tie up. Consider this updateGoalAchievements method in ActivityItemView. It directly updates progress values as people complete activities. It can throw errors. I'll capture those errors in a state variable. I'll also pass the error to my telemetry system so that I can improve the app in the future. And when reasonable, I'll present an alert letting people know how to recover from the error. That's an error case handled. There are also a couple of places in the UI where state can go stale. Earlier, we removed the didSet observers that set dateEdited. And we'll want to add back that behavior now. Here's the bug: let's say I want to sort my activities for another trip by dateEdited.\n\nIn the sort dropdown I'll select \"Date Edited.\" Then if I check off this activity called \"Meditate under a tree,\" it should move to the top of the list because I just updated one of its properties. But it just sits there.\n\nNo errors are thrown here in terms of the persistence layer, but still something isn't right.\n\nNow that we've implemented persistence, we'll re-enable real-time updates to the dateEdited property using the Continuous Observation feature added to the Observation framework in the 2027 releases.\n\nIn the initializer of ActivityItemView, set up an observer using the new withContinuousObservation function. This view is where people can edit activities, so it's a good place for the observation. Whenever someone changes either isComplete or the name of an Activity, the observation framework will run this code to set dateEdited to the current time, triggering our query to automatically update the list of activities.\n\nThis is also a natural place to add a side effect on Trip. Whenever an activity's status is toggled, or an activity is added or removed, we update the isComplete property for the trip as a whole.\n\nNow you know what it takes to integrate Apple's declarative persistence framework into your own apps. Start by considering the appropriate representation of your app's state, and declare the Model types that make up your schema. From there, write targeted queries using predicates to balance between memory use and on-disk storage. And stay up to date on how you can continue to adjust your SwiftUI views for optimal interoperability with SwiftData.\n\nAnd with that, I can check off today's last activity, and earn my badge.\n\nThanks for listening, and safe travels.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:39",
+ "title": "Convert Activity to a persistent model with @Model",
+ "language": "swift",
+ "code": "import Foundation\nimport SwiftData\n\n// SwiftData automatically generates Observable conformance\n@Model\nclass Activity {\n var name: String\n var isComplete: Bool = false\n var dateCreated = Date.now\n var dateEdited = Date.now\n}"
+ },
+ {
+ "timestamp": "6:06",
+ "title": "Add Codable conformance to TripCollection",
+ "language": "swift",
+ "code": "enum TripCollection: String, CaseIterable, RawRepresentable, Codable {\n case springEscapes\n case summerVibes\n case fallGetaways\n case winterRetreats\n}"
+ },
+ {
+ "timestamp": "10:32",
+ "title": "Set up model relationships between Trip, TripImage, and Activity",
+ "language": "swift",
+ "code": "import Foundation\nimport SwiftData\n\n@Model\nclass Trip {\n var name: String\n var collection: TripCollection\n \n var photo: TripImage\n var thumbnailData: Data?\n \n @Relationship(deleteRule: .cascade, inverse: \\Activity.trip)\n var activities: [Activity] = []\n \n private(set) var creationDate = Date.now\n var subtitle: String?\n var isComplete: Bool = false\n}"
+ },
+ {
+ "timestamp": "13:21",
+ "title": "Enable interoperability between your schema and SwiftUI views",
+ "language": "swift",
+ "code": "import SwiftUI\nimport SwiftData\n\n@main\nstruct WishlistApp: App {\n let container: ModelContainer = {\n do {\n let modelContainer = try ModelContainer(for: Trip.self, Activity.self, TripImage.self, Goal.self, TripGoal.self, ActivityGoal.self)\n try SampleData.seedIfNeeded(in: modelContainer.mainContext)\n return modelContainer\n } catch {\n fatalError(\"Could not create model container: \\(error)\")\n }\n }()\n\n var body: some Scene {\n WindowGroup {\n ContentView()\n .preferredColorScheme(.dark)\n }\n .modelContainer(container)\n }\n}"
+ },
+ {
+ "timestamp": "16:27",
+ "title": "Fetch achieved and upcoming goals",
+ "language": "swift",
+ "code": "@Query(filter: #Predicate { $0.isAchieved }, sort: \\Goal.dateAchieved, order: .reverse)\nprivate var achievedGoals: [Goal]\n\n@Query(filter: #Predicate { !$0.isAchieved }, sort: \\Goal.sortOrder)\nprivate var upcomingGoals: [Goal]"
+ },
+ {
+ "timestamp": "16:49",
+ "title": "Fetch recent trips",
+ "language": "swift",
+ "code": "import SwiftUI\nimport SwiftData\n\nstruct RecentTripsPageView: View {\n // Fetch most recent trips in reverse chronological order\n @Query(FetchDescriptor(sortBy: [SortDescriptor(\\Trip.creationDate, order: .reverse)], fetchLimit: 5))\n private var trips: [Trip]\n\n @Namespace private var namespace\n\n var body: some View {\n TabView {\n ForEach(trips) { trip in\n NavigationLink {\n TripDetailView(trip: trip)\n .navigationTransition(\n .zoom(sourceID: trip.id, in: namespace))\n } label: {\n TripImageView(trip: trip)\n .overlay(alignment: .bottomLeading) {\n VStack(alignment: .leading) {\n Text(\"RECENTLY ADDED\")\n .font(.subheadline)\n .fontWeight(.bold)\n .foregroundStyle(.limeGreen)\n\n Text(trip.name)\n .font(.title)\n .fontWidth(.expanded)\n .fontWeight(.medium)\n .foregroundStyle(.primary)\n }\n .padding(.horizontal)\n .padding(.bottom, 54)\n }\n .matchedTransitionSource(id: trip.id, in: namespace)\n }\n .buttonStyle(.plain)\n }\n }\n .tabViewStyle(.page)\n .containerRelativeFrame([.horizontal, .vertical]) { length, axis in\n if axis == .vertical {\n return length / 1.3\n } else {\n return length\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "17:26",
+ "title": "Dynamically construct a query in the initializer of TripCollectionView",
+ "language": "swift",
+ "code": "init(tripCollection: TripCollection, cardSize: TripCard.Size, namespace: Namespace.ID) {\n _trips = Query(filter: #Predicate { $0.collection == tripCollection }, sort: \\Trip.name)\n self.tripCollection = tripCollection\n self.cardSize = cardSize\n self.namespace = namespace\n}"
+ },
+ {
+ "timestamp": "18:13",
+ "title": "Search for trips and activities by name",
+ "language": "swift",
+ "code": "import SwiftUI\nimport SwiftData\n\nprivate struct SearchResultsListView: View {\n @Query(sort: \\Trip.name) private var trips: [Trip]\n @Query(sort: \\Activity.name) private var activities: [Activity]\n\n var searchText: String\n var namespace: Namespace.ID\n\n init(searchText: String, namespace: Namespace.ID) {\n self.searchText = searchText\n self.namespace = namespace\n\n if searchText.isEmpty {\n _trips = Query(FetchDescriptor(sortBy: [SortDescriptor(\\Trip.creationDate, order: .reverse)], fetchLimit: 3))\n _activities = Query(filter: #Predicate { _ in false })\n } else {\n // All trips whose name matches searchText, sorted lexicographically\n let tripSearchPredicate = #Predicate { $0.name.localizedStandardContains(searchText) }\n _trips = Query(filter: tripSearchPredicate, sort: \\Trip.name)\n // All matching activities that belong to a trip\n let activitySearchPredicate = #Predicate { $0.trip != nil && $0.name.localizedStandardContains(searchText) }\n _activities = Query(filter: activitySearchPredicate, sort: \\Activity.name)\n }\n }\n\n var body: some View {\n List {\n if !trips.isEmpty {\n TripSearchSectionView(trips: trips, namespace: namespace, title: searchText.isEmpty ? \"Recent Trips\" : \"Trips\")\n }\n\n if !activities.isEmpty {\n ActivitySearchSectionView(activities: activities)\n }\n }\n .overlay {\n if trips.isEmpty && activities.isEmpty {\n ContentUnavailableView(\n \"No results for “\\(searchText)”\",\n systemImage: \"magnifyingglass\",\n description: Text(\"Check spelling or try a new search.\")\n )\n }\n }\n .listStyle(.plain)\n }\n}"
+ },
+ {
+ "timestamp": "19:42",
+ "title": "Capture and report errors from ActivityItemView",
+ "language": "swift",
+ "code": "var body: some View {\n HStack(alignment: .firstTextBaseline, spacing: 17) {\n Group {\n if isEditing {\n rowContentWhenEditing\n } else {\n rowContentWhenNotEditing\n }\n }\n .transition(.opacity.animation(.snappy))\n .animation(.snappy, value: isEditing)\n }\n .onDisappear {\n do {\n try updateGoalAchievements()\n } catch {\n updateError = error\n reportError(error)\n }\n }\n .alert(error: $updateError) {\n // Customize the presentation of the error\n }\n}"
+ },
+ {
+ "timestamp": "21:04",
+ "title": "Update dateEdited and propagate side effects on property changes",
+ "language": "swift",
+ "code": "init(activity: Activity, isLast: Bool, isEditing: Bool) {\n activity.token = withContinuousObservation(options: .didSet) { event in\n _ = activity.name\n _ = activity.isComplete\n\n if event.matches(\\Activity.name) {\n activity.dateEdited = .now\n }\n\n if event.matches(\\Activity.isComplete) {\n activity.dateEdited = .now\n activity.trip?.isComplete = activity.trip?.activities.isEmpty == false\n && activity.trip?.activities.allSatisfy { $0.isComplete } == true\n }\n }\n self.activity = activity\n self.isLast = isLast\n self.isEditing = isEditing\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Wishlist: Planning travel in a SwiftUI app",
+ "url": "https://developer.apple.com/documentation/SwiftUI/wishlist-planning-travel-in-a-swiftui-app"
+ },
+ {
+ "title": "SwiftData",
+ "url": "https://developer.apple.com/documentation/SwiftData"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/275/4/7c64f887-3c3c-4bdf-8472-72d6b96f8e3d/downloads/wwdc2026-275_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/275/4/7c64f887-3c3c-4bdf-8472-72d6b96f8e3d/downloads/wwdc2026-275_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "274",
+ "year": "2026",
+ "title": "What’s new in SwiftData",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/274"
+ },
+ {
+ "id": "291",
+ "year": "2025",
+ "title": "SwiftData: Dive into inheritance and schema migration",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/291"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:17.534Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-277.json b/data/wwdc/videos/2026-277.json
new file mode 100644
index 0000000..349400d
--- /dev/null
+++ b/data/wwdc/videos/2026-277.json
@@ -0,0 +1,81 @@
+{
+ "id": "277",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/277/",
+ "title": "WidgetKit foundations",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, my name is Jonathan Long and I'm an engineer on the system experience team.\n\nPeople love using widgets. Your widgets provide relevant content in the convenient places people visit most. Widgets are available on many Apple platforms including iOS, iPadOS, watchOS, visionOS, and macOS. Your widgets extend the reach of your app across the system providing another opportunity for people to experience your great apps. So let's dive into widgets and discuss the foundations of widget kit, how to build your first widget, and how to keep it up to date. Today, I will cover Widget fundamentals and building your first widget. What WidgetKit offers to better integrate your app and widget experience. And how to ensure your widgets adapt when the system environment changes, like to a tinted environment. First, fundamentals, beginning with what are widgets? There are three qualities that make widgets memorable.\n\nWidgets are glanceable. People should be able to understand your widgets with a quick glance. For example, weather shows me just enough information about the current forecast to help me get ready for my day.\n\nWidgets are relevant. Widgets content should match expectations for time, personal patterns, and location. The calendar widget shows me the next events at the current time and updates throughout the day. Widgets are personalizable; able to be configured with the content that matters to me, like this photos memory widget showing me a great memory at a family camp with my daughters.\n\nUsing WidgetKit and SwiftUI you can provide great glanceable, relevant, and personalizable widgets for your apps content.\n\nI have been building an app for my book club to encourage me to read more. The app lets me track how much I have read, the books I have completed, and creates a schedule to help me complete a book before my next book club meeting. I also created some widgets so I can view this data right from my home screen.\n\nThe first widget is the reading goals widget encouraging me to read throughout the day.\n\nThe second, the reading log widget, helps me keep track of my current progress through a book. The third widget builds a schedule for me to follow so I can finish my book on time.\n\nThroughout this talk I'll cover key decisions for each widget that can help when creating your widgets.\n\nI'll start by covering how an app provides widgets to the system. Whether an app is built with SwiftUI or UIKit, it can have widgets, and the widgets will be built with SwiftUI. Your app provides widgets to the system from a widget extension. This extension runs as a separate process from your app. Because your widget extension is running as a separate process, you will need to use a shared container in an app group to provide data from your app to your widget extension. You can use something like a shared database or user defaults. The widget extension is solely focused on the widget. The system and widget kit interact with the extension to get the data needed for widgets.\n\nWidgetKit asks the extension for content. This content is called a timeline.\n\nA timeline is made up of multiple timeline entries. WidgetKit provides these entries as data for the widgets view. The resulting views are archived. The system displays them at their relevant time.\n\nWidgetExtensions expose your widgets to the system and provide their timeline content. Timelines are a series of entries, each carrying the data needed to render your widget's view at a specific point in time.\n\nNow let's look at the code for the reading goals widget that brings these pieces together.\n\nWhen you create a new target for your widget extension, Xcode generates a widget for you.\n\nFor a simple widget this implementation already has a lot of what we need. Starting with a body providing a WidgetConfiguration.\n\nThere are two types of configurations for widgets: AppIntentConfigurations and StaticConfigurations. AppIntentConfigurations are used when your widget can be configured by the user.\n\nMy widget will be automatically configured using the current book I am reading, so I am using the static configuration which is the most simple. The static configuration needs a few parameters. The kind, which is a unique identifier for this specific type of widget.\n\nA timeline provider that produces timeline entries for the widget. And a closure that takes a timeline entry and returns a View. From the closure you return a SwiftUI view for the provided timeline entry.\n\nBecause my book club app is built in SwiftUI, I already have a view, the DailyReadingGoalView, that is exactly what I need for my widget.\n\nTo specify the background, I am using the containerBackground modifier. This modifier identifies which view is the background of my widget.\n\nWhen people using my widget customize their devices with a colored or clear tint, this allows the system to replace the background view specified with a glass material view.\n\nThe view portion of the Reading Goal widget is implemented. Let's have a closer look at what is expected of the timeline provider and how timelines keep widgets relevant.\n\nThe timeline provider supplies entries to represent three separate states for my widget: Snapshots, Placeholder, and Timeline.\n\nA snapshot is a realistic preview of a widget. It's what people see in the widget gallery.\n\nThis is an opportunity for my widget to make a strong first impression. My app has no data before someone starts using it, so I feature a popular book, Atomic Habits, with a default message. So people can imagine my widget at its best before they ever add it to their Home Screen. A placeholder is a stand-in view the system shows when your widget doesn't have content to display yet, like the very first time it's loading your timeline. Because it needs to show up instantly, fetching a placeholder has to be synchronous. So provide a placeholder that does not need data from disk or over the network.\n\nFor the Reading Goals widget, I'm using SwiftUI's redacted modifier, which creates a simplified version of my View.\n\nA timeline entry is what your widget shows at a specific moment in time. That could be right now, or any point in the future. Your timeline provider produces a collection of these entries, and the system renders each one at its specific time.\n\nThe timeline entry for the reading goals widget has all the information the widget needs to render this view. The motivational message, pages read, title, and cover name.\n\nMost of this data will be the same for this widget except for the motivational message. The message updates at various times throughout the day. As I begin discussing the timelines, I'm going to only show the message that will be changing over time.\n\nTimelines are critical to how widgets stay up to date.\n\nEach timeline entry provides the needed data to render a widget view.\n\nThis timeline is currently showing two entries, one at 9am and one at 11:30am.\n\nThe core piece of data my reading goals widget needs is the motivational message that updates throughout the day as widget kit advances the timeline.\n\nThese timeline entries are provided to my widget to produce the relevant view for that entry at the specific time, keeping my widget relevant with fresh content.\n\nAt some point your timeline will need to be refreshed with more timeline entries. This is referred to as reloading a timeline. Timelines specify their reload behavior through a reload policy.\n\nThe reload policy can be one of three options: atEnd, afterDate, and never.\n\nLet's discuss each policy and when it makes sense to choose which one.\n\nThe atEnd policy indicates that the timeline should be reloaded when all timeline entries have been exhausted.\n\nThroughout the day new timeline entries will be rendered.\n\nWhen the last entry has been shown at 1:00pm, the system issues a reload, asking your widget extension for more content.\n\nYour widget extension will provide a new timeline with more timeline entries. The reading goals widget updates its motivational message at varied times throughout the day. The atEnd policy makes sense because this widget provides a series of entries where the end is not a single known time and needs to be reloaded when the timeline is exhausted.\n\nThe afterDate policy allows you to specify a specific date your widget desires a reload. These are different timeline entries powering the reading schedule widget. I have redacted the complete set of data and only include the chapter information that changes over time. Here the reading schedule widget provides two timeline entries indicating what needs to be read today and tomorrow.\n\nThe widget needs to be reloaded at the end of the day to recalculate the downstream schedule.\n\nIt uses the afterDate reload policy specifying a date at the end of the day.\n\nOnce the current time reaches this date, my extension is reloaded and asked for a new timeline.\n\nThe new timeline provides entries for Tuesday and Wednesday with the recalculated schedule. The afterDate policy is great in cases like the reading schedule widget when there is a specific time you know your widget will need to be reloaded.\n\nAnd lastly, the never reload policy. As the name suggests, your widget won't reload on its own. Use this when automatic reloads don't make sense. The Reading Log widget is a good example. It only needs to refresh when someone interacts with the app or widget. When you do need to reload, you can do this with an explicit call to WidgetCenter's reload APIs or by sending a push notification.\n\nTo dig deeper into reload policies, check out \"Principles of great widgets\" from WWDC21.\n\nThere are a few best practices to keep in mind when considering building and reloading your timeline.\n\nProvide multiple timeline entries whenever possible. This ensures the system always has content to show for your widget.\n\nWidgetKit was built with all day battery life in mind. So each widget is given a budget for updates. The budget is heavily influenced by users viewing habits and is updated throughout the day.\n\nFrequency of updates can vary and the system is smart enough to adapt reloads for your widget as it makes sense.\n\nKnow that frequent reloads while your app is in the foreground might be throttled. If your widget data may have changed a final call to reload when your app enters the background is usually a good idea.\n\nSome content is ephemeral with a defined start and end date, needs more frequent updates, and wants alerting capabilities, like a sporting event. If this describes your content, consider building a live activity. You can learn more about Live Activities in \"Live Activities essentials\" from WWDC 26.\n\nSo far, all my widgets have been the same size — the system medium family.\n\nBut now that my WidgetExtension and timeline provider are wired up, a whole lineup of other widget families are available to me.\n\nWidgets come in all different shapes and sizes. It is recommended to support as many sizes as you can so that people using your widget have choices when placing their widgets.\n\nThe system extra large portrait family was introduced in visionOS 26. New in macOS, iOS, and iPadOS 27, the system extra large portrait family is now available. This new family allows people using your widgets to have a really good look at your apps content.\n\nThe Book Club reading schedule can reuse the same data from the medium widget we have already implemented. This larger size allows me to see how far along I should be in the next couple of days.\n\nNot all widget families make sense for your widget, and starting with a few families is most simple.\n\nHere is my daily reading goal widget I am building. I can use the .supportedFamilies modifier, listing all the widget families my widget supports.\n\nWhen adding a new widget family, I can reuse the same widget and timeline provider, and build a swiftUI view that makes sense for the new families shape and size.\n\nI just covered the basic steps you need to build your first widget. Build a widget with a widget extension that is focused on your widget. You provide content to your widget through a timeline made up of timeline entries.\n\nChoose a timeline reload policy that makes sense for your use case.\n\nMy app now has a great iOS widget to keep me on track for book club. And my widget is actually available in other places as well.\n\nMy iOS widget is available on CarPlay and as a remote widget on macOS.\n\nWe just discussed what you need to do to build a widget. Now let's discuss some additional options WidgetKit provides to better integrate your apps content with your widget.\n\nYou can better integrate your widget with your app by using Deep links, making your widgets configurable, and by adding interactive elements to your widgets.\n\nThe default interaction for your widget is to open your app, which is a great starting point. If your widget is showing specific content from within your app, your widget can provide a deep link directly to that content.\n\nLet's jump back into the Reading Goals Widget code and see how to do this.\n\nThe reading goals widget shows the current book I am reading. I will provide a deep link to the book's details page so tapping the widget lands people where they expect.\n\nI will use the widgetURL modifier specifying a deep link URL for my app to handle on launch. The URL encodes the book's ID, so the app launches directly to the book's details page.\n\nDeep links are a great way to integrate your widget with your app, keeping people in context as they move between them. Configurable widgets are another great way to integrate your widget with your app. Making your widget configurable lets people personalize their widget with specific content from your app.\n\nThe weather widget is a great example. People can configure this widget to show conditions from a location that is important to them. I like to keep track of the weather in Yosemite, just in case I can plan a spur of the moment trip.\n\nThe reading log widget in my book club app is another example of a configurable widget. People using my app can configure this widget to track reading sessions for a specific book. Configurable widgets can be configured from wherever they are placed, like from the iOS home screen.\n\nThis is the configuration UI for my reading log widget which only has one parameter. The UI allows people using my widget to select the book they want to track. I made the most recently read book the default so people don't need to make a selection. Configurable widgets also let people add multiple widgets with different configurations. Here my home screen has three reading log widgets tracking the three different books I am reading.\n\nWhen you're thinking about configurable widgets, there are a few things to keep in mind. Consider whether your widget's content should change depending on who's using it. Keep configuration fast — one or two parameters is usually all you need.\n\nAnd don't require configuration up front, provide sensible defaults that people can tweak later if they want to. To learn more about making your widget configurable with App Intents, check out \"Explore enhancements to App Intents\" from WWDC23.\n\nConfigurable widgets give people using your widget options to make their devices relevant and personal for them.\n\nYour widget can also integrate with your app through interactive elements. Buttons and toggles let people perform actions directly from the widget.\n\nReminders is a great example of an interactive widget. Once I have completed a task, I can check it off right from the widget. Receiving the dopamine hit I need to keep moving through my day.\n\nWhen you're thinking about interactive elements, here are a few things to keep in mind.\n\nWidgets can surface interactive elements as either a toggle or a button.\n\nThink about the most important action in your app and expose it from your widget, like the complete chapter button on the reading log widget. Widget views are archived and rendered by the system, so your code isn't running while the widget is on screen. Buttons and toggles take an App Intent that the system can execute on your behalf when someone interacts with the element. To learn more about interactive widgets, check out \"Bring widgets to life\" from WWDC23.\n\nYour widget is an extension of your app. Deep links, configurable widgets, and interactive elements are all great ways to unify the experience between your app and widget.\n\nPeople using your widget can also customize their system experience. Widgets were designed to adapt to these system changes. On iOS, the system can be customized to be tinted with a specific color or to have a clear tint. With either customization, the system renders your widget through a glass material, tinting your content and replacing your background with an adaptive glass effect. This keeps every widget on the Home Screen feeling cohesive. SwiftUI does a lot of the heavy lifting here, but it is important to test your widget to make sure it looks great.\n\nAs I begin testing my reading goals widget, I can see that it is looking really great when rendered in full color. Before I'm done, I want to test with a tinted customization to make sure my widget is rendering as expected.\n\nI'll do this by running on device and customizing my home screen.\n\nWhen I change my iPhone to use the clear mode, my widget isn't rendering correctly. The book cover is a large white rectangle, which is not the intended behavior. This is my BookCoverImage view. In the view's body the image is rendered for this specific book from the asset catalog. When rendering in the accented rendering mode the system is unable to accent the image appropriately.\n\nI can specify the rendering mode to be used for this image with the widgetAccentedRenderingMode modifier.\n\nWith this option my widget now renders the book cover using full color. I am using the full color option so the book covers are rendered respecting their original colors. With this change my widget is looking great in the accented rendering mode.\n\nIt's important to test your widget in every environment they appear. Start with your local devices. Try your widgets in full color, tinted, and clear mode to make sure they render as expected.\n\nRemember that your iOS widgets also show up on macOS as remote widgets. Take the time to test that your interactions still feel right when someone's using them from a Mac.\n\nLean on SwiftUI previews to iterate quickly.\n\nIn the swiftUI canvas on the right side of Xcode you can check different families, color schemes, and rendering modes without ever leaving Xcode.\n\nTurn on WidgetKit developer mode while you're testing to lift constraints like reload budgets. This allows you to iterate on your widgets more quickly. To learn more about adapting your widget to these system customizations, check out \"What's new in widgets\" from WWDC25.\n\nI covered a lot today. Before I let you go, keep these things in mind.\n\nI hope you are inspired to go and build a great widget! Consider how you can extend and personalize your widget experience by integrating with your app. And be sure to test and adapt your widget to all the custom environments that each platform provides.\n\nWidgets make my iPhone fun and personal to me! I can't wait to use the great widgets you create!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:50",
+ "title": "DailyReadingGoalWidget",
+ "language": "swift",
+ "code": "struct DailyReadingGoalWidget: Widget {\n let kind = \"DailyReadingGoalWidget\"\n \n var body: some WidgetConfiguration {\n StaticConfiguration(\n kind: kind,\n provider: DailyReadingGoalProvider()\n ) { entry in\n DailyReadingGoalView(book: entry.book,\n message: entry.message,\n timeOfDay: entry.timeOfDay)\n .environment(\\.colorScheme, .dark)\n .containerBackground(for: .widget) {\n Background()\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "12:25",
+ "title": "Supported Families",
+ "language": "swift",
+ "code": "struct DailyReadingGoalWidget: Widget {\n let kind = \"DailyReadingGoalWidget\"\n\n var body: some WidgetConfiguration {\n StaticConfiguration(\n kind: kind,\n provider: DailyReadingGoalProvider()\n ) { entry in\n DailyReadingGoalView(book: entry.book,\n message: entry.message,\n timeOfDay: entry.timeOfDay)\n .environment(\\.colorScheme, .dark)\n .containerBackground(for: .widget) {\n Background()\n }\n }\n .supportedFamilies([.systemMedium])\n }\n}"
+ },
+ {
+ "timestamp": "14:03",
+ "title": "Adding deep links",
+ "language": "swift",
+ "code": "struct DailyReadingGoalWidget: Widget {\n let kind = \"DailyReadingGoalWidget\"\n\n var body: some WidgetConfiguration {\n StaticConfiguration(\n kind: kind,\n provider: DailyReadingGoalProvider()\n ) { entry in\n DailyReadingGoalView(book: entry.book,\n message: entry.message,\n timeOfDay: entry.timeOfDay)\n .environment(\\.colorScheme, .dark)\n .containerBackground(for: .widget) {\n Background()\n }\n .widgetURL(URL(string: \"bookclub://reading/\\(book.bookID)\"))\n }\n .supportedFamilies([.systemMedium])\n }\n}"
+ },
+ {
+ "timestamp": "18:17",
+ "title": "Accented rendering mode",
+ "language": "swift",
+ "code": "struct BookCoverImage: View {\n let imageName: String\n\n var body: some View {\n Image(imageName: bundle: .main)\n .widgetAccentedRenderingMode(.fullColor)\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/277/4/e9dd0c7d-3a2e-4cf3-9e65-c9cba19d3616/downloads/wwdc2026-277_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/277/4/e9dd0c7d-3a2e-4cf3-9e65-c9cba19d3616/downloads/wwdc2026-277_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "223",
+ "year": "2026",
+ "title": "Live Activities essentials",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/223"
+ },
+ {
+ "id": "278",
+ "year": "2025",
+ "title": "What’s new in widgets",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/278"
+ },
+ {
+ "id": "10028",
+ "year": "2023",
+ "title": "Bring widgets to life",
+ "url": "https://developer.apple.com/videos/play/wwdc2023/10028"
+ },
+ {
+ "id": "10103",
+ "year": "2023",
+ "title": "Explore enhancements to App Intents",
+ "url": "https://developer.apple.com/videos/play/wwdc2023/10103"
+ },
+ {
+ "id": "10048",
+ "year": "2021",
+ "title": "Principles of great widgets",
+ "url": "https://developer.apple.com/videos/play/wwdc2021/10048"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:17.634Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-278.json b/data/wwdc/videos/2026-278.json
new file mode 100644
index 0000000..e81e7a6
--- /dev/null
+++ b/data/wwdc/videos/2026-278.json
@@ -0,0 +1,146 @@
+{
+ "id": "278",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/278/",
+ "title": "Modernize your UIKit app",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Michael Ochs, an engineering manager on the UI Frameworks team.\n\nAnd today I'll tell you how to modernize your UIKit app. In this video I'll tell you about some big changes to app adaptivity requirements, and how iPhone apps are fully resizable.\n\nNext, I'll present some new APIs in tab bars, navigation bars and menus.\n\nThen, I'll talk about how to support Apple Intelligence and some changes you should be aware of.\n\nAnd finally I'll cover a new skill that helps an agent to handle most of this modernization work automatically.\n\nLet's get started with app adaptivity.\n\nYour app can already appear in many different environments. There are different screen sizes, and apps can run side by side - with other apps and even themselves. On iPad, people expect apps to resize and move to external displays seamlessly. On iPhone, people expect apps to work in both portrait and landscape orientations. They also expect to mirror their iPhone to the Mac. In iOS and macOS 27 this experience has been improved and apps running in these environments are now exposed to many of the dynamic changes any app native to the platform needs to support.\n\nWhen people use iPhone Mirroring on the Mac, they can fully resize the iPhone window, allowing your apps to resize and adapt. Likewise, an iPhone-only app running on iPad will be fully resizable like any other iPad app. Because of this, it is important that your app dynamically adjusts to any available scene size at runtime. If your app is a universal binary, you are off to a great start. But there is more to do to make your app fully adaptive and have it dynamically adjust to the environment it is running in. I will cover the most important steps and common issues you might encounter when making your app more adaptive: verifying your app is no longer using app lifecycle, and instead has adopted scene lifecycle, the basis for any adaptive app, making sure your app is not using main screen references, and how to handle user interface idiom and interface orientation checks.\n\nThe first step is to move from app lifecycle to scene lifecycle.\n\nMost apps are already using scene lifecycle. It is the basis for an adaptive app and for many of the other tasks I'm going to show you in this video. UIScene lifecycle is now required when building with the latest SDKs. Without it, your application will no longer launch. Verify that your app is using a UISceneDelegate which is the basis for scene lifecycle.\n\nIf you have not yet migrated to scene lifecycle, check out the video \"Make your UIKit app more flexible\" from WWDC25 and read \"Transitioning to the UIKit scene-based lifecycle\" in the documentation. Next, when your iPhone app is mirrored on a Mac, or when a person moves your app to an external display on the iPad, the screen associated with your scene changes. That means that any reference to the main screen in your code will provide incorrect information for the environment your app is running in.\n\nIt is important that you do not reference the main screen in your app. Instead, access the screen dynamically from a window's window scene.\n\nWhen a view or view controller is not available in the immediate context, pass in a screen reference to any method that needs it.\n\nEven better than fetching the correct screen is to remove screen references all together. There are a lot of other APIs that are better suited for an adaptive app. I'll take you through two common patterns.\n\nAccessing the scale of the screen should be replaced with the trait collection's displayScale.\n\nViews and view controllers automatically update their trait collections and provide reasonable fallbacks even when they are not part of the visible hierarchy. A lot of common override points are also tracked, meaning that you do not need to explicitly monitor for changes. Instead, the system tracks which trait collection properties are being used inside common layout and drawing methods, like layoutSubviews, updateProperties, drawRect, and many others.\n\nIf a change to a tracked trait is detected, the system will ensure these methods are called again so your UI automatically updates accordingly. Check out \"Automatic trait tracking\" in the documentation to learn more. In places where automatic trait tracking is not available, registerForTraitChanges can be used to observe changes.\n\nThis method allows you to set up a closure that is called when a specific trait in the receiving view changes.\n\nUse this to invalidate caches or update other data related to that view.\n\nCheck out \"Adapting your app when traits change\" in the documentation to learn more. Another common use of the screen is to check its bounds to get the amount of space available to your app.\n\nHowever, in an adaptive environment, the space your scene has available is not always the full screen. So if you still have any references to screen bounds in your app, now is the time to remove these.\n\nThe window scene's effective geometry provides information on how much space your app has available. If you need to monitor the effective geometry for changes, implement windowScene:didUpdateEffectiveGeometry: in your scene delegate.\n\nIn your views and view controllers, instead of referencing the scene bounds, use the available size of your view controller's view or your view's superview to determine how much space is available to it.\n\nThis makes your UI less dependent on how it is presented. It will adjust better when your view controller appears in other contexts, for example inside of a split view controller.\n\nFor games, resizing can be challenging. Due to this, UIRequiresFullscreen is honored on iPhone in resizable environments starting in iOS 27. Its behavior has also been updated and no longer opts your app fully out of resizing. Instead, it enables discrete resizing that honors your supported interface orientations.\n\nIn discrete resizing, every time a person changes the scene size, the system transitions the scene to a new screen configuration matching that size, so your game always renders at full quality in the available space. If your app is using the User Interface idiom trait, be aware that this trait is no longer meaningful for any kind of layout decision.\n\nYour app is expected to use the additional space in a meaningful way, regardless of whether it is running in the phone or pad user interface idiom.\n\nWhen your iPhone app is running on an iPad or in iPhone Mirroring on the Mac, it will be fully resizable, but it will still run under the phone user interface idiom. Stop checking the user interface idiom for any layout decisions in your code.\n\nUse size classes instead to handle sizing constraints, such as collapsing menus and updating your app's layout for the available space. If you need finer control, use the surrounding view's size like I mentioned earlier.\n\nInterface orientation also is no longer useful for layout decisions.\n\nIn iOS 27, an app's supported interface orientation is a preference provided to the system. It will be ignored when your app is running in a resizable environment. You should not consider interface orientation for any layout calculations. In iPhone Mirroring on the Mac, your apps will always be running in the portrait interface orientation, regardless of the aspect ratio of your app's scene. Any interface orientation checks in your app should also be updated to use size classes. This conceptual shift was introduced in iOS 8.\n\nAt WWDC2014, Bruce Nilo said: \"A device rotation is only an animated bounds change.\" With an array of device sizes, resizable windows on iPad, and resizable iPhone apps on Mac, today this insight is more relevant than ever. And speaking of interface orientation: in iOS 27 UIView also conforms to the new Body protocols from CoreMotion and CoreLocation.\n\nThis makes it much easier to configure your motion and location managers. Connect them to the view that visualizes the motion data, such as a compass or a map view. This ensures the data is always in the right coordinate space, regardless of interface orientation.\n\nThat covers the main adaptivity changes. Now here is how to test these changes in your app.\n\nXcode 27 brings new ways to iterate on your app's behavior across different screen sizes, without having to install your app on multiple separate simulators or devices.\n\nIn the new Device Hub app as well as in Xcode Previews, click the \"enter resize mode\" icon. Then, drag the edges of the device to resize it freely. This allows you to iterate on your changes quicker. Once you are satisfied with the results, make sure to test iPhone Mirroring and iPad with real devices.\n\nTo learn more about the Device Hub app and its tools, watch \"Get the most out of Device Hub\".\n\nAnd that's adaptivity. Before I show you a new agentic coding skill that can help you with the work necessary to make your app fully adaptive, there are two more topics to cover. First up: bars and menus.\n\nOn iPad, tab bars can expand into a full sidebar representation to surface more sections of the app hierarchy when the current environment supports a sidebar. On iPhone, the bottom tab bar is shown across all sizes by default.\n\nNew in iOS 27, iPhone apps can also opt into sidebars by setting the tab bar controller's sidebar.preferredPlacement to .sidebar. Note that in contrast to the iPad, this is an app choice. If your app opts into the sidebar representation, there is no way to toggle between a sidebar and tab bar layout in the UI.\n\nInstead, the system determines if there is enough space to show a sidebar, for example when the horizontal size class is regular.\n\nTo determine if the tab bar's sidebar representation can be shown, use the sidebar's isAvailable property.\n\nIf a sidebar is not currently available, surface the UI behind nested tabs in other parts of your app. To learn more about managing tab groups, watch \"Make your UIKit app more flexible\" from WWDC25, and to learn more about tab bars and its integration with sidebars, watch \"Elevate your tab and sidebar experience in iPadOS\" from WWDC24.\n\nUITabBarController also lets you customize the prominent tab. The prominent tab is always visible, even when the tab bar collapses during scrolling. In iOS 27, you have the option to make any tab prominent by setting the prominentTabIdentifier.\n\nOkay, that's tab bars. Now let's talk about navigation bars.\n\nNavigation bars can interactively slide away as people scroll. This provides more room on the screen for your app's content. By default, navigation bars minimize in certain conditions defined by the system.\n\nYou can force this behavior, one way or the other, by setting the barMinimizationBehavior property on your navigation item to .always or .never. If you handle safe area avoidance yourself, set barMinimizationSafeAreaAdjustment to .never so bar minimization doesn't update insets automatically.\n\nAnother change during scroll interactions is an updated appearance for the scroll edge effects. As such, you should review your design, especially where you have previously overridden the defaults provided by the OS.\n\nIn particular, the .automatic style no longer switches between the existing soft and hard styles but provides its own visuals for additional clarity. If you have overridden the style from .automatic previously, that decision should be re-evaluated, especially when set to .soft, as that no longer matches the default system appearance.\n\nWith the refined look of Liquid Glass, images you set on menu elements may not be shown by default in some contexts, such as in the menu bars on iPadOS and macOS.\n\nIf you still need an image to be visible, set the preferredImageVisibility property to override the default system behavior.\n\nReview the updated Human Interface Guidelines for when to include visible images in a menu element.\n\nAnd that's bars and menus. Now, how to support Apple Intelligence in your app.\n\nMenus in iOS 27 feature an Ask Siri button, to allow people to start a conversation with Siri right from your app. This is a powerful entry point that allows people to interact with the context they care about.\n\nMenus will automatically display this item when there's content relevant for Siri. To provide more relevant information specific to your app, use the new View Annotations API. With it you can annotate specific views with AppEntities. Check out \"Explore advanced App Intents features for Siri and Apple Intelligence\" to learn more.\n\nIf your app supports drag and drop, Siri can load resources from your application's drag handlers.\n\nWhen Apple Intelligence is invoked from context menus, the system will call available drag delegate methods to load the content. Avoid performing animations or presenting modal UI from sessionWillBegin. Drag sessions can be initiated without a user gesture. If your app has a stateful UI that shows up when a user initiates a drag, put that code in sessionDidMove instead.\n\nIf the adaptivity changes I outlined sound like a lot of work, I've got you covered. Let's talk about what's on everyone's mind: agentic coding! New in Xcode 27 is an app modernization skill. It has a deep understanding of the adaptivity tasks I outlined. And with the context of your project, it can make a lot of these changes automatically. Use Xcode's intelligence features and ask an agent to make your app more adaptable.\n\nIt will automatically convert main screen calls to traitCollection or scene bounds checks, adding invalidation logic if necessary.\n\nIt will also replace interface orientation checks with size class checks. It will even convert your app to use scene lifecycle. For more complex tasks, it will ask clarifying questions. And for tasks too large to handle in a single session, it will add comments to help you keep track of the remaining work.\n\nAnd to use skills in other tools, you can export the ones Xcode uses with \"xcrun agent skills export\". This will create markdown files that you can then import in your workflows.\n\nSkills like this one are a powerful way to prepare your app for resizable environments.\n\nAnd… that's it! These are the most important things to modernize your app.\n\nBuild your app with the iOS 27 SDK, and try out the resizable simulator in the new Device Hub app and test your app in iPhone Mirroring on macOS 27. Identify areas in your app that need to be a little more flexible. And if you like agentic coding, give the new skill a try and discover how much it can do automatically.\n\nThanks for joining. I cannot wait to resize your apps.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:24",
+ "title": "Use local screen references",
+ "language": "swift",
+ "code": "// Use local screen references\n// Access the correct screen through a windowScene\nlet screen = window?.windowScene?.screen\n\n// Pass in local screen references\nfunc generateThumbnail(_ image: UIImage, screen: UIScreen) -> UIImage {\n // existing code, replacing main screen with local screen reference\n // ...\n}"
+ },
+ {
+ "timestamp": "3:49",
+ "title": "Replace screen scale with displayScale",
+ "language": "swift",
+ "code": "// Replace the screen's scale with trait collection's displayScale\noverride func layoutSubviews() {\n super.layoutSubviews()\n\n // layoutSubviews will be called again automatically when displayScale changes\n let displayScale = traitCollection.displayScale\n // ...\n}"
+ },
+ {
+ "timestamp": "4:36",
+ "title": "Register for trait changes",
+ "language": "swift",
+ "code": "// Manually register for trait changes\nlet displayScaleTrait: [UITrait] = [UITraitDisplayScale.self]\nregisterForTraitChanges(displayScaleTrait) {\n (view: GalleryView, previousTraitCollection: UITraitCollection) in\n view.cache.invalidate()\n}"
+ },
+ {
+ "timestamp": "5:19",
+ "title": "Monitor effective geometry changes",
+ "language": "swift",
+ "code": "// UIWindowSceneDelegate\nfunc windowScene(\n _ windowScene: UIWindowScene,\n didUpdateEffectiveGeometry previousEffectiveGeometry: UIWindowScene.Geometry\n) {\n let geometry = windowScene.effectiveGeometry\n let availableSpace = geometry.coordinateSpace.bounds\n // ...\n}"
+ },
+ {
+ "timestamp": "5:35",
+ "title": "Check available space using view bounds",
+ "language": "swift",
+ "code": "// Checking available space\noverride func viewDidLayoutSubviews() {\n super.viewDidLayoutSubviews()\n\n let availableSpace = view.bounds.size\n // ...\n}"
+ },
+ {
+ "timestamp": "8:12",
+ "title": "Configure motion and location body",
+ "language": "swift",
+ "code": "// Configure motion and heading bodies\noverride func viewDidLoad() {\n super.viewDidLoad()\n\n motionManager.deviceMotionBody = view\n locationManager.headingBody = view\n}"
+ },
+ {
+ "timestamp": "9:51",
+ "title": "Opt into sidebar layout",
+ "language": "swift",
+ "code": "tabBarController.sidebar.preferredPlacement = .sidebar"
+ },
+ {
+ "timestamp": "10:22",
+ "title": "Check sidebar availability",
+ "language": "swift",
+ "code": "tabBarController.sidebar.isAvailable"
+ },
+ {
+ "timestamp": "10:53",
+ "title": "Set prominent tab identifier",
+ "language": "swift",
+ "code": "// Set the prominent tab\nlet tabs = [\n // ...\n]\nlet tabBarController = UITabBarController(tabs: tabs)\ntabBarController.prominentTabIdentifier = \"cart\""
+ },
+ {
+ "timestamp": "11:30",
+ "title": "Customize bar minimization behavior",
+ "language": "swift",
+ "code": "// Customize bar minimization behavior\noverride init(\n nibName nibNameOrNil: String?,\n bundle nibBundleOrNil: Bundle?\n) {\n super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)\n\n navigationItem.barMinimizationBehavior = .always\n navigationItem.barMinimizationSafeAreaAdjustment = .never\n}"
+ },
+ {
+ "timestamp": "15:05",
+ "title": "Export Xcode skills for use in other tools",
+ "language": "swift",
+ "code": "xcrun agent skills export"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "TN3208: Preparing your app’s launch screen to meet App Store requirements",
+ "url": "https://developer.apple.com/documentation/Technotes/tn3208-preparing-your-apps-launch-screen-to-meet-app-store-requirements"
+ },
+ {
+ "title": "TN3210: Optimizing your app for iPhone Mirroring",
+ "url": "https://developer.apple.com/documentation/Technotes/tn3210-optimizing-your-app-for-iphone-mirroring"
+ },
+ {
+ "title": "Make your UIKit app more flexible",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/282/"
+ },
+ {
+ "title": "Adapting your app when traits change",
+ "url": "https://developer.apple.com/documentation/UIKit/adapting-your-app-when-traits-change"
+ },
+ {
+ "title": "Transitioning to the UIKit scene-based life cycle",
+ "url": "https://developer.apple.com/documentation/UIKit/transitioning-to-the-uikit-scene-based-life-cycle"
+ },
+ {
+ "title": "Automatic trait tracking",
+ "url": "https://developer.apple.com/documentation/UIKit/automatic-trait-tracking"
+ },
+ {
+ "title": "Human Interface Guidelines: Menus",
+ "url": "https://developer.apple.com/design/human-interface-guidelines/menus"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/278/4/8c3f2e61-52d3-4915-9543-96e2f13adc8b/downloads/wwdc2026-278_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/278/4/8c3f2e61-52d3-4915-9543-96e2f13adc8b/downloads/wwdc2026-278_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "343",
+ "year": "2026",
+ "title": "Explore advanced App Intents features for Siri and Apple Intelligence",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/343"
+ },
+ {
+ "id": "260",
+ "year": "2026",
+ "title": "Get the most out of Device Hub",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/260"
+ },
+ {
+ "id": "282",
+ "year": "2025",
+ "title": "Make your UIKit app more flexible",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/282"
+ },
+ {
+ "id": "10147",
+ "year": "2024",
+ "title": "Elevate your tab and sidebar experience in iPadOS",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10147"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:18.121Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-279.json b/data/wwdc/videos/2026-279.json
new file mode 100644
index 0000000..b3ba018
--- /dev/null
+++ b/data/wwdc/videos/2026-279.json
@@ -0,0 +1,124 @@
+{
+ "id": "279",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/279/",
+ "title": "Explore advances in RealityKit",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, my name is Dennis and I'm a software engineer on the RealityKit team. Welcome to my session: \"Explore advances in RealityKit\" where I'll be introducing you to some of the latest advancements in RealityKit.\n\nIn 2019 we introduced RealityKit, a framework that enables you to easily build 3D spatial experiences across a wide range of Apple platforms. With RealityKit you can build your apps and games once and deploy them to visionOS, iOS, iPadOS, macOS, and tvOS.\n\nAnd this year we are introducing a better way to build these apps and games with Reality Composer Pro 3.\n\nCheck out these sessions to learn more about Reality Composer Pro 3: from its fast, iterative scene editing tools to its powerful graph interfaces for modeling complex particle and character behaviors. And thanks to the amazing feedback we got from all of you we are adding exciting new features to RealityKit this year. These features will make it easier than ever to craft high-fidelity, immersive experiences. To start I'll go over the advancements we made to RealityKit's lighting and shadows, enabling more realistic effects that blend seamlessly into your world. Then I'll introduce RealityKit's navigation mesh as a way to help your players and NPCs navigate the worlds you have crafted.\n\nI'll show you how to build elaborate cloth simulations that can be used to dress your virtual furnishings and characters.\n\nIf you're not careful, utilizing these features can hinder performance. To help you tackle this I'll cover the tools available to you to both mitigate and monitor such issues.\n\nOnce we have a handle on performance I'll demonstrate how you can bring a piece of the real world into your apps and games with RealityKit's ability to render 3D gaussian splats. Finally, I'll talk about RealityKit's immersive audio features and how they can improve the spatial realism of your virtual worlds. Let's begin right away and go over what's new in RealityKit's lighting and shadows through a new game we have built: Chaparral Village. In this game, a mysterious village is transported into the player's world. They are then shrunk down by an owl and must navigate their way through this village in order to get to the alchemy area where they'll brew a number of potions under the owl's direction.\n\nLet's take a look inside the alchemy area of Chaparral Village.\n\nHmmm, this looks pretty good but it seems a little dark in some of the corners.\n\nHere, we can take advantage of RealityKit's support for lightmaps to enhance the lighting of the area's interior.\n\nUsing the light baker in Reality Composer Pro 3 I'll create an indirect lighting lightmap and apply it to the scene.\n\nThere, the corners and shadows have brightened as they're now correctly seeing contributions from reflected light. RealityKit's API supports attaching your own lightmap textures for: indirect lighting, ambient occlusion, and beauty. But to get the best experience I recommend using Reality Composer Pro 3's light baker to generate these instead. To learn more about light baking in Reality Composer Pro 3 please check out the session: \"Iterate your spatial scenes faster with Reality Composer Pro 3.\" Lightmaps help you render complex lighting effects, but only for static lighting. We can, however, bring one of these complex effects to dynamic lights with RealityKit's soft shadows. By default, the shadows in RealityKit have hard edges. This is accurate in some scenarios. For instance, if the light source is infinitesimally small. But this is no longer accurate when the light has some area. Now, the shadow cast exhibits softer edges as only some of the light is obscured in these regions, also known as the penumbra of the shadow. The size of the penumbra is influenced by the light's area. The greater the light's area, the greater the penumbra.\n\nHere is the hearth in the alchemy area. I am currently simulating the light coming from the hearth using a spotlight. Let's make the shadows cast by this spotlight soft.\n\nI'll start by getting the hearth's spotlight's shadow. I'll then update the shadow's lightSize variable. This represents the diameter, in meters, of the light. By default it is set to 0 producing a hard shadow. I'll update it to 0.7 meters to produce a nice soft shadow.\n\nI also have to set the shadow's quality. This variable controls the number of samples used to calculate the soft shadow. Setting the quality to high can produce more pleasing results but with a greater performance penalty. For this reason, I'll stick to medium as this still looks good at the viewing distance.\n\nNote that if I had set the quality to low, the shadow would be hard regardless of the light size. The quality must be set to either medium or high to achieve soft shadows.\n\nFinally, I add the shadow back to the hearthSpotLight entity.\n\nNow, if I apply the changes to the shadow, we can see the shadows have softened, especially the shadow cast by the cauldron.\n\nSo far all of the lighting I've shown you is casting onto virtual objects. But what if we wanted your lights to extent beyond the virtual world? To cast lights onto your world? Here is a planetarium projector I've built with RealityKit. As the projector spins, my real world environment is painted with virtual stars and nebulae. Notice how they realistically conform to the walls in my room. I was able to achieve this effect with two RealityKit features: projective textures and physical space lighting. Let's first take a look at projective textures. Imagine taking a flashlight and a piece of film. If I shined light through the film the image on the film will now appear on whatever surface the flashlight is pointing at. This effect is what RealityKit's projective texture feature emulates. With projective textures you can render various effects from the pattern caused by light shining through an intricate window or, when animated, the bright undulating caustics seen on the sea floor. For my virtual planetarium, the stars and nebulae are attached as projective textures to rotating spotlights. To build one of these spotlights I'll start by creating an Entity and attaching a SpotLightComponent to it.\n\nI'll make sure the color is white so I don't apply a tint to the projective texture. I'll tweak the other parameters as necessary for the room I am in.\n\nFor example, a larger room may need more intensity to make the projective texture visible on the walls. I'll then generate a stars and nebulae texture. This will be the texture that I project from the planetarium.\n\nFinally, I'll create and attach my SpotLight's projectiveTexture component and attach the texture I just generated.\n\nHere we can see the stars and nebulae projecting onto virtual walls.\n\nBut how do we get them to project onto the walls in my real room? For that, we must enable physical space lighting. This feature enables virtual lights to interact with system environments or the world around you using RealityKit's scene understanding mesh.\n\nCurrently, physical space lighting is only supported for spotlights and point lights.\n\nLet's enable this feature on our planetarium's spotlights.\n\nHere we have the spotLightEntity from earlier. To enable physical space lighting all we have to do is add the SpotLight's SurroundingsLight component. That's it.\n\nNow the the stars and nebulae project onto my real world room.\n\nWe have also enabled this physical space lighting effect in the alchemy area of the Chaparral village. Speaking of which, how does the player's character choose the path to take to the alchemy area? This path is determined using RealityKit's navigation mesh.\n\nLet me illustrate how such a mesh works with a simple example.\n\nImagine a scene where I start off on one side of the map and my goal is to make it to the flags on the other side. Seems simple enough. But oh no! Some obstacles have appeared in my way! I can use a navigation mesh to define the traversable parts of this scene, avoiding the dense forests. RealityKit can then use this navigation mesh to calculate a path to the flag. But what if I don't want to rule out the forests entirely? I can make my way through the forests, I'll just be a slower. We can reflect this in the navigation mesh by giving these regions a different traversal cost. This will reflect the lower speed I'll go at when going through the forest. Now, if I calculate a path it will take into account this cost and pick a new route for me. But what's this? A rift has broken up my scene and left the flag stranded. This has led to two regions represented with two disconnected navigation meshes. But not to worry, I can connect them using an off mesh connection, in this case a bridge. The placement of this connection, however, means that a new path must be calculated.\n\nTo use RealityKit's navigation mesh, you first need to define a NavigationMeshResource. This holds the geometric data for the navigation mesh, including labeled areas, custom flags for those areas, and the connection between areas. This can either be defined using the Swift API or in Reality Composer Pro 3. To learn how to build navigation meshes in Reality Composer Pro 3 check out the session: \"Supercharge your spatial workflows with Reality Composer Pro 3.\" This NavigationMeshResource is then fed into a NavigationComponent. This component has a filter that is used to define the cost of areas, and which areas to include or exclude given the area's flag. The NavigationComponent is then used by the NavigationController to calculate the path. Either synchronously or asynchronously.\n\nIn Chaparral Village the navigation mesh is queried in the navigate entity extension function.\n\nWithin navigate I first create a NavigationController. The controller requires an entity with a navigation component. I'll get this from the entity itself. I'll then use the async computePath function to get the path from the entity's current position to a desired position where the player tapped.\n\nIf the result is nil, then the NavigationController could not find a valid path and we return.\n\nIf this array is empty, then we have reached our destination and we return as well.\n\nOtherwise, this array holds a collection of path nodes representing the path the NavigationController computed.\n\nI'll iterate over these nodes so that I can determine the final path the entity should take.\n\nIf it's a node on the navigation mesh itself, I can just append its position to our path. If it's an off-mesh connection, then we have to traverse a ladder. I'll handle this situation separately.\n\nHere, we can see the player's character navigating the village with RealityKit's navigation mesh. Once they reach the top they must walk through two curtains that adorn the entrance to the alchemy area.\n\nThese curtains were built with RealityKit's advanced cloth simulation.\n\nIn RealityKit's cloth simulation the cloth is described through a mesh, where the vertices represent particles and the edges connecting the vertices represent springs.\n\nGiven a mesh with enough vertices, RealityKit can accurately simulate everything from the flow of this golden dress to these bed covers.\n\nSee how, as I pull off these covers, they realistically crease and fold, all in real time.\n\nTo use RealityKit's cloth simulation you need to add a cloth body component to your scene. This component represents cloths themselves. It contains a reference to its material properties and a cloth mesh resource that describes the layout of the particles and springs. You can also add a cloth collider component which represent the rigid objects that a cloth can collide with, like the bed or mannequin from earlier. Just as with the cloth body component, it contains a reference to its material properties and the geometry of the collider itself. To run the simulation you need to add the cloth simulation component. This component contains an array of materials that are referenced by the cloth bodies and colliders. Each material has a set of properties like spring stiffness and friction. The specific properties depend on whether it's describing a cloth or cloth collider material. The simulation itself also has a number of properties that effect all descendent entities involved in the cloth simulation. Some of these properties include which solver to use, the gravity to apply, and how big of a time step to take in the simulation.\n\nIn Chaparral Village, RealityKit's cloth simulation is used to adorn curtains to the entrance of the alchemy area. But how do we implement the hoops that keep the curtains from falling down? For that we use the custom curtain pin component.\n\nHere we iterate over pins, an array that holds a tuple of these custom components, and their corresponding entity.\n\nWe use this entity's position as the position at which we are pinning the curtain. Then we build a sphere to represent the size of the pin itself. The size is controlled through the custom component. We use the position and the sphere we just created to get an array of all the vertices that should be pinned or made immovable.\n\nWe then set these vertices to kinematic. Kinematic vertices can only be moved by an entity's transform and not by the cloth simulation itself. This will essentially keep them in place. And with that, we can pin the curtains to the alchemy area at the position of the hoops and keep them from falling over.\n\nAt this point I have gone over a number of features that, when not used with care, may come at a performance cost. To help tackle this I'll first cover a technique that can be used to improve performance and then how you can track a performance indicator of your apps and games so that they can adapt accordingly. Mesh level of detail refers to the process of rendering geometry at a lower detail such that the visual impact is negligible.\n\nTo demonstrate this I'll use the cauldron from the alchemy area in Chaparral Village. By convention, these so-called \"levels of detail,\" or LODs, start at index 0. Notice how the geometric complexity of the cauldron decreases as I go to LOD one, two, three, four, and finally five. At LOD 5 the cauldron looks pretty bad. However, if I scale it down as if the cauldron were very far away, I can now compare it to LOD 0. The difference is negligible and we require less compute to render the cauldron.\n\nLet's see how we can set up LODs in RealityKit.\n\nThe different LODs are specified as arrays of entities. In this example I'll have 3 different LODs each made up of one entity.\n\nThen I will create the entity that will hold the LODs themselves and switch between them.\n\nBut how does RealityKit know which LOD to use? For that I need to pick a switching algorithm.\n\nI will walk you through two of RealityKit's switching algorithms. The first one is based on the distance to the camera. The further away from the camera the entity is, the higher the LOD we can select. The second one is based on the screen area the entity takes up. The less area the entity takes up on screen, the higher the LOD we can select.\n\nHere, I am using the LevelOfDetailComponent's addByCameraDistance convenience function to setup camera distance based LODs.\n\nFor each LOD, I specify a max distance at which I will use this LOD. Once the entity goes beyond this distance, it'll switch over to the next LOD.\n\nFor the last LOD, I want to set the max distance to infinity to indicate that, no matter how far the entity is beyond the previous threshold, I will be using this LOD.\n\nIf I want to switch LODs based on screen area then I use the addByScreenArea convenience function. Here, I specify a minimum area as a fraction of the screen area. If the entity takes up less than this specified screen area, it'll switch over to the next LOD.\n\nUsing LODs in your apps and games is a great way to improve performance. However, it is also important to react in your apps and games when performance is becoming a problem.\n\nThis can be done by registering an observer on the thermalStateDidChange notification. This will allow me to know if the device's processor is running too hot. If a change in the thermal state occurred, I can query the current state. If it's nominal or fair, then my mitigation efforts succeeded and the app or game can keep running as is.\n\nHowever, if it's serious or critical, then I should take steps to improve performance, like making the LOD switching thresholds more aggressive, or lowering the quality of shadows. Keeping a handle on the performance of your apps and games is important as it ensures the comfort of your users and enables you to take advantage of other advanced RealityKit features.\n\nOne such feature is RealityKit's ability to render 3D gaussian splats.\n\n3D gaussian splats are a high performance, high quality technique to render volumetric data captured from the real world. With this technique, 3D scenes are represented as a collection of 3D gaussians.\n\nYou can think of these as ellipsoids with different levels of opacity. To render such a scene we would need to evaluate, at each pixel, a ray along all the gaussians. There are a number of optimizations that can be made when doing this, and if you use RealityKit's API, it will handle them for you. To see this API in action, download the gaussian splat sample from developer.apple.com.\n\nHere I have the sample running on an Apple Vision Pro. Notice the fine details of the potted succulent. We were able to capture the geometric complexity of the plants and the soil they sit in, and RealityKit was able to render them both flawlessly.\n\nRealityKit API does not assume a specific file format for gaussian splats. Instead, you have to provide buffers that describe the properties of the splats in a capture. Specifically: their position, scale, rotation, opacity, and their spherical harmonics.\n\nThe spherical harmonics allow you to control the color of an ellipsoid depending on the viewing direction. We also need to specify a degree for the spherical harmonics. This articulates the number of color variations as you move around the ellipsoid. For instance, a degree of 0 implies a solid color at all viewing directions. I can then assimilate all of these buffers into a BufferResource and create a GaussianSplatResource. From this resource, I'll create a GaussianSplatComponent. Then, to render the 3D gaussian splats, I will attach the component to an entity in my scene. With that, you are able to bring 3D gaussian splats into your virtual experiences, enabling you to bring high-fidelity captures of real world objects to your users. Finally, let's go over RealityKit's new immersive audio features, enabling enhanced realism for your Apple Vision Pro apps and games through sound.\n\nSpatial audio rendering is an important aspect of sound design for spatial computing.\n\nAccurate direction and timing of the direct path and the reflection path are required for a realistic spatial audio experience. As a person and the audio source move around the environment, the timing and orientation must be updated accordingly to maintain realism.\n\nThe geometry and materials of your environment have a big impact on how an audio source sounds.\n\nFor example, the same audio source will sound very different if it's in a small living room or a large museum.\n\nWe can use RealityKit to simulate the reflections and reverb of our environment using raytraced geometrical acoustics.\n\nFor example, in this kitchen and dining room scene, we can use RealityKit's custom reverb mesh to acoustically model the wood floors, plaster walls, and stone countertop. Depending on where a person and the audio source are in the scene, they will take on the reverb appropriate for their placement.\n\nTo show this feature in action we are releasing a sample that takes advantage of the custom reverb mesh. In this sample, you can hear a virtual band play as you navigate around a large museum environment. You can even control the sound coming from each instrument independently. Thanks to RealityKit's custom reverb mesh, the sounds emitted from these instruments realistically scatters around the museum before they hit your ears.\n\nThis can only truly be experienced on an Apple Vision Pro. Download the sample from developer.apple.com and give it a try! To create a custom reverb mesh, you start by defining the geometry of the scene with a ReverbMeshResource. This can be created from a mesh descriptor or mesh resource. But the easiest way to get started is with a shoebox, which is a box with faces pointed inward. I'll set it to be 5 meters wide, 4 meters tall and 6 meters deep.\n\nI'll then combine this mesh with the dryWall preset audio material to create a simulated reverb.\n\nFinally I'll use this reverb to create a reverb component and attach it to an entity in the scene. This will allow the reverb mesh to take effect. But I'm not limited to just the built-in preset materials. Let's see how I can create custom materials for my custom reverb mesh. I'll start by defining a thickCarpet material. I want this material to be more absorbent than the preset carpet material so I call scalingAbsorption and increasing the absorption a little bit for all frequencies. Next, I'll create a bookshelf material, but this time from scratch. This means we need to define both the absorption coefficients and scattering coefficients.\n\nThese define how much sound energy is absorbed or scattered at different frequencies. I'll start by setting the absorption coefficients for the 10-band center frequencies. But what if I only know the coefficients for specific frequencies? Then, I'll define the scattering coefficients for a few specific frequencies. RealityKit will then extrapolate and cover the entire audible frequency spectrum. Finally, I create the bookshelf material itself by composing the absorption and scattering data.\n\nPlease note that this only works in immersive spaces. If you are in a shared space the system's room-sense reverb geometry will be used instead. This is a reverb mesh that the Apple Vision Pro has built based on your real-world surroundings.\n\nI've covered a lot of material in this session, but this only scratches the surface of what RealityKit has to offer this year. There are many more features that we are also releasing this year: Like coordinated multi-source audio that enables precise, synchronizing audio playback across multiple entities; high quality character rendering, that provides subsurface scattering and advanced hair shaders to bring your characters to life; portal customizations, where you can create custom portal materials to alter a portal's opacity and shape; and much more.\n\nI recommend that you visit the Apple developer portal at developer.apple.com to download the samples from this session so that you can see some of these exciting new RealityKit features in action. And while you are there, make sure to check out Reality Composer Pro 3, a major new release of Reality Composer Pro that enables you to better take advantage of RealityKit's features. You can check out these sessions to learn more about Reality Composer Pro 3 and all its new capabilities. I can't wait to see what amazing spatial experiences you build with RealityKit. Thank you for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "4:02",
+ "title": "Soft shadows",
+ "language": "swift",
+ "code": "// Enable soft shadows for the hearth spotlight\n \nguard var shadow = hearthSpotlight.components[SpotLightComponent.Shadow.self] else {\n // handle error\n}\nshadow.lightSize = 0.7 // meters\n \nshadow.quality = .medium // or .high\n// shadow.quality = .low // will result in hard shadows\n \nhearthSpotlight.components.set(shadow)"
+ },
+ {
+ "timestamp": "6:13",
+ "title": "Projective textures",
+ "language": "swift",
+ "code": "// Create one of the planetarium spotlights\n \nlet spotLightEntity = Entity()\nspotLightEntity.components.set(SpotLightComponent(\n color: .white,\n intensity: intensity,\n innerAngleInDegrees: innerAngle,\n outerAngleInDegrees: outerAngle,\n attenuationRadius: attenuationRadius,\n))\n \nlet projectiveTexture: TextureResource = generateStarsAndNebulaeTexture()\nspotLightEntity.components.set(SpotLightComponent.ProjectiveTexture(\n texture: projectiveTexture\n))"
+ },
+ {
+ "timestamp": "7:13",
+ "title": "Physical space lighting",
+ "language": "swift",
+ "code": "// Enable physical space lighting\n \nspotLightEntity.components.set(SpotLightComponent.SurroundingsLight())"
+ },
+ {
+ "timestamp": "9:46",
+ "title": "Querying the navigation mesh",
+ "language": "swift",
+ "code": "// Querying the navigation mesh in Chaparral Village\n\nextension Entity {\n public func navigate(/* ... */) async {\n let navigator = try! NavigationController(entity: self)\n guard let result = await navigator.computePath(from: fromPosition, to: toPosition) \n else {\n return\n }\n if result.isEmpty {\n return\n }\n for node in result {\n switch node.category {\n case .meshPoint:\n finalPath.append(node.position)\n case .offMeshConnection:\n // handle ladders\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "12:51",
+ "title": "Pinning cloth to anchor points",
+ "language": "swift",
+ "code": "// Pin the curtains to the Alchemist's lab\n\nfor (pin, pinComponent) in pins {\n let position = pin.position(relativeTo: event.entity)\n let selectionSphere = ClothSphereShape(radius: pinComponent.radius)\n \n let vertices = clothMesh.vertices(in: .sphere(selectionSphere),\n center: position)\n clothBody.motionTypes.set(vertexIndices: vertices, value: .kinematic)\n}"
+ },
+ {
+ "timestamp": "14:42",
+ "title": "LOD by camera distance",
+ "language": "swift",
+ "code": "// Create entity with LODs\n \nlet lod0 = [ModelEntity(mesh: lodMesh0)]\nlet lod1 = [ModelEntity(mesh: lodMesh1)]\nlet lod2 = [ModelEntity(mesh: lodMesh2)]\n\nlet entity = Entity()\n\nLevelOfDetailComponent.addByCameraDistance(to: entity, levels: [\n (entities: lod0, maxDistance: 1.0 /* meters */), // highest detail\n (entities: lod1, maxDistance: 5.0), // medium detail\n (entities: lod2, maxDistance: .infinity), // lowest detail\n])"
+ },
+ {
+ "timestamp": "15:58",
+ "title": "LOD by screen area",
+ "language": "swift",
+ "code": "// Create entity with LODs\n \nlet lod0 = [ModelEntity(mesh: lodMesh0)]\nlet lod1 = [ModelEntity(mesh: lodMesh1)]\nlet lod2 = [ModelEntity(mesh: lodMesh2)]\n\nlet entity = Entity()\n\nLevelOfDetailComponent.addByScreenArea(to: entity, levels: [\n (entities: lod0, minArea: 0.2 /* fraction of screen area */), // highest detail\n (entities: lod1, minArea: 0.1), // medium detail\n (entities: lod2, minArea: 0.01), // lowest detail\n])"
+ },
+ {
+ "timestamp": "16:26",
+ "title": "Responding to thermal state changes",
+ "language": "swift",
+ "code": "// Respond to changes in device thermal state\n \nNotificationCenter.default.addObserver(of: ProcessInfo.self,\n for: .thermalStateDidChange) {_ in\n switch ProcessInfo.processInfo.thermalState {\n case .nominal, .fair:\n // Stay the course\n case .serious, .critical:\n // Improve performance by:\n // More aggressive LOD switching\n // Lower shadow quality\n }\n}"
+ },
+ {
+ "timestamp": "18:44",
+ "title": "Creating a Gaussian splat",
+ "language": "swift",
+ "code": "// Create Gaussian splat resource and component\n\nlet resource = try GaussianSplatResource.BufferResource(count: splatCount,\n position: positionBuffer,\n scale: scaleBuffer,\n rotation: rotationBuffer,\n opacity: opacityBuffer,\n sphericalHarmonics:\n (sphericalHarmonicsBuffer, degree))\n\nlet splatResource = GaussianSplatResource(resource)\n\nlet splatComponent = GaussianSplatComponent(splatResource)\n\nsplatEntity.components.set(splatComponent)"
+ },
+ {
+ "timestamp": "20:49",
+ "title": "Creating a custom reverb mesh",
+ "language": "swift",
+ "code": "// Create and use custom reverb mesh\n\nlet mesh: ReverbMeshResource = .shoebox(size: [5, 4, 6])\n \nlet reverb: Reverb = .simulated(mesh: mesh, materials: [.dryWall])\n \nentity.components.set(ReverbComponent(reverb: reverb))"
+ },
+ {
+ "timestamp": "21:33",
+ "title": "Creating custom reverb materials",
+ "language": "swift",
+ "code": "// Create custom materials for custom reverb mesh\n \nlet thickCarpet: Audio.Material = .carpet.scalingAbsorption {freq in 0.1 }\n \nlet bookshelf: Audio.Material\n \n// Absorption coefficients by center frequency:\n// 31.5Hz, 63Hz, 125Hz, 250Hz, 500Hz, 1kHz, 2kHz, 4kHz, 8kHz, 16kHz\nlet bookshelfAbsorption = Audio.Absorption(\n [0.10, 0.15, 0.28, 0.20, 0.15, 0.10, 0.10, 0.07, 0.07, 0.05])\n \n// Scattering coefficients for: 500Hz, 1000Hz, 4000Hz\nlet bookshelfScattering = Audio.Scattering([500: 0.5, 1000: 0.6, 4000: 0.7])\n \nbookshelf = .init(absorption: bookshelfAbsorption,\n scattering: bookshelfScattering)"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Gaussian splats on visionOS",
+ "url": "https://developer.apple.com/documentation/visionOS/gaussian-splats-on-visionos"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/279/4/ab575725-be7d-4348-a3ae-6595ef4070c4/downloads/wwdc2026-279_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/279/4/ab575725-be7d-4348-a3ae-6595ef4070c4/downloads/wwdc2026-279_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "252",
+ "year": "2026",
+ "title": "Design no-code games with Reality Composer Pro 3",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/252"
+ },
+ {
+ "id": "281",
+ "year": "2026",
+ "title": "Extend Reality Composer Pro 3 functionality with Xcode",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/281"
+ },
+ {
+ "id": "280",
+ "year": "2026",
+ "title": "Iterate your spatial scenes faster with Reality Composer Pro 3",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/280"
+ },
+ {
+ "id": "393",
+ "year": "2026",
+ "title": "Supercharge your spatial workflows with Reality Composer Pro 3",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/393"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:17.872Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-280.json b/data/wwdc/videos/2026-280.json
new file mode 100644
index 0000000..7c8de90
--- /dev/null
+++ b/data/wwdc/videos/2026-280.json
@@ -0,0 +1,56 @@
+{
+ "id": "280",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/280/",
+ "title": "Iterate your spatial scenes faster with Reality Composer Pro 3",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hi everyone! I'm JP, an engineer on Reality Composer Pro here at Apple. In this session I want to talk about how you can iterate on your spatial content faster than ever using Reality Composer Pro 3. Since the launch of Apple Vision Pro, we have seen remarkable spatial experiences brought to the platform. Reality Composer Pro has been at the center of bringing that amazing content to visionOS, and it's inspiring to see what this community has been building. Our goal is to continue to give you more and better tools to bring your ideas to life with as little friction as possible. With that in mind, we are introducing some brand new capabilities that will help you realize even your most ambitious spatial projects! Reality Composer Pro 3 is built from the ground up for fast, iterative and collaborative workflows. It helps you get further along in your development without the need to touch Xcode. In this session, I'll take you through some of these brand-new capabilities, like Live preview, Lightmaps, and the Reality Composer Pro Assistant, which can generate 3D content using AI. I'll show you the process of adding a few small features to the interactive game Chaparral Village. I'll start with a quick overview of the editor core functionalities. I'll cover how to use entities and components to build up a scene. I'll introduce the new prototype and instancing system, designed to help you efficiently organize, reuse and manage your assets. I'll demonstrate how you can preview and even author your content directly on the Vision Pro. I'll use Lightmaps to bake the indirect lighting of my scene. And finally, I'll introduce you to the Reality Composer Pro Assistant, and how you can leverage it to add content to your world. First things first. Reality Composer Pro 3 is no longer available as an Xcode developer tool. You can now download it from developer.apple.com and simply launch it straight from your Applications folder. If you would like to familiarize yourself with the basics of using the Reality Composer Pro Editor, I would recommend checking out the session \"Meet Reality Composer Pro\" from WWDC23. Let's start with a quick overview of the project I'll work on today. This is the Alchemy Area from the Chaparral Village. All objects in this scene were modeled in Blender, imported as USD files, and then laid out in Reality Composer Pro. To get a better look, I'll use the Focus Mode from the View menu and explore the scene a bit.\n\nThis is a gorgeous scene. I'll start by adding an asset to my project. To do so, I'll use the import asset icon available in the Project Browser and select the Cauldron USD file I have on my desktop.\n\nWhen a USD file is imported, its content gets organized and optimized into an import bundle. Once imported, I can expand the bundle to inspect it.\n\nWithin the bundle, I see the imported geometry, materials, textures, and more. The cauldron asset looks good. Now, to bring it into the lab, I'll just drag and drop the bundle into the viewport.\n\nThis added asset is shown in the hierarchy view and it's called an entity. It has a Transform Component that's shown in the inspector panel on the right. Entities and components are the core building blocks of everything you create in Reality Composer Pro 3. Let's go through them in more detail. In the hierarchy panel I can see a list of all the entities that make up my scene, and they can be re-ordered and nested as needed. I'll expand the fireplace and drag the cauldron underneath it.\n\nI'll select the cauldron entity and position it at the right spot and angle by adjusting the values within the Transform Component.\n\nI can choose to add more components to an entity using the Add Component button.\n\nYou can add lights, physics, audio, and a lot more to your entity using this panel.\n\nI'll use a couple of these components to create an interesting visual effect for my scene. First, I'll create a child under the Table entity. I will use the context menu and choose 'Add Child Entity'. I will call it 'Magic Effect'.\n\nTo frame an entity, you can always press 'f'.\n\nTo the Magic Effect entity, I'll add another child called 'Glow'.\n\nand I'll add a simple Point Light component.\n\nI'll adjust the position of the light and tweak its attenuation, adjust the color, and adjust the intensity.\n\nI'll also add the new Compute Simulation component to the Magic Effect.\n\nIn the inspector panel, I'll choose the Magic Graph that I created for my project. I can do that using the Compute Graph picker. Notice that it shows all available compute graphs in this project. I have a Magic Graph and a Brewing Graph available. I'll use the Magic Graph for now, and keep the Brewing Graph for later. Compute Graph makes GPU programming accessible to anyone. Its node-based graphs lets you build anything, from simple particle systems to complex fluid simulations. For a deeper dive on this topic, watch the session \"Supercharge your spatial workflows with Reality Composer Pro 3\". You'll notice that the Compute Graph isn't visible right now. That's because this graph only runs during the simulation stage. To test it, I'll use the launch control and run my game by pressing the Play button. This is where Reality Composer Pro 3 really starts to shine.\n\nThe Alchemy Area is now running and I can see the Compute Graph being simulated. Let me get a closer look.\n\nI'll dock the simulation tab next to the scene tab.\n\nThis allows me to keep authoring my content even as the game is running. I'll place this Magic Effect into the bowl on the table and tweak the graph's twist amount to my liking.\n\nThe simulation tab allows me to make tweaks rapidly without any deployment process getting in the way. From physics simulation to script graphs, to animations, everything you author in Reality Composer Pro can be previewed in real time in the simulation tab, greatly reducing the friction between you and the final experience. Next, I'll cover a new addition to Reality Composer Pro called 'prototypes', which can be used to create powerful and reusable objects. To turn an entity into a prototype, you can drag it from the hierarchy tab directly into the Project Browser.\n\nThis creates a new prototype asset. I can then instantiate this new prototype by dragging it into the viewport.\n\nI'll rename this new instance 'Brewing Effect'.\n\nNotice that now I have two instances using the Magic Effect prototype. I can provide overrides to these instances, allowing me to customize them. First, I'll change the effect of the brewing instance to the Brewing Graph I mentioned earlier...\n\nand adjust the color, attenuation, and falloff of the Glow entity.\n\nUh oh... that looks bad. I can always reset an override back to its source value by choosing Reset in the context menu. I'll right click on the Attenuation Falloff property and reset it.\n\nOk, that's much better. I'll keep the original falloff value. With prototypes, you edit your content in one place and the system handles the rest. You can instantiate a prototype multiple times and override any of those instances individually. If you don't like those overrides, you can reset them to their original values or you can even propagate the overrides back to the source. Nothing is ever permanently changed unless you want it to be. Next, I will show you another cool new feature in Reality Composer Pro called Live Preview. Since we're building this experience for the Vision Pro, I can target a simulation to be played on any Vision Pro device currently connected to my Mac. I'll use the launch control panel to start a Live Preview session which will ship later this year. This will open the companion app on visionOS. Now I can continue to author in Reality Composer Pro and see the updates reflected instantly. Notice how the blue fill light has the new physical space lighting feature enabled. Authoring an effect like this on device allows me to get an instant feel of its impact in a spatial experience. This way of live previewing dramatically cuts down on iteration times. It removes any guesswork from the process of authoring. What you see is truly what you get. This is coming along nicely but after these lighting edits, I'm noticing that the scene no longer feels quite right. This is because the indirect lighting previously generated for this scene no longer matches the new lighting. To fix this, I'll use the new Lightmaps in Reality Composer Pro. Indirect lighting captures the way light bounces around a scene and contributes to areas not directly visible by any single light. The space underneath this table for instance has no direct lighting to it but by simulating indirect lighting, we can capture the subtle light that still reaches that area, however faint it may be. For example, in my scene, most of the Alchemy Area isn't directly lit by the fireplace. But Lightmaps helps fill in those darker areas with soft bounced light, greatly improving the overall look of the scene. Simulating indirect lighting can be costly but as the lights in the Alchemy Area don't move, I can use the new lightmapping component to pre-calculate the indirect lighting term and save the results to a texture called a Lightmap. The Alchemy Area entity has a Lightmap component attached to it.\n\nHere I can control what lighting term gets baked and fine-tune quality settings. I'll change the quality of the Lightmaps under Bake Settings from low to high.\n\nTo preview the output of the baked lights, I'll open the Lightmap Preview tab. I can do this from the Tab menu.\n\nThis allows me to see in real time how much the indirect lighting impacts the scene. I'll tweak the Lightmap settings.\n\nThe preview tab lets me get a clear picture of the final result before committing to a full bake. I'm pretty happy with those settings, and I'm ready to regenerate the scene's Lightmap. While my lights are getting baked, let me go through the different lighting terms that the Lightmap component supports. In addition to the Indirect Lighting, Reality Composer Pro can also generate Ambient Occlusion and Beauty Lightmaps. Ambient Occlusion represents the visibility of each point in a scene to its surroundings. And Beauty represents the final color of each point in a scene taking into account both indirect and direct lighting. Let me go check on my Lightmaps. And it's done! My Lightmaps are completely baked and my scene looks beautiful. Before I wrap up, I'd like to add one more thing to this scene. I was thinking it would look great if we had a few more items on the work bench. For this I'll use the all new Reality Composer Pro Assistant. The new AI Assistant is always available from the right panel.\n\nFrom there I can simply prompt the assistant for help.\n\nThat looks great. Let's also add a few candles.\n\nAnd my scene is looking complete. The Reality Composer Pro Assistant uses powerful generative models to craft 3D objects and materials on demand, letting you iterate faster, experiment freely, and turn ideas into reality with ease. It is also ready to answer any Reality Composer Pro questions you might have! I've covered a lot of ground today, and it only scratches the surface of what Reality Composer Pro 3 has to offer. To continue your journey with Reality Composer Pro, first, download it from developer.apple.com. While you're there, be sure to explore the available sample projects. There is a lot more in Reality Composer Pro that I wasn't able to cover in this session. To learn more, I recommend that you check out the Reality Composer Pro sessions. I speak on behalf of the whole team when I say we cannot wait to see what you will create with it. Thanks for watching!",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/280/4/0f02d465-7874-4ac3-aac3-b1b792efecd3/downloads/wwdc2026-280_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/280/4/0f02d465-7874-4ac3-aac3-b1b792efecd3/downloads/wwdc2026-280_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "252",
+ "year": "2026",
+ "title": "Design no-code games with Reality Composer Pro 3",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/252"
+ },
+ {
+ "id": "279",
+ "year": "2026",
+ "title": "Explore advances in RealityKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/279"
+ },
+ {
+ "id": "281",
+ "year": "2026",
+ "title": "Extend Reality Composer Pro 3 functionality with Xcode",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/281"
+ },
+ {
+ "id": "393",
+ "year": "2026",
+ "title": "Supercharge your spatial workflows with Reality Composer Pro 3",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/393"
+ },
+ {
+ "id": "10083",
+ "year": "2023",
+ "title": "Meet Reality Composer Pro",
+ "url": "https://developer.apple.com/videos/play/wwdc2023/10083"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:17.933Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-281.json b/data/wwdc/videos/2026-281.json
new file mode 100644
index 0000000..a8ee830
--- /dev/null
+++ b/data/wwdc/videos/2026-281.json
@@ -0,0 +1,112 @@
+{
+ "id": "281",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/281/",
+ "title": "Extend Reality Composer Pro 3 functionality with Xcode",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Niklas. In this session I'll show how you can extend Reality Composer Pro with plugi-ns to make it possible for artists and content creators to work with assets directly in the editor, iterate quickly, and build lots of interesting 3D apps and games. This is a feature that will be available later this year. This is Reality Composer Pro 3. The latest version of Apple's game and 3D content editor for RealityKit. This updated version comes with support for larger scenes, artist friendly workflows for quick iterations over content, and the ability to preview scenes in headset. You can learn more about how to use Reality Composer Pro 3 in the session, \"Iterate your spatial scenes faster with Reality Composer Pro 3.\" Another great session to check out is: \"Design no-code games with Reality Composer Pro 3.\" That session shows how to build a game without writing any code by using Reality Composer Pro's visual scripting tool Script Graph. In this presentation, I want to show what you can do with code. I'll show how you can build plug-ins in Xcode that expose your project specific content to Reality Composer Pro 3. Your custom components will appear inside the editor, where they can be tweaked by artists and designers. For example, artists can change the water level or the rotation speed and see the cauldron react in real time, without having to build and deploy an app. I'll first go through the general mechanism for extending the editor. Then, I'll show how to use that to get custom components and custom systems running in the editor. I'll also show how you can add your own custom animations to the sequencer timeline. And finally, I'll show how to create your own custom nodes for the Script Graph. I'll start by looking at the general mechanism for extending Reality Composer Pro and how that works when you're working on a team. I will be working in the Chaparral Village game. This game was made by a team of developers, artists, and designers. The game has both a Reality Composer Pro project and an Xcode project. The editor project is typically used by the artists and designers to create the content of your experience, while the Xcode project is used by engineers to build the final app as well as the plugin that lets artists create custom data in the editor. The Reality Composer Pro project and the Xcode project are linked together so that you can launch the app directly from within the editor. You can setup this linking for your own projects using the simulation bar at the top of the window. The editor project and the Xcode project live together in the same git repository. Artists and engineers make changes locally and then commit and push to share them with the rest of the team. When you import a file into Reality Composer Pro 3, it gets converted to an internal data format and saved on disk as JSON files. You can use git's built-in tools to merge your changes, but the editor also comes with a custom merge tool that can merge files with fewer conflicts than standard git merge. To get data from Reality Composer Pro 3 into an app, you export it as a Reality File, the serialization format for RealityKit. Here's an illustration of how all of this fits together. The Xcode project is used to build the final game app as well as the plug-in framework for Reality Composer Pro 3. Reality Composer Pro 3 is used to set up the different 3D scenes and objects in the game. The RCPCustomComponents.framework makes the components and systems that the developers have created available in the editor so that the artists and level designers can edit them, see how they work, and improve the content interactively. Finally, the scenes created in Reality Composer Pro 3 are exported to a Reality File which is linked into and loaded by the app. When the code is updated, the developers build a new plug-in framework for everybody on the team to use as well as a new app. When the content is changed, a new reality file is exported to test in the final app. The Xcode project has been set up with two different schemas. ChaparralVillage which builds the actual app and RCPCustomComponents which builds the plugin for Reality Composer Pro 3. All the custom components and custom systems in the project are shared between these two schemas. Let's see how that works. I'll use the plug-in system to create a custom component and expose it to Reality Composer Pro. I want to work with the artists to create an effect for this cauldron. In the game, the cauldron will be used for mixing potions and I want to be able to control the water level so that it can go up and down as ingredients are added. I also want to add a swirling effect, so that you can see the water rotating when the potions are mixed, but let's start with just setting the water level. I could do this with a Script Graph. It would look something like this. On an update, event I get the water surface entity, and move it to a specific position. But if I wanted to do something more advanced, it might make sense to use a custom component instead of a Script Graph. Script Graphs and custom Swift code can do similar things and often it's a personal preference what you want to use. But really big Script Graphs can be hard to maintain, so that might be a good reason to switch to code. Code also lets you interact with other Apple APIs that are not available in Script Graph such as SwiftUI. For the cauldron, I ultimately want the water level to tie into other systems, such as ingredients floating on the surface, so writing code makes sense. To do this, I start with creating a simple component to hold the water level of the cauldron. It has a single property that stores the water level. In addition to the Component protocol, the Cauldron also implements Codable. This is needed to be able to represent this Component in Reality Composer Pro 3 and serialize it to Reality Files. For more advanced components that have runtime properties that shouldn't show up in the editor, it might also make sense to implement CodingKeys, but for this simple component we just want to serialize all properties, so that's not needed. Next, I'll create a custom system for setting the water level. In the system update I find the entities that have the Cauldron component. Then, I look for the water mesh child entity and adjust the position of the water mesh based on the water level set in the Cauldron component. After that, I need to make sure that Reality Composer Pro 3 can use my custom component and the corresponding system. To do this, I need to create a plugin class that implements the RealityComposerProPlugin protocol. This protocol comes from the RealityComposerPro Swift package. This package is automatically added to your Xcode project when you link it in the editor using the Run With Xcode option in the simulation toolbar. In the setup method of this class, I use the context I get from Reality Composer Pro to register my components and systems. This makes these components and systems usable by the editor. I also need to make sure that Reality Composer Pro 3 can create the plug-in. I do that by implementing a createRealityComposerProPlugin() function that creates and returns my plug-in. You'll notice that this function returns the class as a raw pointer, this is because I need this function to be exported in the DLL interface. I also need to mark it as a C function and give it an exported name so that it can be found by the plug-in loader. Let's see how all of this works in practice. First, I'll create the Cauldron component with the waterLevel property.\n\nThen, I'll add the system that positions the water plane based on the property's value.\n\nFinally, let's register the component and the system with Reality Composer Pro.\n\nNow, I can build the plug-in scheme to create the dynamic library.\n\nNow I can open the project in the editor. Since this project has plug-ins, Reality Composer Pro asks me if I trust it. I select Trust to load the plug-ins.\n\nOnce I've accepted to load the plug-in, the editor will show me the component that was imported. If I go to the project's build settings, the imported components and systems show up there too. I can also use this settings panel to specify a custom plug-in directory.\n\nThe imported components are found in the Custom Components folder in the project. Now that I've imported the component and Reality Composer Pro 3 knows about it, I can add it to my Cauldron entity.\n\nAs I change the water level property in the editor, you can see the surface reacting. My custom system is running inside the editor through the plug-in mechanism and changing the water level based on the property value. Artists and designers can use this to fine tune properties in the editor without having to rebuild and relaunch the app.\n\nIf I want to debug my system, I can set a breakpoint in the code and attach to the editor application.\n\nWhen it runs my code, it will stop in the Xcode debugger, and I can check my logic.\n\nNow that I have the basic water surface working, I want to take it to the next level by adding a swirling effect. As the player stirs the cauldron to mix the potion I want the surface to bend into a vortex shape. To change the surface shape, a tech artist has built this vortex shader using the Shader Graph system in Reality Composer Pro 3. The Shader Graph has parameters for things like rotation speed and it will create the vortex shape based on those parameters. I want to be able to control these parameters from my custom component. To do that, I start by adding some properties to guide the shape of the vortex. And then, I need to modify my cauldron system from before so that it propagates these parameters to the Shader Graph. First, I retrieve the Shader Graph material. Then, I use a helper function to compute the shape of the water surface based on the Cauldron properties. I use the computed shape to set the Shader Graph parameters. And finally, I assign this material back on the model. Let's see how this works in practice. First, I'll add my new properties to the Cauldron.\n\nThen, I'll write a function to compute the water surface.\n\nFinally, I'll update the system so that it sets the shader parameters and then rebuild the plug-in.\n\nWhenever I rebuild the plug-in, I need to restart Reality Composer Pro to get the changes in. Reality Composer Pro will again ask me if I trust the project. If you don't want to see this dialog anymore you can check the \"Don't ask again\" checkbox.\n\nNext, a dialog will appear that shows me the changes to my custom components. These changes all look good to me, so I'll accept them.\n\nIf I go back to the Cauldron component, I will see the new properties there.\n\nLet me set it up with some default values for the water level and vortex coefficient.\n\nNow, let me try some different values for the rotation speed. You can see that as I increase the rotation speed, the vortex gets deeper.\n\nNext, I want to talk about how to use plug-ins in the animation system. The animation sequencer supports custom animation actions that can be defined in the plugin and then added to the sequencer timeline. I want to create a custom action that sets the water level of the cauldron. To do this, I need to implement the EntityAction protocol. And to be saved in a Reality File, it needs to be Codable. My action takes two parameters: a start level and an end level for the water surface so that it can animate it between these two values. For the EntityAction protocol I also need to return the animated value type as a Transform. This is needed to access my entity in the animation executor. I also to need to write the code that executes the action and updates the water level when the animation runs. To do this, I create a static subscribe() function in my entity action that I will call from my plug-in loading code. In this function, I use the subscribe method in EntityAction to subscribe to the .updated event that gets called when RealityKit runs animations. I will use this to perform my custom animation action. I'll get the elapsed animation time as a normalized number between 0 and 1. Then I compute the current water level by using the normalized time to interpolate between the start and end water levels. I get the cauldron component from the entity and update its water level. And finally, I set the modified cauldron component back on the entity. The final piece of the puzzle is to register this custom animation with Reality Composer Pro 3. Just as with custom components and custom systems, I need to register the action with the context for the editor to be aware of it. I also need to call the subscribe function I created earlier so that the action executes on animation updates. Let's see how this works in practice. First, I will define my custom entity action.\n\nThen, I'll add the code that implements this action.\n\nFinally, I'll register this action with the editor and rebuild the plug-in.\n\nWhen I restart the editor and open the project it will show me that a new action was imported.\n\nLet's create an animation that uses this action. First, I'll create a new sequence in the editor.\n\nThen, I'll open the sequence and set the root entity of my animation to be a scene that contains the cauldron. I have prepared a scene called CauldronWorld for this purpose.\n\nNext, I'll add an animation track to the sequence and pick the cauldron as the entity I want to use for this track.\n\nNow I can drag the SetWaterLevelAction from the left panel into the timeline of my track to create an action that runs on the cauldron. I'll expand this action in the inspector so that I can set its parameters: the start and stop values for the cauldron's water level. I'll set the start value to 0.3 and the stop value to 0.5. Now, I can play back the animation and see the water level change based on my custom action. Artists and designers can use Script Graphs to create gameplay and interactivity in Reality Composer Pro 3 projects without having to write any code. The built in script nodes go a long way but if you want to take this to the next level you can use a plug-in to add custom nodes that the designers can make use of in their Script Graphs. The quickest way to expose a custom component to Script Graphs is to use the @Scriptable macro. For this, I first need to import the RealityKitScripting and RealityKitScriptingMacros modules. This is where this macro is defined. Just as the RealityComposerPro Swift package, this package is automatically set up for you when you create your Xcode project from within Reality Composer Pro. Then, I simply tag my component struct with the @Scriptable macro. This will expand to a schema variable that describes the component so that I can register it with the scripting system. Like all other registrations, this happens in the setup function for the plug-in. Scripting modules need to be registered on the main thread. I first create a scripting configuration for my project. In the initializer for the configuration, I need to return a list of all my scripting modules. I'll create a single module that holds my Cauldron schema. This schema is generated by the @Scriptable macro. And then I'll add my configuration to RealityKitScripting. Let's see how this works in practice. First, I'll import the scripting modules and add the @Scriptable macro to the Cauldron component.\n\nThen, I'll add the code for registering the scripting module with the editor and rebuild the plug-in.\n\nLet's create a Script Graph that makes use of the new custom nodes for the Cauldron component. I'll start by adding a Scripting component to my Cauldron entity.\n\nI double click the new component to open the Script Graph editor. Let's make it so that when the user presses a key on the keyboard, the water level changes. I'll start by adding an update node. This node fires every time the scene updates.\n\nNext, I'll add an If node and hook it up to a keypress so that the node fires whenever the key is pressed.\n\nI'll hook up the true connector from the If node to set the water level of the cauldron.\n\nNow, when the \"a\" key is pressed, the water level of the cauldron will be set to 0.25. I'll hook up another key to set a different water level. To do that, let me just copy and paste the whole graph.\n\nThen, in the copy, I'll change the key to \"z\" and the water level to 0.5.\n\nI can test this out by opening a simulation view. As I press the \"a\" and \"z\" keys the water level goes up and down.\n\nThat was a lot of ground I covered in this session. I showed you how to create some simple plug-ins using Xcode and extend the functionality of the editor to work with your apps data and even run your code inside the editor. To learn more, I suggest checking out the \"Explore advances in RealityKit\" session to find out about the latest additions to RealityKit. I also recommend going through the \"Supercharge your spatial workflows with Reality Composer Pro 3\" session that covers how to improve your productivity with the editor. I look forward to see what amazing experiences you will create with the new Reality Composer Pro 3. Thanks for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "6:08",
+ "title": "Cauldron component",
+ "language": "swift",
+ "code": "// Add a component to represent the water level\n\nimport RealityKit\n\npublic struct Cauldron: Component, Codable {\n public var waterLevel: Float\n\n enum CodingKeys: CodingKey {\n case waterLevel\n }\n}"
+ },
+ {
+ "timestamp": "6:42",
+ "title": "CauldronSystem",
+ "language": "swift",
+ "code": "// Add a system to control the water level\n\nimport RealityKit\n\npublic struct CauldronSystem: System {\n let query = EntityComponentQuery(Cauldron.self)\n public init(scene: Scene) {}\n\n public func update(context: SceneUpdateContext) {\n for (entity, cauldron) in context.entities(matching: query) {\n guard let water = entity.findEntity(named: \"Cauldron_Water_mesh\")\n else { continue }\n water.setPosition(SIMD3(0, 1, 0) * cauldron.waterLevel, relativeTo: entity)\n }\n }\n}"
+ },
+ {
+ "timestamp": "7:00",
+ "title": "RCPCustomComponentsPlugin",
+ "language": "swift",
+ "code": "// Make sure that Reality Composer Pro 3 knows about the Cauldron and CauldronSystem\n\nimport RealityComposerPro\n\nfinal class RCPCustomComponentsPlugin: RealityComposerProPlugin {\n public func setup(context: any RealityComposerProContext) {\n context.registerComponent(Cauldron.self)\n context.registerSystem(CauldronSystem.self)\n }\n}\n\n@_cdecl(\"createRealityComposerProPlugin\")\npublic func createRealityComposerProPlugin() -> UnsafeMutableRawPointer {\n return RCPCustomComponentsPlugin().passRetained()\n}"
+ },
+ {
+ "timestamp": "10:49",
+ "title": "Cauldron component with vortex properties",
+ "language": "swift",
+ "code": "// Properties to control water surface\n\nimport RealityKit\n\npublic struct Cauldron: Component, Codable {\n public var waterLevel: Float\n public var rotationSpeed: Float\n public var minWaterLevel: Float\n public var maxWaterLevel: Float\n public var vortexCoeff: Float\n}"
+ },
+ {
+ "timestamp": "11:05",
+ "title": "CauldronSystem update with ShaderGraph",
+ "language": "swift",
+ "code": "public func update(context: SceneUpdateContext) {\n for (entity, cauldron) in context.entities(matching: query) {\n guard let water = entity.findEntity(named: \"Cauldron_Water_mesh\") else { continue }\n water.setPosition(SIMD3(0, 1, 0) * cauldron.waterLevel, relativeTo: entity)\n\n guard var model = water.components[ModelComponent.self] else { continue }\n guard var mat = model.materials.first as? ShaderGraphMaterial else { continue }\n let surface = computeSurface(cauldron: cauldron)\n try? mat.setParameter(name: \"Level Radius\", value: .float(surface.levelRadius))\n try? mat.setParameter(name: \"Lowest Point\",\n value: .float(cauldron.waterLevel - surface.lowestPoint))\n try? mat.setParameter(name: \"Height Change\", value: .float(surface.heightChange))\n try? mat.setParameter(name: \"Level Coeff\", value: .float(surface.levelCoeff))\n try? mat.setParameter(name: \"Is Level\", value: .bool(surface.isLevel))\n model.materials[0] = mat\n water.components.set(model)\n }\n}"
+ },
+ {
+ "timestamp": "13:25",
+ "title": "SetWaterLevelAction",
+ "language": "swift",
+ "code": "// Custom action for setting the water level of the Cauldron\n\nimport RealityKit\n\npublic struct SetWaterLevelAction: EntityAction, Codable {\n // Parameters for the action\n public let startWaterLevel: Float\n public let endWaterLevel: Float\n\n // Required by EntityAction protocol\n public var animatedValueType: (any AnimatableData.Type)? { Transform.self }\n}"
+ },
+ {
+ "timestamp": "14:05",
+ "title": "SetWaterLevelAction subscribe",
+ "language": "swift",
+ "code": "extension SetWaterLevelAction {\n static func subscribe() {\n Task { @MainActor in\n SetWaterLevelAction.subscribe(to: .updated) { event in\n let normalizedTime = (event.playbackController.time - event.startTime) /\n event.duration\n let action = event.action\n let currentLevel = action.startWaterLevel +\n Float(normalizedTime) * (action.endWaterLevel - action.startWaterLevel)\n guard let entity = event.targetEntity else { return }\n guard var cauldron = entity.components[Cauldron.self] else { return }\n cauldron.waterLevel = currentLevel\n entity.components.set(cauldron)\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "14:56",
+ "title": "RCPCustomComponentsPlugin with action",
+ "language": "swift",
+ "code": "// Make sure that Reality Composer Pro 3 knows about the SetWaterLevelAction\n\nimport RealityComposerPro\n\nfinal class RCPCustomComponentsPlugin: RealityComposerProPlugin {\n public func setup(context: any RealityComposerProContext) {\n context.registerComponent(Cauldron.self)\n context.registerSystem(CauldronSystem.self)\n\n context.registerAction(SetWaterLevelAction.self)\n SetWaterLevelAction.subscribe()\n }\n}\n\n@_cdecl(\"createRealityComposerProPlugin\")\npublic func createRealityComposerProPlugin() -> UnsafeMutableRawPointer {\n return RCPCustomComponentsPlugin().passRetained()\n}"
+ },
+ {
+ "timestamp": "17:32",
+ "title": "Cauldron with @Scriptable macro",
+ "language": "swift",
+ "code": "// Expose Cauldron to Script Graphs\n\nimport RealityKit\nimport RealityKitScripting\nimport RealityKitScriptingMacros\n\n@Scriptable\npublic struct Cauldron: Component, Codable {\n public var waterLevel: Float\n public var rotationSpeed: Float\n public var minWaterLevel: Float\n public var maxWaterLevel: Float\n public var vortexCoeff: Float\n}"
+ },
+ {
+ "timestamp": "18:08",
+ "title": "Register scripting module",
+ "language": "swift",
+ "code": "// Register scripting module\n\npublic func setup(context: any RealityComposerProContext) {\n context.registerComponent(Cauldron.self)\n context.registerSystem(CauldronSystem.self)\n\n context.registerAction(SetWaterLevelAction.self)\n SetWaterLevelAction.subscribe()\n\n Task { @MainActor in\n let config = RKS.Configuration(id: \"ChaparralVillage\")\n .onInitialize { _ in\n [\n Module(\"ChaparralVillage\") {\n Cauldron.SchemaProvider.schema\n }\n ]\n }\n try! RKS.addConfiguration(config)\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/281/6/1aef704f-ccc6-4c1d-b7b7-94da42d29609/downloads/wwdc2026-281_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/281/6/1aef704f-ccc6-4c1d-b7b7-94da42d29609/downloads/wwdc2026-281_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "252",
+ "year": "2026",
+ "title": "Design no-code games with Reality Composer Pro 3",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/252"
+ },
+ {
+ "id": "279",
+ "year": "2026",
+ "title": "Explore advances in RealityKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/279"
+ },
+ {
+ "id": "280",
+ "year": "2026",
+ "title": "Iterate your spatial scenes faster with Reality Composer Pro 3",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/280"
+ },
+ {
+ "id": "393",
+ "year": "2026",
+ "title": "Supercharge your spatial workflows with Reality Composer Pro 3",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/393"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:17.990Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-282.json b/data/wwdc/videos/2026-282.json
new file mode 100644
index 0000000..e221fb2
--- /dev/null
+++ b/data/wwdc/videos/2026-282.json
@@ -0,0 +1,129 @@
+{
+ "id": "282",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/282/",
+ "title": "Discover the Spatial Preview framework",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, my name is Quincy German, and I'm a software engineer on the Vision OS team. One of the features I use the most on Apple Vision Pro is Mac Virtual Display, which lets me work on my Mac through a virtual screen provided by Vision Pro. In Mac OS and Vision OS 27, it's even easier to preview content from Mac to Vision Pro. I'm excited to present Spatial Preview. A framework that lets people leverage the spatial computing capabilities of Vision OS when working with content on their Mac. You can check out the Spatial Preview Framework in Action with the Preview app for Mac OS. alongside other features to help people work with spatial content, including 3D content editing, photorealistic rendering, camera viewpoints, and spatial media output like Apple immersive video frames and spatial photos Using Mac Virtual Display, people can share content seamlessly to Vision Pro. On Vision OS, the QuickLook app receives this content and now allows them to work immersively, like moving around to the different cameras setup for this living room scene. These are tools that are very useful to get a sense of scale and layout of a 3D design, like this one. With Spatial Preview, people can now extend content from their Mac into the world around them. And these features aren't just available in the preview app. They're also exposed as an API for you. So you can augment new or existing macOS applications to take advantage of the power of Vision Pro.\n\nThese tools enable dynamic workflows with spatial content, including live synchronization and editing across devices With real-time synchronization, apps like Cinema 4D and SketchUp are transforming the creative process, unlocking real-time, collaborative 3D workflows, like iterating on material changes live. And in this session, I'm going to cover how you can too.\n\nI'll start with an overview of the framework, designed for developers building content creation tools or any app that works with spatial content.\n\nNext, I'll walk through an example workflow showing how to share and update documents like Apple Immersive Video Frames in place.\n\nLastly, I'll show how you can create apps that fully immerse people in the 3D content they're working on, live, by using Universal Scene Description, which is a format used to describe 3D scenes. By the end of this session, you'll have everything you need to leverage the spatial preview framework and enable people to work with content from their Mac on Vision OS.\n\nNow, let's take a look at the components provided by the framework in order to get a sense of just how easy it is to set up The first step is selecting an endpoint that points to a device to share to. If someone's actively using Mac Virtual Display, it's easy to use the device that's already connected. Alternatively, you can add the device picker UI to your app so people can pick from any nearby Vision Pro on the same iCloud account.\n\nNext, create a spatial preview session for the content. There are two types of preview sessions. Document preview sessions, cover types like spatial photos, videos, and document types like PDF. USD preview sessions handle 3D content.\n\nOnce the session starts, Quick Look is launched on Vision Pro and the content your app provides to the session appears. No code is required on Vision OS to set this up.\n\nNow that you have an overview of how a spatial preview session works, let's look at an example of how to send and update documents with document preview.\n\nIn this example, I'll take a still from an Apple immersive video, which is generated out of the preview app on Mac. Then I'll use the Mac Virtual Display Endpoint to start a document preview session I'll then provide the image to the session so it appears in QuickLook. Let's look at the code to set this up.\n\nFirst, create a connected spatial endpoint observer to obtain the spatial preview endpoint from Mac Virtual Display.\n\nThen, create a document preview session, specifying the document type and a name for the session.\n\nWith the returned endpoint, start the session on the selected device and provide the content URL. Remember, Mac Virtual Display may not always be active, so consider integrating the Spatial Preview device picker as a view in your UI to select a different device. Use a sheet in Swift UI to control when it appears. Then proceed to create the document preview session, just as you saw previously, with the device endpoint selected in the UI. It's this easy to get started with spatial preview. Let's see the result. Putting the code I wrote into a button in the UI launches the content on Vision OS when pressed, and I can view it in immersive mode. This is pretty cool.\n\nLet's take this one step further and build a gallery of immersive architectural renderings.\n\nWhen you call update contents, you're reusing the same scene that was launched when you started the session. If you make a new document preview session and call start, that will launch a new scene. So for the gallery view, I'm going to want to leverage update contents to ensure I reuse the same scene.\n\nIn this view, I'll create a row of buttons for all the video frames in the gallery and make the buttons call update contents on the session to swap to a different file when selected.\n\nI also set up a task that observes the session state. If the scene is closed on Vision OS, I'll receive a state change here that the session has been invalidated. When you're done with a session, call close to end it. Visual S will automatically dismiss that scene. Now, the gallery switches between immersive renderings live within the same scene using Document Preview Session's Update Contents function. This is a great way for people to review design renders and get a better sense of the content they're working on at scale. Beyond Apple immersive video, many other content types work well with spatial preview, including spatial photos. PDFs, standard images and files, and also 3D content. Now that you've seen how easy it is to send and update documents, let's look at how to work with 3D content using USD kit with Spatial Preview If you're new to USD, I recommend starting with the video Understand USD Fundamentals. And to learn how to use the new Swift USD Kit framework, I recommend watching Discover USD Kit and What's New in OpenUSD.\n\nIn this section, I'm going to cover how to use USD Kit to work with 3D content from your macOS app on Vision OS. I'll go over how easy it is to set up a USD preview session and start navigating a 3D scene looking through cameras and applying material overrides. I'll then cover how to edit content and how people can interact and make changes to USD on Vision OS, including adding annotations and moving objects around. Last, I'll go over the events and observable properties of a spatial preview session, including events for animation playback and session synchronization progress, and how to customize the features available in a session Creating a USD preview session is similar to a document preview session, except that the content is a USD kit stage. When you call session start, a scene opens on Vision OS and the USD content appears in a volumetric view. People can then choose to go into an immersive view to see it at full scale. Let's see what the code looks like to set this up.\n\nJust like with document sharing, begin by choosing a target device endpoint. Then use USD kit to load USD content into a stage. Provide that stage to a USD preview session, and then start the session on the selected device endpoint.\n\nThe content appears in 3D on Vision OS in this bounded view. I can rotate the scene around to inspect it, and then go immersive to see it at full scale.\n\nThere are some cameras set up in this scene, as you saw before. So selecting one here will move me to that viewpoint. I can also look more closely at the geometry by overriding the materials to be in wireframe mode This is all functionality built into Vision OS when using Spatial Preview. No additional setup is required from your Mac app. USD scenes can contain incredibly high fidelity content, which might be too complex to render on Vision Pro. By default, Spatial Preview automatically optimizes the USD content before sharing, including mesh decimation, texture downsampling, and potential full scene reconstruction if necessary. all to ensure that it performs well on Vision Pro. If the scene needs to be reconstructed, it won't be editable, but people can still view it and add annotations.\n\nTo opt out of this optimization, pass the unmodified parameter when creating the session. However, if optimization is disabled, complex scenes may not be shareable to Vision Pro and an error will be thrown from the start function. For guidance on how to reduce the rendering cost of your content, check out the developer documentation.\n\nNow, let's see how editing USD content works on Mac OS and Vision OS. In a USD preview session, people can make changes from either device. A live USD stage is used to replicate content between Mac OS and Vision OS when calling regular USD kit APIs. This is very useful if someone wants to make changes to the content on their Mac and see them spatially on Vision OS, or capture the edits during a review session on Vision OS.\n\nBefore jumping into code, let's go over some details of what a USD stage is and some USD terminology.\n\nA stage is composed of one or many layers, and layers contain USD prims.\n\nA USD Prim is an object that can represent many different features like a 3D transformation or a mesh. Invariant sets can swap in alternative data on a prim.\n\nI've set up some USD variants in this living room scene that change the position and rotation of the furniture using variance sets When I select one of these variants in USD kit, the stage is changed and the edits that move the furniture to different locations are applied and synchronized between Mac OS and Vision OS. The buttons along the toolbar here switch between variants on macOS, and the resulting furniture layout is reflected on Vision OS. I can also select a variant in the QuickLook menu and see this change on Mac. This is a great way to quickly iterate on content from Mac on Vision OS. Now, let's see how to listen for changes coming from Vision OS. Changes to the USD stage are automatically synchronized, and you can observe the updates happening in a session, like when someone adds an annotation, using standard USD notices Here I subscribe to objects did change notices that give information about which USD Prim paths have changed. I can iterate through those paths and find annotations to update them in the UI.\n\nFor a text annotation, you'll want information like author and a unique identifier for the annotation in addition to the actual text note. Setting up annotation data in this way allows annotations to show up on Vision OS, as long as they are children of a USD Prim specified as a document annotation group.\n\nIn order to have USD prims be editable on Vision OS, using gestures, they need to have the spatial editable metadata set on them.\n\nPeople can also easily set this up on their USD asset using preview on macOS.\n\nLet's see this in action. When I apply an annotation on Mac OS, it appears on Vision OS. And if I comment here, I like layout A. That is reflected back in the app on Mac OS. And since this furniture has spatial editable metadata, I can move it Say I want to move this chair out the window. That would show up on macOS as well. Let's go over the events you can listen for and the options you can configure within a USD preview session.\n\nYou can control what features are available in QuickLook on Vision OS using the options set on USD Preview Session Start. By default, annotations, object manipulation, and USD export are all enabled.\n\nYou can also subscribe to non-USD events like animation playback time via spatial preview API events.\n\nYou can monitor the sharing progress of a session using Progress Reporter. This is useful if you'd like to display a loading bar on Mac OS while large data is synchronized over Division Pro.\n\nFor example, here, I've hooked up the time and playback events provided by USD preview session. Now I can hit the play button and see this hummingbird animate outside the window in both apps. When you adopt spatial preview in your app, you unlock so many new workflows for creators. And SharePlay support is built in on Vision OS, so collaborators can join the same live session to review and edit spatial content simultaneously. Here, my friend Joy flagged an issue with my room layout, so I'll adjust it in real time. Every participant's view updates immediately, eliminating the back and forth of traditional asset review.\n\nSo how can you get started with Spatial Preview today? We recommend using the USD kit Swift APIs, as Spatial Preview works seamlessly with it. If you already have USD in your Mac app. You can set up bridging to transfer edits between your USD installation and USD kit. For more information, check out the Spatial Preview Developer Documentation.\n\nNext, explore how you can leverage each type of preview session, document preview session and USD preview session. They integrate easily into your Mac app, whether you're looking to preview and update documents, or work with 3D content immersively. It's only a few lines of code to get started. You can go even deeper with 3D using USD kit and spatial preview to support live editing of a USD stage. Enabling real-time collaboration across devices, including over SharePlay. And finally, take advantage of asset review tools like material overrides, camera viewpoints, object manipulation, variants, and annotations.\n\nThe team and I can't wait to see how your apps will take advantage of Spatial Preview, whether you're making a creative app, content review tool, or something entirely new. Thanks for watching.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:58",
+ "title": "Document Preview Session with Device Picker",
+ "language": "swift",
+ "code": "// Send and update documents using the Spatial Preview framework\n\nimport SwiftUI\nimport SpatialPreview\nlet deviceObserver = ConnectedSpatialEndpointObserver()\n\nlet previewSession = DocumentPreviewSession(name: \"Immersive.aivu\", contentType: .aivu)\n\nfunc startPreview(contentURL: URL, endpoint: SpatialPreviewEndpoint) async throws {\n let endpoint = try await deviceObserver.endpoint\n try await previewSession.start(endpoint: endpoint)\n try await previewSession.updateContents(url: contentURL)\n}\n\n@State var showDevicePicker: Bool = false\n\nvar body: some View {\n ...\n .sheet(isPresented: $showDevicePicker) {\n SpatialPreviewDevicePicker(isPresented: $showDevicePicker) { endpoint in\n showDevicePicker = false\n Task {\n try await startPreview(filename: filename, endpoint: endpoint)\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "5:20",
+ "title": "Update Document Contents",
+ "language": "swift",
+ "code": "// Send and update documents using the Spatial Preview framework\n\nimport SwiftUI\nimport SpatialPreview\n\nForEach(contentURLs, id: \\.self) { url in\n Button {\n Task { try await previewSession?.updateContents(url: url) }\n }\n}\n.task(id: previewSession.map { ObjectIdentifier($0) }) {\n for await state in Observations({ session.state }) {\n if state.isInvalidated {\n previewSession = nil\n break\n }\n }\n}\n\ntry await previewSession?.close()"
+ },
+ {
+ "timestamp": "7:36",
+ "title": "Edit USD Live",
+ "language": "swift",
+ "code": "// Edit USD live using USDKit and Spatial Preview\n\nimport SpatialPreview\nimport USDKit\n\nlet deviceObserver = ConnectedSpatialEndpointObserver()\n\nvar usdSession: USDPreviewSession?\n\nfunc shareStage(to endpoint: SpatialPreviewEndpoint) async throws -> USDPreviewSession {\n let endpoint = try await deviceObserver.endpoint\n\n let stageURL = Bundle.main.url(forResource: \"sampleScene\", withExtension: \"usdz\")\n let stage = try USDStage.open(stageURL)\n usdSession = USDPreviewSession(stage: stage)\n\n try await usdSession?.start(endpoint: endpoint)\n}"
+ },
+ {
+ "timestamp": "8:56",
+ "title": "Opt out of optimization",
+ "language": "swift",
+ "code": "// Optimization\n\nimport SpatialPreview\n\n\n\n\nlet endpoint = try await deviceObserver.endpoint\ndo {\n try await usdSession.start(endpoint: endpoint, parameters: .unmodified)\n} catch USDPreviewSession.Error.assetUnshareable {\n // Handle Asset Unshareable error\n}"
+ },
+ {
+ "timestamp": "10:10",
+ "title": "USD Layout Variants",
+ "language": "swift",
+ "code": "// LayoutVariants.usda\n#usda 1.0\nover \"furniture\" (\n variantSets = \"Layout\"\n variants = { string Layout = \"LayoutA\" }\n)\n{\n variantSet \"Layout\" = {\n \"LayoutA\" {\n // Default furniture position and rotation\n }\n \"LayoutB\" {\n // Moves furniture prims to a different position and rotation\n }\n ...\n }\n}"
+ },
+ {
+ "timestamp": "10:17",
+ "title": "Edit USD live using USDKit and Spatial Preview",
+ "language": "swift",
+ "code": "// Edit USD live using USDKit and Spatial Preview\n\nimport SpatialPreview\nimport USDKit\n\nfunc applyLayoutVariant(named layoutVariantName: String) throws {\n let prim = stage.prim(at: SdfPath(\"/root/furniture\"))\n try prim.variantSets?.setSelection(\"Layout\", variantName: layoutVariantName)\n}"
+ },
+ {
+ "timestamp": "10:49",
+ "title": "USD Stage Observations",
+ "language": "swift",
+ "code": "// Edit USD live using Spatial Preview\n\nimport SpatialPreview\nimport USDKit\n\nlet observerToken: ObservationToken\n\nobserverToken = stage.addObserver(for: UsdStage.ObjectsDidChange.self) { notice in\n for path in notice.resyncedPaths {\n let prim = notice.stage.prim(at: path)\n guard prim.isValid else { continue }\n if prim.isAnnotation {\n // Handle annotation change\n break\n }\n }\n}"
+ },
+ {
+ "timestamp": "11:13",
+ "title": "Annotation Spec",
+ "language": "swift",
+ "code": "// Annotation spec example\n\nAppleTextAnnotation {\n // The textual representation of this annotation\n string text\n\n // The identifier for this specific author\n uniform string author\n\n // An identifier that is unique to your data tracking system\n uniform string identifier\n}\n\n/__documentAnnotationGroup__"
+ },
+ {
+ "timestamp": "11:33",
+ "title": "Metadata for Object Manipulation",
+ "language": "swift",
+ "code": "// Metadata required for object manipulation in Quick Look\n\ncustomData = {\n dictionary apple = {\n bool spatialEditable = 1\n }\n}"
+ },
+ {
+ "timestamp": "12:16",
+ "title": "Session Options and Events",
+ "language": "swift",
+ "code": "// Spatial Preview session options and events\n\nimport SpatialPreview\nimport USDKit\n\nsession.start(endpoint: endpoint, options: [.annotations, .perObjectManipulation, .export])\n\nfunc listenForEvents(session: USDPreviewSession) async {\n for await event in session.events {\n if case .timeChanged(let time) = event {\n playbackModel.timeCode = time\n } else if case .playbackStateChanged(let isPlaying) = event {\n playbackModel.playbackStateChanged(isPlaying)\n }\n }\n}"
+ },
+ {
+ "timestamp": "12:38",
+ "title": "Observe Session Progress",
+ "language": "swift",
+ "code": "// Observe Spatial Preview session progress\n\nimport SpatialPreview\nimport USDKit\n\n@State private var sessionProgress: Double = 0\n\nvar body: some View {\n ...\n .task(id: usdSession.map { ObjectIdentifier($0) }) {\n guard let session = usdSession else { return }\n for await fraction in Observations({ session.progress.fractionCompleted }) {\n sessionProgress = fraction\n }\n }\n .overlay(alignment: .bottom) {\n ProgressView(value: sessionProgress)\n .padding()\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Bridging an application’s custom USD runtime to Spatial Preview",
+ "url": "https://developer.apple.com/documentation/SpatialPreview/bridging-an-external-usd-runtime-to-spatial-preview"
+ },
+ {
+ "title": "Working with content from your Mac app using Spatial Preview",
+ "url": "https://developer.apple.com/documentation/SpatialPreview/working-with-content-from-your-mac-app-using-spatial-preview"
+ },
+ {
+ "title": "Reducing the rendering cost of RealityKit content on visionOS",
+ "url": "https://developer.apple.com/documentation/visionOS/reducing-the-rendering-cost-of-RealityKit-content-on-visionOS"
+ },
+ {
+ "title": "Spatial Preview",
+ "url": "https://developer.apple.com/documentation/SpatialPreview"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/282/5/958c34c9-f20e-4c6d-826a-eeed7ce7ba9e/downloads/wwdc2026-282_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/282/5/958c34c9-f20e-4c6d-826a-eeed7ce7ba9e/downloads/wwdc2026-282_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "284",
+ "year": "2026",
+ "title": "Collaborate on structured 3D models in visionOS",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/284"
+ },
+ {
+ "id": "285",
+ "year": "2026",
+ "title": "Discover USDKit and what’s new in OpenUSD",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/285"
+ },
+ {
+ "id": "10129",
+ "year": "2022",
+ "title": "Understand USD fundamentals",
+ "url": "https://developer.apple.com/videos/play/wwdc2022/10129"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:18.415Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-283.json b/data/wwdc/videos/2026-283.json
new file mode 100644
index 0000000..a4732c6
--- /dev/null
+++ b/data/wwdc/videos/2026-283.json
@@ -0,0 +1,91 @@
+{
+ "id": "283",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/283/",
+ "title": "Explore enhancements to visionOS object tracking",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hello, welcome to explore enhancements to visionOS object tracking. I'm Nathan Kong, a Strategic Partnerships Manager on the visionOS team. In this session, we will share the new object tracking and spatial accessory capabilities that build upon features described in previous sessions. As a reminder, in visionOS 2.0 we introduced object tracking, which allows you to turn real world objects into virtual anchors. With only a USDZ model of the item you'd like to track, you can create a reference object through machine learning training in Create ML on your Mac.\n\nBy passing reference objects to our APIs, your app can get the position and orientation of the physical objects to create immersive spatial experiences. In visionOS 27, we are expanding object tracking, giving you the ability to track objects in motion, including handheld items alongside other enhancements.\n\nFor example, you can now accurately track and measure physical spaces using handheld items such as this medical probe. Here we can precisely measure the distance between the vertebrae on this physical spine model. This can enable exciting use cases such surgical training, home remodeling, or guided assembly.\n\nSwitching to spatial accessories, in visionOS 26, we introduced the first set of spatial accessories including the Logitech Muse and the PSVR2 Sense controllers.\n\nSpatial accessories are electronic devices that communicate with Apple Vision Pro, which tracks their realtime position and orientation in the real world.\n\nThese devices also enhance interactivity and immersion in your apps through buttons and haptics.\n\nNow, in visionOS 27, we are expanding support to enable anyone to build your own accessory.\n\nFor example, we can mount an accessory like this Spatial Anchor inside of a physical steering wheel in order to seamlessly align a full scale digital vehicle to it.\n\nWhen I reach out and grab the steering wheel, I truly feel like I'm inside this car.\n\nThis capability unlocks incredible experiences such as immersive racing and flight simulations or vehicle interior design.\n\nIn the remainder of this session we will cover improvements to object tracking, show you how to create a spatial accessory, and conclude with considerations you should keep in mind when deciding which approach is best for your visionOS app. Now let's dive into object tracking.\n\nObject tracking now supports high frame rate tracking. This enables your app to have a better understanding of the position of objects as they move in space. Along with this, we're adding a new extended training mode in Create ML for increased tracking accuracy and robustness, especially when holding objects in hand.\n\nTo enable even more accurate tracking use cases, we're introducing an API to obtain the object's pose in metric space, unaffected by any display corrections. This is essential to enable measuring applications like the medical probe example from before.\n\nLastly, we're bringing object tracking to iOS this year. Let's take a look at some of these updates in action using this flashlight. If I have access to a photorealistic 3D model of the flashlight, I can use it as the reference object for object tracking.\n\nSince my app understands the position of the flashlight, I can take advantage of the new Physical Surroundings Light in RealityKit to relight this room in real time. I can also swap the digital pattern being projected onto the surfaces around me. Even though my hand is occluding part of the flashlight, my app is still able to track the position of the object robustly.\n\nAlternatively, I can 3D print a marker like this as the reference object and mount it to the flashlight.\n\nThis is an easy way to track any handheld object, even if you're unable to obtain a photorealistic 3D model of the device.\n\nNow, let's discuss the specific updates to show you how to upgrade an existing object tracking experience or start a new one from scratch.\n\nFirst I'm going to explain how to enable the new high frame rate tracking.\n\nWe're adding a new Reference Object Configuration API in ARKit on visionOS. It let's you enable high frame rate tracking for an individual reference object before creating your object tracking session. Once, configured, you can pass this configuration object as an additional parameter when loading the reference object as before. Since this this is not a training setting, it can be applied to any reference object depending on the application's needs. Next let's discuss the new extended training mode we added to Create ML.\n\nWhen training a reference object in Create ML, you can now choose between the standard and the extended training mode. The new extended training setting increases the accuracy and robustness of tracking, and is recommended to be used in combination with high frame rate tracking. Please note that the extended training takes significantly longer compared to standard mode.\n\nYou will find the training mode setting in the Object Tracking template in the Create ML app, right below the viewing angle settings. Everything else stays exactly the same when it comes to training a reference object.\n\nIf you prefer to train reference objects through the command line interface, you can configure the training mode there as well. This gives you the flexibility to run the training on a remote machine.\n\nNow we'll review the new benefits of obtaining object poses in metric space.\n\nBy default, object anchor transforms are optimized for placing virtual content aligned with the tracked object in the mixed immersion style.\n\nTo achieve this, the object poses are slightly altered to match the displayed camera images, impacting the accuracy in absolute world coordinates. This can be a limitation when trying to use object tracking for spatial measuring tasks.\n\nIn visionOS 27, we're adding the ARKit Coordinate Space Correction API that let's you obtain the anchor transform with or without these corrections. When querying a tracked object's pose, it provides two options: rendered, returns the pose with display corrections applied to keep virtual content visually aligned with the real-world object, and none, returns the object's pose in metric space, without any corrections. This is useful for measuring the distance between tracked objects or determining where an object sits in physical space as shown in the medical probe demo earlier.\n\nLast but not least, this year we're bringing object tracking to iOS! But first, let's bring back the globe from WWDC24! In iOS 27, we're adding support for reference objects in our ARKit APIs.\n\nMachine learning training is not platform specific. So once trained, all reference objects will be supported in both iOS and visionOS apps.\n\nHere's all it takes to get object tracking running on iOS. You load your reference objects and create a world tracking configuration. These are the same reference object files you use on visionOS.\n\nYou assign objects to either detectionObjects for objects that are mostly stationary, or trackingObjects for high frame rate tracking of moving objects. Then you run the session and handle the anchors in your delegate.\n\nWhen ARKit recognizes an object, didAdd is called and you get an ARObjectAnchor that you can attach content to. While the object gets tracked, didUpdate provides the latest poses, which you can use for your custom app behavior.\n\nAnd if the object is removed from the scene, didRemove lets you clean up and remove the anchor entity created in didAdd.\n\nThose are all the updates to object tracking in visionOS 27. To learn more about how to develop an app with the object tracking API, you can watch the \"Explore object tracking for visionOS\" WWDC session or explore documentation. Next, we'll explore how you can turn your object of interest into a spatial accessory.\n\nI'll start by defining a spatial accessory and highlight some of their benefits. Then I'll cover design considerations you should keep in mind when creating your accessory. I'll explain the process to validate your design and prepare it for visionOS. Share some easy plug-and-play accessories and finally show you how to prepare your visionOS app to take advantage of these devices. Now let's get started with the basics.\n\nA spatial accessory is an electronic device which must contain a board with the following components. A constellation of LEDs visible to Apple Vision Pro for tracking. An IMU to capture the orientation and acceleration of the accessory. And a Bluetooth chip to send the signals to Vision Pro. Spatial accessories can also host any variety of inputs including buttons or a touchpad, and outputs like haptics. Any accessory that has these key components is compatible with visionOS.\n\nLet's take a look at spatial accessories in action. By installing the aforementioned components into the flashlight from before, we can make the device itself a spatial accessory. Even when I quickly wave this flashlight, the digital beam of light follows smoothly due to the low latency tracking enabled by the embedded IMU. And using this physical button I added to accessory, I can turn the digital light off and on, making my experience even more interactive. Vision Pro is able to track this spatial accessory, by seeing the LEDs installed inside the flashlight. Now we'll cover some of the benefits spatial accessories offer. These devices can be tracked at high frequency up to the full display rate with low latency and support use-cases that demand fast motion.\n\nSpatial accessories will continue to track robustly, even when temporarily occluded.\n\nYou can track the accessory under lower light conditions. Lastly, the physical buttons and haptics allow you to make your experiences even more interactive and immersive.\n\nBefore you get started with creating your own accessory, there are important design considerations you should keep in mind. It's important to spread the LEDs around the device in such a way that it creates a distinct and unique pattern when viewed from various angles. Both the LEDs and IMU should be rigidly fixed to the board for accurate tracking.\n\nLast but not least, you should consider the primary way users will interact with your accessory. For example, a handheld accessory should position most of the LEDs in areas where users will not hold the device. You should also consider the size and position of the battery to ensure the accessory is ergonomic.\n\nFor larger accessories used out of arms reach, you should consider the number of LEDs, LED size, and distance between them to ensure the accessory can be tracked accurately even when far away.\n\nFor more information on specific requirements and reference designs, please check out the \"Spatial Accessories\" chapter of the \"Accessory Design Guidelines\" for Apple Devices. Now that you've designed a spatial accessory, let's explore how to validate that it works as expected and prepare it for your visionOS app. The first step is to connect your accessory via Bluetooth to your Vision Pro and validate the signals sent from the device with the ARKit accessory tracking debug view. This tool lets you examine how your Vision Pro sees the accessory and can be found in settings when your device is in developer mode. It helps you with three things: One, verify your LEDs through the headset's IR camera, so you can confirm they're bright, distinct, and properly synchronized. Two, validate your IMU with live metrics on frequency, latency, and per-axis values, in order to check scale, alignment, and motion response. Three, debug timing between your accessory and the headset using the device's IR illuminators as a sync reference.\n\nNext, in order for your visionOS app to track a spatial accessory, you will need to train it with the CreateML bundle.\n\nThis workflow uses information about both the physical appearance of the device and the location of the LEDs to create a reference accessory file.\n\nTo get started, create a USDZ of your design that contains a photorealistic 3D model of the device annotated with the positions of the IMU and LEDs. Using this annotated USDZ, you can use the command line interface to generate the reference accessory file and add it to your app.\n\nAs the manufacturer of the accessory, you bundle this file in your app and declare it as an exported UTType in your Info.plist. This registers your accessory system-wide, so any app on Apple Vision Pro can use it.\n\nIf you're a developer using a third-party accessory, you can also bundle the file yourself and declare it as an imported type, so your app works independently.\n\nBefore building a spatial accessory from scratch, you can also start testing and develop apps with plug-and-play accessories.\n\nManufacturers like DFRobot and MIKROE will release off-the-shelf reference hardware and development kits later this year.\n\nThese accessories can be immediately used for testing or implemented in your visionOS app. Let's see one of these plug-and-play accessories in action.\n\nAs you can see here, I can simply mount a spatial accessory like the seeMote Cap to the flashlight and use the Spatial Accessories API to enable the same digital relighting experience as before.\n\nNow that you have a spatial accessory, let's explore how to connect it to your app. You discover accessories using the new GCSpatialAccessory class. This works with any device that has a referenceaccessory bundle.\n\nWhen you call Accessory(device), ARKit resolves it automatically. From there, you can run an AccessoryTrackingProvider just like before.\n\nWe're also adding a new updateAccessories method to switch between accessories while your session is running to avoiding interruptions to tracking. And with that you can create your own spatial accessory and connect it to your visionOS app. To learn more about how to enable tracking with inputs and haptics in your app, you can watch the \"Explore spatial accessory input on visionOS\" session.\n\nLet's wrap things up. Today you've seen four different approaches for tracking an object in your app. Let's review some considerations you should keep in mind when selecting which approach to use.\n\nObject tracking excels in scenarios where accurate and precise tracking is required, such as measurement applications.\n\nIn case you do not have access to a photorealistic 3D model, you can train your referenceObject on a marker you mount on the object of interest.\n\nSpatial accessories offer even higher refresh rates and lower latency, which is optimal for experiences that demand fast moving objects.\n\nIf you want to create an even more interactive and immersive experience with physical objects, you can design your own accessory with custom buttons and haptics.\n\nThe possibilities enabled by these new object tracking capabilities are endless. We're excited to see the transformational experiences for work and play you can create. Have a fantastic WWDC26!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:50",
+ "title": "Enable high frame rate tracking",
+ "language": "swift",
+ "code": "// Enable high frame rate tracking\n\n// Create reference object configuration\nvar configuration = ReferenceObject.Configuration()\nconfiguration.highFrameRateTrackingEnabled = true\n\n// Load the reference object with ARKit API\nlet refObjURL = Bundle.main.url(forResource: \"flashlight\", withExtension: \".referenceobject\")\nlet refObject = try? await ReferenceObject(from: refObjURL!, configuration: configuration)"
+ },
+ {
+ "timestamp": "4:50",
+ "title": "Extended training mode via command-line",
+ "language": "swift",
+ "code": "// Extended training mode on Mac using command-line interface\n\n% xrun createml objecttracker --source flashlight.usdz --output flashlight.referenceobject --training-mode extended --all-angles"
+ },
+ {
+ "timestamp": "5:25",
+ "title": "Object pose coordinate spaces",
+ "language": "swift",
+ "code": "// Different object pose spaces\n\n// Obtain anchor transform with display corrections\n\nlet renderingPose = myObjectAnchor.coordinateSpace(correction: .rendered)\n\n// Obtain anchor transform in metric space\n\nlet metricPose = myObjectAnchor.coordinateSpace(correction: .none)"
+ },
+ {
+ "timestamp": "6:22",
+ "title": "Implement object tracking in iOS",
+ "language": "swift",
+ "code": "// Implement object tracking in iOS\n\nimport ARKit\nimport RealityKit\n\nclass ObjectTrackingARSessionDelegate: NSObject, ARSessionDelegate {\n let arView = ARView(frame: .zero)\n var entities: [UUID: AnchorEntity] = [:]\n\n func start() throws {\n let stationaryObject = try ARReferenceObject(archiveURL:\n Bundle.main.url(forResource: \"stationary\", withExtension: \"referenceobject\")!)\n let movingObject = try ARReferenceObject(archiveURL:\n Bundle.main.url(forResource: \"moving\", withExtension: \"referenceobject\")!)\n\n let configuration = ARWorldTrackingConfiguration()\n configuration.detectionObjects = [stationaryObject] // Low frame rate\n configuration.trackingObjects = [movingObject] // High frame rate\n\n arView.session.delegate = self\n arView.session.run(configuration)\n }\n\n\t\t\t\tfunc session(_ session: ARSession, didAdd anchors: [ARAnchor]) {\n for case let anchor as ARObjectAnchor in anchors {\n let entity = AnchorEntity(anchor: anchor)\n entities[anchor.identifier] = entity\n arView.scene.addAnchor(entity)\n }\n }\n\n func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {\n for case let anchor as ARObjectAnchor in anchors {\n entities[anchor.identifier]?.isEnabled = anchor.isTracked\n }\n }\n\n func session(_ session: ARSession, didRemove anchors: [ARAnchor]) {\n for case let anchor as ARObjectAnchor in anchors {\n if let entity = entities.removeValue(forKey: anchor.identifier) {\n arView.scene.removeAnchor(entity)\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "12:26",
+ "title": "Discover and connect a spatial accessory",
+ "language": "swift",
+ "code": "import ARKit\nimport GameController\n\n// Generic accessory discovery\n\nif let device = GCSpatialAccessory.spatialAccessories.first {\n\n // Resolves the .referenceaccessory bundle automatically\n \n let accessory = try await Accessory(device: device)\n let provider = AccessoryTrackingProvider(accessories: [accessory])\n try await arkitSession.run([provider])\n}\n\n// Update tracked accessories without restarting the session \n\ntry await provider.updateAccessories([newAccessory])"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Implementing object tracking in your app",
+ "url": "https://developer.apple.com/documentation/visionOS/implementing-object-tracking-in-your-app"
+ },
+ {
+ "title": "Working with generic spatial accessories",
+ "url": "https://developer.apple.com/documentation/visionOS/working-with-generic-spatial-accessories"
+ },
+ {
+ "title": "Preparing spatial accessories for tracking in your visionOS app",
+ "url": "https://developer.apple.com/documentation/ARKit/preparing-spatial-accessories-for-tracking-in-your-visionos-app"
+ },
+ {
+ "title": "Spatial accessory design guidelines for Apple devices (check section 20)",
+ "url": "https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf#page=135"
+ },
+ {
+ "title": "Exploring object tracking with ARKit",
+ "url": "https://developer.apple.com/documentation/visionOS/exploring_object_tracking_with_arkit"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/283/4/22b92960-c65b-450f-b42c-6d6bff64a9b4/downloads/wwdc2026-283_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/283/4/22b92960-c65b-450f-b42c-6d6bff64a9b4/downloads/wwdc2026-283_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "289",
+ "year": "2025",
+ "title": "Explore spatial accessory input on visionOS",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/289"
+ },
+ {
+ "id": "10101",
+ "year": "2024",
+ "title": "Explore object tracking for visionOS",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10101"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:18.323Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-284.json b/data/wwdc/videos/2026-284.json
new file mode 100644
index 0000000..a03116b
--- /dev/null
+++ b/data/wwdc/videos/2026-284.json
@@ -0,0 +1,81 @@
+{
+ "id": "284",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/284/",
+ "title": "Collaborate on structured 3D models in visionOS",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Welcome to Collaborate on Structured 3D Models in visionOS. I'm Bill. Today I am going to talk about building spatial experiences on Apple Vision Pro. Specifically, how to work with complex assemblies and multi-dimensional data in ways that simply aren't possible on a flat screen.\n\nLet's start with a look at what that can feel like.\n\nHere we see a team in a SharePlay call, a design review of the AirPods Pro. Everyone in the call sees the same asset, at the same fidelity, in the same space. The case pulls closer. It unlocks — the way it would on a workbench, except the workbench is wherever you happen to be. The bottom assembly lifts out. One person examines the interior, then rotates it so a colleague can see exactly the same thing. A point — not an annotation, not a screenshot, just a gesture toward the part that matters and the group understands.\n\nThe model collapses back. Clipping engages, a cross section opens, and the main logic board is right there — exposed in context, in a way that isn't feasible on a 2D screen.\n\nClipping disengages. The full assembly expands. Someone reaches in, pulls the motherboard free, and holds it up for the rest of the team. Three people, one model, and the tools they needed to understand it.\n\nWhat makes all of this possible comes down to three things Apple Vision Pro does really well. Real-time collaboration: multiple people, in a shared space, at the same moment Manipulation of rich representations: because any data with enough dimensions deserves more than a flat screen.\n\nEnvironment lighting: your physical world, grounding every virtual decision And those three capabilities aren't just useful for CAD. Anywhere you have complex, multidimensional information to reason about — urban planning, logistics, real estate, production design — the same principles apply. For a technical deep dive on making SharePlay work in your apps please see \"Share visionOS experiences with nearby people\" from WWDC25.\n\nThere are 4 major considerations in building out the sample code.\n\nI'll start with sharing some important aspects of preparing your 3D assets so that people can easily manipulate and work with them.\n\nComplex spatial assets are structured collections of components, organized as assemblies. I'll show you how to enable manipulating these assemblies.\n\nThen, I'll go over clipping. That allows the insides of an assembly to become as accessible as the outside.\n\nAnd finally autoexpansion, where the asset expands itself, and every sub-assembly separates and moves into position, revealing the full structure. On to asset preparation.\n\nAssets without structure are hard to reason about and hard to use in code.\n\nWithout structure the code can't make decisions on what to hide or show, what to make manipulable.\n\nWhen preparing an asset there are a lot of things to consider, what I'll focus on here is how to think about the structure of your asset, what assemblies contain what sub-assemblies. In other words the hierarchy of the model.\n\nSome of the other considerations, of a more technical aspect, are covered elsewhere.\n\nFor details, check out \"Optimize your 3D assets for spatial computing\" from WWDC 24.\n\nA 3D model is a part-whole relationship. Flatten everything to the root, and even simple operations become surprisingly painful. You've still got all the geometry — it's just not organized.\n\nThis engine block was exported without preserving its structure. Everything flattened up to the root. InteriorPart_01, InteriorPart_03, part 25, all the way down. No sub-assemblies. No grouping. And here's the thing. This asset looks completely fine in the viewport. The geometry is all there and it renders correctly. But the structure that would make it interactive? Gone. If I want to isolate one piston, it's in here somewhere. Was it InteriorPart_47 or InteriorPart_18. I don't know, and neither does my code.\n\nConsider this updated asset, it's got a deep, nested hierarchy.\n\nIt's complex — and that's intentional.\n\nHere we have hidden the outside of the engine and all the pistons but one. We can see a piston and the crankshaft, alone. Each one is its own node — named, organized, and grouped.\n\nIf I want to animate just the piston — isolate it, highlight it, let a person reach out and pull it free — I can. It's organized, I can write code that can find it.\n\nThat's hierarchy doing exactly what it's supposed to do. Now that we have a good hierarchy, I'll show you how to enable people to pull this hierarchy apart.\n\nA good hierarchical structure will give people the ability to select an individual part, or move it using the natural input system on Apple Vision Pro.\n\nThis can be achieved using RealityKit's ManipulationComponent API correctly. I'll show you how this is done. Here is a demo of it in action. This is the sample app that I showed you at the beginning of this presentation. It showcases this AirPods Pro asset that I plan to use in a design review session later.\n\nWatch what happens when we hit Open.\n\nA moment ago, this was one object.\n\nNow every part of it is individually interactive — grab one piece, leave the rest.\n\nLet me show you how I got this working.\n\nTo enable people to move this assembly around, all we have to do is attach a ManipulationComponent to it. That's our starting point. Get the object manipulable so people can orient it, move it, and scale it with natural hand movements.\n\nTo learn more about ManipulationComponent see \"What's new in RealityKit\" from WWDC 25. To enable people to move this assembly around, all we have to do is attach a ManipulationComponent to it.\n\nTo do that we move the ManipulationComponent down to the children. Suddenly, the top enclosure can be pulled away while the bottom enclosure stays put. A collaborator can rotate one of the ear buds while someone else is examining the other at the same time. That shift, from \"thing to look at\" to \"thing to explore,\" happens entirely because of where that component lives in the tree. Nothing else changed.\n\nOnce you've pulled things apart, you can move ManipulationComponent back up to the root. Now the whole spread moves as a single object again. Reposition it together, rotate it, bring one section closer — the internal relationships stay exactly as you left them.\n\nThe hierarchy hasn't changed. The geometry hasn't changed. Just where the component lives in the tree. And that's the whole idea here: component placement drives behavior. Let's look at the code to make this work. Remove the the ManipulationComponent and InputTargetComponent from the entity. That makes this entity not manipulable.\n\nThen the code iterates the sub-entities.\n\nAdd the InputTargetComponent and the ManipulationComponent to each sub-entity.\n\nIn the sample code I make sure to set the ManipulationComponent's releaseBehavior to .stay. That makes it so the entity stays where the person puts it when they release it.\n\nOne more important note here, I'm specifically not showing the addition of the CollisionComponent. But it is critical for event processing that your entities have collision components, don't forget to add them. Of course, if you are going to open an assembly you probably want to close it too.\n\nClosing an assembly follows the same process but in reverse.\n\nRemove the Manipulation and InputTarget components from the sub-entities.\n\nAdd the Manipulation and InputTarget components back to the entity. That's it, the tree is now able to be manipulated as one element as if it's closed or as independent entities if it's open.\n\nNext up is clipping. Any sufficiently complex asset has layers you can't see from the surface. The internal structure of a building, the routing behind a panel, the infrastructure beneath a city block. Clipping lets people see through the asset, literally, and it's a new RealityKit capability in visionOS 27. Let me first show you a demo of it in action, then take you through how to edit the clipping planes.\n\nHere you will notice the assembly, unclipped sitting in space, the clipping state is .off. Then I turn clipping on and enable the clipped state of the assembly. The clipping plane is inside the assembly perpendicular to one of the primary axis, the +z vector in this case. It shows the internal structure. The clipping state is .on. Next the clipping plane moves around. That is the really cool thing I'll show you how to do.\n\nBefore we do though, let's look at ClippingComponent.\n\nClippingComponent has 4 properties. I'll go through the ones used in the sample code so you know what's there. bounds is the property you'll be working with most — an axis-aligned bounding box in entity local space. Anything outside it gets discarded by the renderer each frame.\n\nshouldClipChildren defaults to false. If you add this to a parent assembly and your children aren't clipping, that's why. The sample code sets it to true.\n\nshouldClipSelf defaults to true, which is almost always what you want. Our goal is to make the bounds editable. I'll show you how that's done after a quick look at the axis aligned bounding box. The six faces of the bounding box become six interactive plane entities — one per axis, positive and negative.\n\nEach face here represented by a different color.\n\nSomeone grabs the +x face and pulls it to reveal more of the interior, or pushes it back out to restore it. Each plane controls exactly one scalar value in the bounds.\n\nThat's the entire interaction model. Six planes, six numbers. Keep that in mind as we work through the implementation.\n\nTo manage clipping we have a three-state machine. Clipping can be .off — the assembly is not clipped. When clipping is .on the model is clipped according to the bounding box, In the .editing state clipping planes are visible and interactive. People can change the clipping bounds by moving them. Let's look at it in action.\n\nIn the .off state there is no clipping of the assembly, only the outside of the assembly is visible.\n\nIn the .on state clipping is active and shows the inner workings and layout of the sub-assemblies.\n\nIn .editing state, clipping planes are on and people are able to move the planes. As the planes move the clipping bounds are changed and more or less of the internal layout of the model becomes visible. Let's look at the technical details that make this all work.\n\nThere are three components involved. In the off state we have the ClippingBoundsCache, a custom component in the sample code. It keeps track of the clipping bounds that were last edited and provides that value to the ClippingComponent when the state switches to .on. In the .on state the ClippingComponent is created and added to the entity. This is the RealityKit component we discussed earlier. Geometry that's outside its bounds is discarded.\n\nIn .editing we add another custom component called ClippingTransformSync, the sample uses that to keep track of the assemblies transform and update the ClippingControl when the transform changes. The ClippingControl is the entity we use to manage the clipping planes and make them interactive. They are the visual affordance that allow people to see where the clipping planes are and edit them. There are four coordinate frames involved. The first is the world coordinate system, it's where everything else sits.\n\nModel is where the model lives, and is the coordinate frame the clipping component operates in. Changes to the bounds need to be made in this frame.\n\nThe clipping control frame is where we put the editing planes that allow people to change the ClippingComponent's bounding box. The clipping plane coordinate frame is where the editing planes live and where the drag gesture events are expressed. Changes to the position of the planes need to be in this frame and constrained to move in a direction expressed in the model frame.\n\nWorld has two children. The Clipping Control and Model Clipping Control contains the editing planes And Clipping Plane is the coordinate frame where the drag gestures are expressed. The task is to get the change in drag gesture expressed in the Model frame constrained then converted back to the Clipping Plane.\n\nTo this point we've been talking about the clipping as a monolithic thing, but there are two distinct parts that are worth separating. The ClippingComponent is in the model's coordinate space so to edit these bounds we need to have the change expressed and constrained in that coordinate frame.\n\nThe visual planes provided a visual understanding of what the movements do. The planes need to move with the events as well, but they are expressed in the clipping plane coordinate system, so updates to their positions need to be expressed in their coordinate frame. In both frames the change needs to be constrained to the direction normal to the bounding box plane. Let me show you how all this fits together.\n\nThere are 4 distinct steps from the drag gesture to updates for the bounds and plane position. I add a drag gesture to the clipping planes, one for each plane. Again, this is the coordinate frame the events arrive in. I transform that into the World coordinate frame.\n\nFrom there, I transform to the Model frame. Then I constrain the delta to the appropriate direction — +x, -y, etcetera — depending on which plane the person is moving.\n\nNow I have the value I need, in the coordinate frame I need, constrained to the correct direction, and can update the clipping bounds. To update the plane's location I'll have to convert this vector to the plane's coordinate frame. We'll look at that in just a second. For more information about the gesture component check out \"Better Together: SwiftUI and RealityKit\" from WWDC 25.\n\nWith the big picture in mind, let's look at each step in detail.\n\nThe gesture is expressed in the Clipping Plane coordinate frame. It will look something like this. The drag delta has values 0.5, -0.75, and 0.1. These values are the expression of the drag delta in the Clipping Plane coordinate frame. The task is to change the expression of this vector to the Model coordinate frame.\n\nThe drag delta vector is transformed from the Clipping Plane coordinate frame into the World coordinate frame. Keep in mind, the vector hasn't changed, only the coordinate frame the vector is represented in. Since Clipping Plane and World are not the same coordinate frame, the numbers change. It's the same vector, just a different representation.\n\nBefore we can update the clipping bounds, the drag delta vector must be in the Model coordinate frame. So, we do one more transform from the World coordinate frame into the Model coordinate frame. Again, the vector hasn't changed, just the coordinate frame it is represented in.\n\nNow comes the mathematical magic. We project the drag delta onto the proper direction, which is just a fancy math way to say how long the drag delta is in the direction we care about. Let's remove the AirPods Pro case so we can see the process better. Projection sounds complex, but it really is just how long is the drag delta along the direction we care about.\n\nIt's like measuring the shadow of the drag delta vector cast on the direction vector.\n\nHere is the math equation, don't let it scare you. I'll break it down piece by piece. First it's finding a vector in the direction we care about.\n\nYou might have done this before, it's the vector dived by its length squared.\n\nIn our case this is simple, the direction we care about is the normal to the plane, +x here, or {1, 0, 0}.\n\nThen we do a dot product, which is a type of vector multiplication, with the drag delta and the direction vector. That gives us the amount we want the bounding box to change, but it's just a number, we also need the direction. So we multiply the direction we care about, the normal to the plane, by the amount we found in the last step. Now I have the constrained delta in the Model coordinate frame. That is exactly what I need to updated the bounding box of the ClippingComponent.\n\nHere is the assembly clipped, the inside is as visible as the outside. That's pretty cool.\n\nNow, I go through the same constraint process. But this time to the Clipping Plane coordinate frame.\n\nWe have to transform the constrained drag delta from the Model coordinate frame into the Clipping Plane coordinate frame then we project that down onto the plane's normal, the same way we did last time. This gives us the value we need to move the plane. But, since it's been projected onto the normal the change is constrained to move only in that direction, instead of where ever the person moved their hand. That makes sure the changes from the gesture feel natural.\n\nHere we see the planes turned on and waiting for people to interact. We keep them on in the .editing state so that people know they can reach out and move any one of these six planes.\n\n6 planes, 4 coordinate frames. Simple transformations between each makes the individual calculations easier to reason about.\n\nOnce you have the hierarchy of the coordinate systems in mind and the really cool math trick of projection, you can make this interaction feel natural in your apps. Now, let's talk about automatic expansion of the sub-assemblies that make up a 3D model. People use this to reveal the model's inner structure. This feature can be great for a mechanical assembly, a building, really any asset where understanding how the parts relate to the whole would be helpful. I want the model to expand in an intuitive way. But I don't want to force the person to choose that direction. We are going to use a bit of math so the code can make the decision. I'll walk you through that, and show you exactly how it works.\n\nWhen an assembly loads, its children sit exactly where they are defined in the file.\n\nFor a well-constructed asset that means they're probably overlapping — nested inside each other the way they exist in the real object. That's correct, but it's not useful for exploration.\n\nExpansion fans the children apart along a single axis, giving each one space to be seen and grabbed independently.\n\nOne tap, the assembly opens itself. The process of doing that is not complicated, let's look at it a piece at a time.\n\nWe could display the assembly spread along the x-axis, like this. It does capture the idea of the pieces pulling apart in space to expose the interior layout.\n\nBut, we'd like the expansion to feel more natural. More like what a person would expect in a design review.\n\nAnd have it expand along the y-axis like this. The question is, how does the code choose which axis to expand along? For that I'll take a brief diversion into two concepts: variance and weighting.\n\nA low variance is just a way to say all the values we have are basically in the same spot.\n\nFor example, in this dartboard diagram the values are anything, like ice cream sales to incidence of sunburn. It's just a way to get across the idea that values, whatever values, are close together.\n\nA high variance will have the values spread out.\n\nNow that we have a feel for variance, let's look at it a little closer.\n\nI'll move to one dimension to make things a little clearer. Again these values are just numbers, they could represent anything, the frequency of 6 guitar strings, or anything else that has a single value per sample. Each of our points is placed on a number line along with an indicator of how far the value is from the average.\n\nThe distance from average is called deviation. This is the first step in finding the variance.\n\nNext, is to square each of the deviation values then add them together, and divide by the count to find the average. That's the variance. It's simple enough math, the technical terms \"variance\" and \"deviation\" is usually what gets people. Conceptually, it's just a way to numerically specify how much our set of values is spread out from the average.\n\nNow consider, some of these values might be more important than others.\n\nThat's where weighting comes in. We use it to distinguish the importance of individual values.\n\nEach of the points now has a radius to represent a weight factor, the larger the circle the more important the value.\n\nThe weighting factor could be anything, saturation of a gradient, or any other value that expresses importance. We are staying abstract to illustrate the process. We use the weight, or importance, to calculate a weighted variance. In addition to squaring the deviation values we multiply each by its weight.\n\nAnd boom, now we have a weighted variance. With the weight, each value can have a different importance. And that's exactly what we'll do to figure out what axis to expand our assembly along. We'll calculate the \"volume-weighted position variance\", what a mouthful, along each axis and expand along the axis with the largest variance.\n\nHere, notice the table: one row for each of the sub-assemblies. It shows their volumes, and their positions.\n\nThese are the values I'll use to find the natural axis to expand along. I'll use the x, y, and z values to calculate the variance in each direction.\n\nThe dots are sized to reflect the volume of each element. I'll use the volume as the weighting factor.\n\nHere is the volume-weighted variance for the x axis. Most of the sub-assemblies lie at or very near the same position along the x axis. That leaves us with a small variance along x. Since most of the sub-assemblies are at or near each other the volume weighting factor doesn't make a difference along the x axis. The two ear buds are spread out on the x axis and do their best to contribute, but there volume is not enough to make up for all the other elements at the same place.\n\nThe result along the z axis is even smaller. The Bottom insert does have some volume to weight its contribution, but it's too close to the average position to make much of a dent. The Hinge and Lid retention magnet have too small of a volume to pull the variance very far.\n\nThis takes us to the y axis, the clear winner here. The large parts are further away along the y axis and the larger volume provides weight to their distance.\n\nWith y being the clear winner we assemble a set of FromToBy animations to move the sub-assemblies into position along the y axis.\n\nAnd there we have it, the interior of the model exposed for people to interact with. There is a lot I've covered today from showing you how to prepare an asset hierarchy, manipulate its parts, use a clipping plane to look through a complex assembly, and even expand the parts out along an axis to give you a detailed view of the tiniest part of your assembly.\n\nThis workflow can help you build design review apps that can greatly enhance people's productivity.\n\nDownload and explore the sample project from developer.apple.com I'd highly encourage you to familiarize yourself with concepts in statistics, vector math, and linear algebra. Those are the parts of math we leaned heavily on today, hopefully you find them a little less scary now than you did before.\n\nIf you'd like to control models in real time from your Mac app, the spatial preview framework might be a great choice for you.\n\nDo check out the session \"Discover the spatial preview framework\" to learn more. Additionally, you could even augment a physical object like a race car simulator cockpit and overlay your virtual content on top of the simulator and explore its internal structure with what you learned in this session.\n\nTo learn more about augmenting physical objects please check out the \"Explore enhancements to visionOS object tracking\" session.\n\nThanks again for your attention today and I look forward to seeing the cool stuff you do with these ideas in your apps.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "7:10",
+ "title": "Opening an assembly",
+ "language": "swift",
+ "code": "func openAssembly() {\n components[ManipulationComponent.self] = nil\n components[InputTargetComponent.self] = nil\n\n for child in assemblyChildren {\n child.components.set(InputTargetComponent())\n\n var manipulation = ManipulationComponent()\n manipulation.releaseBehavior = .stay\n child.manipulationComponent = manipulation\n }\n}"
+ },
+ {
+ "timestamp": "7:11",
+ "title": "Closing an assembly",
+ "language": "swift",
+ "code": "func closeAssembly() {\n for child in assemblyChildren {\n child.manipulationComponent = nil\n child.components[InputTargetComponent.self] = nil\n }\n\n components.set(InputTargetComponent())\n var manipulation = ManipulationComponent()\n manipulation.releaseBehavior = .stay\n manipulationComponent = manipulation\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Manipulating models with RealityKit",
+ "url": "https://developer.apple.com/documentation/RealityKit/manipulating-models-with-realitykit"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/284/4/fa1d15b1-3f28-415a-907a-8ae1bb344494/downloads/wwdc2026-284_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/284/4/fa1d15b1-3f28-415a-907a-8ae1bb344494/downloads/wwdc2026-284_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "282",
+ "year": "2026",
+ "title": "Discover the Spatial Preview framework",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/282"
+ },
+ {
+ "id": "283",
+ "year": "2026",
+ "title": "Explore enhancements to visionOS object tracking",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/283"
+ },
+ {
+ "id": "274",
+ "year": "2025",
+ "title": "Better together: SwiftUI and RealityKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/274"
+ },
+ {
+ "id": "318",
+ "year": "2025",
+ "title": "Share visionOS experiences with nearby people",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/318"
+ },
+ {
+ "id": "287",
+ "year": "2025",
+ "title": "What’s new in RealityKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/287"
+ },
+ {
+ "id": "10186",
+ "year": "2024",
+ "title": "Optimize your 3D assets for spatial computing",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10186"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:18.826Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-285.json b/data/wwdc/videos/2026-285.json
new file mode 100644
index 0000000..442b693
--- /dev/null
+++ b/data/wwdc/videos/2026-285.json
@@ -0,0 +1,64 @@
+{
+ "id": "285",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/285/",
+ "title": "Discover USDKit and what’s new in OpenUSD",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Welcome to Discover USDKit, and what's new in OpenUSD. I'm Lee, an engineer on Apple's Spatial Standards team.\n\nUSD is the backbone of the spatial experiences Apple creates and how we represent spatial scenes.\n\nFrom the apps you create, to the content you see across our platforms, all these spatial experiences are built on top of USD.\n\nUSD was created by Pixar, and Apple was early to recognize its potential. We've been collaborating with them ever since.\n\nToday, we're taking that even further. I'll be covering the latest advancements in USD on our platforms and a powerful new framework called USDKit and how it can bridge between Mac and Vision Pro for seamless 3D experiences.\n\nLets start off by looking at the foundational updates we've made to USD on our platforms.\n\nUSD has become the common 3D language across many industries. Award-winning films, AAA games, factory floors, surgical suites, autonomous vehicles, AI-driven simulations; and the reach continues to expand.\n\nApple is leading the charge in making USD the foundation for the next generation of spatial experiences.\n\nThe open source project behind USD is called OpenUSD, the industry-standard library for describing 3D scenes originally pioneered by Pixar. But USD doesn't work alone.\n\nIt integrates with MaterialX, originally from Lucasfilm for rich material descriptions, and new this year, OpenVDB, originally from DreamWorks, bringing volumetric data into the mix. Each of these technologies was built by world-class visual effects and animation studios, and together, they form a powerful, composable foundation for 3D.\n\nAcross all our platforms this year, we've updated all three, so you can take advantage of the latest improvements in each.\n\nApple is a member of the Academy Software Foundation, the home of open source projects like MaterialX and OpenVDB.\n\nThrough the Foundation, we actively contribute to these projects, ensuring they evolve in ways that benefit developers and creators across not just our platforms, but the 3D ecosystem as a whole.\n\nBeyond the code, Apple is a founding member of the Alliance for OpenUSD, working to make USD a true industry standard, not just in practice, but on paper.\n\nThis year, we helped release the first formal specification for the core of USD, with domain specifications for geometry, materials, and physics already underway.\n\nThis is an open effort, and these working groups are where the future of USD gets decided. If you want a voice in shaping how the industry builds and exchanges 3D content, we encourage you to get involved.\n\nGaussian Splats are one of the most exciting recent breakthroughs in 3D representation.\n\nRather than using traditional geometry, splats capture a scene as millions of fuzzy, overlapping particles, each encoding position, color, and opacity to faithfully reconstruct complex real-world environments.\n\nAs you can see in this visualization, splats are able to capture incredibly subtle lighting responses and are capable of bringing real-world scenes to life.\n\nWorking with our Alliance for OpenUSD partners, including NVIDIA, Adobe, and Pixar, we are introducing a new USD primitive type, Particle Fields, which is capable of describing Gaussian Splats as well as other representations from this rapidly evolving area of research.\n\nThis brings Gaussian Splats into the same scene as your meshes, materials, and other traditional 3D data for the very first time.\n\nLet's take a look at powerful new experiences across our platforms and the new USDKit framework built to power them.\n\nUSDKit takes care of the heavy lifting, so you can stay focused on your content and applications. All of this is made possible by the standardization work we have been building towards.\n\nPreview has long been the go-to place on Mac for viewing images, PDFs, and 3D content, and for images and PDFs, it has always offered a powerful set of editing tools right out of the box - no extra software needed.\n\nThis year, we are bringing that same philosophy to 3D.\n\nPreview now brings essential 3D editing to your Mac, covering the operations you reach for most.\n\nYou can manipulate objects directly in the scene, edit properties and lighting, work with full scene hierarchies, and convert and compress assets, all without needing to learn a dedicated 3D application.\n\nWhile it's simple on the surface, it's all backed by a production quality rendering and processing pipeline.\n\nPreview and Quick Look on Mac now give you a choice of renderer. RealityKit brings consistency across Mac, iPhone, iPad, and Vision Pro, and Storm remains available for those with existing production pipelines needs.\n\nFor your most complex scenes, Preview adds a brand new Raytracer for stunning, high fidelity results.\n\nAll three support OpenPBR, a significant upgrade over USDPreviewSurface that brings richer, more physically accurate materials to your workflow.\n\nThe new Raytracer in Preview is built for scenes that demand more. Whether you are visualizing architectural spaces or preparing product imagery, it delivers accurate reflections, precise shadows, and physically correct lighting.\n\nA production quality ground-truth renderer on every Mac.\n\nPreview integrates with the new Spatial Preview framework on macOS 27, creating a direct connection between your Mac and Quick Look on Vision Pro.\n\nAs you work on your USD scene in Preview, changes are visible live in Quick Look on the Vision Pro, right in your own space, and through SharePlay, you can bring an entire team in at once. Creative directors and artists can walk around the same scene together, reviewing lighting, composition, and spatial scale in real time.\n\nAnd now, with the Spatial Preview framework, this kind of collaborative spatial workflow is easier than ever to bring to your own Mac apps. Check out the Spatial Preview session to learn how.\n\nUSD is now part of the web too.\n\nSafari introduces the Model tag, bringing 3D content to web pages as naturally as images or video.\n\nEmbed a USD model in your page, and on macOS and iOS, your users get a fully interactive 3D experience right in the browser.\n\nAnd on visionOS, it goes even further. That same model breaks out of the page and is presented, spatially, right in the user's space.\n\nEverything we have shown you today is powered by a brand new system framework: USDKit. Let's take a look.\n\nUSDKit brings first class USD support to your Swift apps, with deep integration for RealityKit and Spatial Preview built right in. We designed USDKit to work for everyone. Developers who already know USD will find the concepts immediately feel familiar, and for Swift developers coming to USD for the first time, USDKit meets you where you are, with patterns and paradigms you already know. Before I dive in to an example, let's cover a few key USD concepts. In USD, a Layer is a single data file. Layers can be combined together through a powerful feature called Composition. And a Stage is the composed result of one or more layers. This is your window into the full scene. Everything in a scene is represented as a USD Prim. Each prim has a Schema, which defines its type.\n\nPrims also carry Attributes, which hold the actual data and Metadata, which describes information about the prim itself. Now I've covered some key concepts; let's walk through an example of how to use USDKit in practice. I will load a stage, make some modifications, and export the result. Getting started with USDKit begins with a stage. I can create a fresh one in memory with a simple USDStage initializer. For this demo though, I have an existing scene I want to work with. I can open it by passing in a file URL to USDStage.open. Since this involves file access, it can throw, so I use try. And there it is. My scene loads up and it's already looking great. I thought there was supposed to be an oscilloscope on the bench. Let's see if it's in the scene somewhere, and if not, I can add it in.\n\nFirst, let me traverse the stage hierarchy to see if the oscilloscope is already in there. No luck! So I define a new transform prim at the path I want it to live at. Now here is where things get really useful. Rather than copying all the data for the asset directly into my stage, I can add a light-weight reference to it. The asset lives in its own file, or layer, authored by someone else entirely, and I am simply pulling it in. This is the power of composition. Everyone works on their own piece of the scene, and USD brings it all together. And the best part? Any updates they make automatically show up in my stage too, because I am just referencing in their file. Great! The asset is in the scene, but as you can see, it's not quite where I want it to be. Let me show you how I can move it into the right place.\n\nTo move the prim, I first call addTransformOperation, which takes care of creating the correct attributes on the prim and updating the transform order automatically.\n\nAfter that all I need to do is to set the translation value, and the asset moves up on to the workbench, right where I want it to be.\n\nPerfect! This is looking much better already, but before I share this with the world, I want to make sure it's ready for as wide an audience as possible.\n\nGreat 3D experiences should be accessible to everyone. At Apple, accessibility is a core value, and that includes spatial content too.\n\nThat is why we have driven the standardization of accessibility metadata directly in USD, establishing how assistive labels and descriptions are defined on 3D objects across the industry. We have designed it with flexibility built in, so it can evolve in the future.\n\nSince it is native to USD, you can author it through any USD API, and to make it as easy as possible to adopt, we have added direct support right in Blender and Maya.\n\nTo add the accessibility data to my new asset, I first apply the AccessibilityAPI schema to the prim. This adds the necessary metadata to signal that the schema is present.\n\nSince USDKit does not provide all the schema-specific APIs, I create the label and description attributes directly, making sure to use the correct attribute names as defined in the specification.\n\nWith those in place, I can set their values: a concise label and a rich description that gives assistive technologies everything they need to understand the object in context. The asset is now ready, but high quality production USD scenes can grow to many gigabytes in size. The ALab scene we have been working with today is a great example of that. Sharing something this large is not always practical, so let me show you how I can shrink it down.\n\nIn collaboration with the Alliance for Open Media, we have added support for a state of the art mesh compression codec capable of reducing mesh sizes by up to 90%.\n\nCombined with our existing texture compression using AVIF, the numbers speak for themselves. The average asset is now seven times smaller, without compromising visual quality.\n\nSmaller assets mean faster delivery, lower storage costs, and a better experience for users across every platform.\n\nUSDKit support for compression is built right into the exportPackage API. I pass in the output URL to the exportPackage method on my stage, and then I enable the texture and mesh compression through the export options. A couple of lines and I am done. And if you're not writing code, you can get the same results directly in Preview or through the usdcrush command line tool. We are working with Pixar to bring this compression support to the OpenUSD project, so the whole ecosystem can benefit. Because integrating USD can be complex, there are a number of different ways to access the technology. For app developers on our platforms, USDKit is the way to go. It is system provided, deeply integrated, and everything we have shown today is built on top of it.\n\nFor those with more advanced needs or cross-platform workflows that go beyond what USDKit covers, we have you taken care of too. SwiftUSD brings open source Swift bindings available right through the Swift Package Manager, and for cross-platform C++ codebases, we have made it easier than ever to embed OpenUSD directly as a framework.\n\nWhatever path fits your workflow, they are all built on top of the same foundation. That means your USD files move freely between all of them.\n\nLet's recap what we've covered. USD on Apple platforms has taken a big step forward this year. Preview now brings essential 3D editing and powerful rendering options to your Mac. Spatial Preview allows you to easily immerse yourself in your content and share it with others, and the new Model tag in Safari brings USD natively to the web. At the center of it all is USDKit, a new system framework that makes working with USD in Swift a first class experience. If you are looking to author and work with 3D content in your app, USDKit is the best place to start.\n\nThere's a lot more to explore this year. We have sessions diving deeper into Spatial Preview, bringing USD to the web, and building rich spatial experiences with RealityKit and Reality Composer Pro. Whatever your workflow, there is something here for you. Check out the linked resources for more information.\n\nUSD has never been more powerful, and we cannot wait to see what you create with it. Thank you for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "8:12",
+ "title": "Opening a USD Stage",
+ "language": "swift",
+ "code": "import USDKit\n\n// Create a new empty in-memory stage\n\nlet stage = USDStage()\n\n// Open a stage from a file on disk\n\nlet url = URL(fileURLWithPath: \"/ALab/entry.usda\")\nlet stage = try USDStage.open(url)"
+ },
+ {
+ "timestamp": "8:44",
+ "title": "Traversing the Stage Hierarchy",
+ "language": "swift",
+ "code": "// Traverse all prims looking for the oscilloscope\nfor prim in stage.descendants {\n if prim.name == \"scope\" {\n // There it is! 🔬\n }\n}\n\n// It wasn't there — define a new Xform prim for it\n\nlet scope = stage.definePrim(at: \"/World/scope\", type: “Xform\"))\n \n// Add a file reference to the prim\n\ntry scope.references.add(“/ALab/assets/scope.usda”)"
+ },
+ {
+ "timestamp": "9:36",
+ "title": "Moving a Prim with a Transform Operation",
+ "language": "swift",
+ "code": "// Creates xformOp:translate and updates xformOpOrder automatically\n\nscope.addTransformOperation(type: .translate)\nscope[\"xformOp:translate\", as: USDValue.Vec3d.self] = [2.5, 0.0, -1.0]"
+ },
+ {
+ "timestamp": "10:42",
+ "title": "Applying Accessibility Metadata",
+ "language": "swift",
+ "code": "// Apply the multi-apply AccessibilityAPI schema with instance name \"default\"\n\ntry scope.applyAPISchema(\"AccessibilityAPI\", instanceName:\"default\")\n\n// Create the label and description attributes\n\nscope.makeAttribute(named: \"accessibility:default:label\", as: .string)\nscope.makeAttribute(named: \"accessibility:default:description\", as: .string)\n\n// Set their values\n\nscope[\"accessibility:default:label\", as: String.self] = \"Oscilloscope\"\nscope[\"accessibility:default:description\", as: String.self] = \n \"Vintage signal analyzer with a 3D wireframe display, topped by a color bar test monitor\""
+ },
+ {
+ "timestamp": "12:05",
+ "title": "Exporting with Mesh and Texture Compression",
+ "language": "swift",
+ "code": "let output = URL(fileURLWithPath: \"/ALab/alab_compressed.usdz\")\n\n// Export the stage as a USDZ package\n\ntry stage.exportPackage(\n to: output,\n options: [\n .preferSmallTextureFiles(quality: .standard), // compress textures\n .preferSmallMeshFiles // compress mesh geometry\n ]\n)"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/285/4/335150b6-c2b8-4711-a632-45a34d449eac/downloads/wwdc2026-285_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/285/4/335150b6-c2b8-4711-a632-45a34d449eac/downloads/wwdc2026-285_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "282",
+ "year": "2026",
+ "title": "Discover the Spatial Preview framework",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/282"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:18.481Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-286.json b/data/wwdc/videos/2026-286.json
new file mode 100644
index 0000000..7960701
--- /dev/null
+++ b/data/wwdc/videos/2026-286.json
@@ -0,0 +1,89 @@
+{
+ "id": "286",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/286/",
+ "title": "Use foveated streaming to bring immersive content to visionOS",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi I'm Adrian and I'm an engineer at Apple.\n\nvisionOS allows you to bring people to new worlds and create rich spatial applications. Some existing spatial apps require an external device, such as a PC. Or they're built with alternative technologies like OpenXR. Now there's a new way to enhance these apps for Apple Vision Pro.\n\nIt's called Foveated Streaming! Foveated Streaming helps Apple Vision Pro connect to external devices, like a PC, to stream OpenXR content.\n\nvisionOS automatically sends input data, like hands, controller positions, and microphone. And the device streams your OpenXR content as video and audio.\n\nWe introduced the Foveated Streaming framework in visionOS 26.4 and developers have already created some amazing experiences with it. For example, X-Plane 12 from Laminar Research brings a premium flight simulation experience to Apple Vision Pro. The X-Plane app on visionOS uses ARKit to understand your space and equipment, and streams the simulated experience from a PC. It feels incredible to interact with a physical flight simulator while fully immersed in the virtual world. You can look around the cockpit, or out the side windows. visionOS seamlessly blends the real and the virtual to deliver an immersive and effective simulation.\n\niRacing, a motorsport racing game for PC, delivers an incredible sim racing experience on Apple Vision Pro.\n\nThe iRacing Connect app uses ARKit hand tracking to match the position of your physical racing wheel with the virtual cockpit. So, you can see your hands holding the virtual wheel! And Innoactive brings Autodesk VRED to Apple Vision Pro. This enables designers and engineers to visualize massive 3D assets and simulations.\n\nIn this video, designers at Kia use this technology to see their cars at one-to-one scale, and in rigorous detail. So, Foveated Streaming already enables some fantastic experiences on Apple Vision Pro. And the video quality is incredible, because our system intelligently optimizes the video stream.\n\nFoveated Streaming compresses video based on where a person is looking. We use the eye tracking technology in Apple Vision Pro to stream things you're focused on in higher detail. This sophisticated stream processing is built in to visionOS, so we've done the heavy lifting for you. It all happens so quickly that you don't even notice! Under the hood, visionOS comes with NVIDIA CloudXR™ streaming, built-in. This means that your immersive content is streamed with a high performance streaming protocol and with minimal latency. CloudXR™ is so performant that you can stream immersive content without any cables attached, over Wi-Fi. It can stream content from a local PC over your home network, or from the cloud.\n\nYour apps can also integrate with native visionOS frameworks. So you can build user interfaces with SwiftUI, you can mix your content and the real world with ARKit, and you can combine on-device RealityKit rendering with your streamed content. It's super easy to use. We've found that you can start streaming your OpenXR application in just one day. And in a week, you can enhance your application with capabilities that you can only find on visionOS. So let's talk all about it! First, I'll describe how Foveated Streaming works, and show you how to get started in just a few hours.\n\nNext, we will set up a visionOS app to display the streamed content.\n\nThen we'll set up your OpenXR client so that it can stream to Apple Vision Pro. And finally, I'll talk about how you can enhance your application with capabilities only possible on visionOS.\n\nSo let's get started! Here is a block diagram that outlines the frameworks you will build on. Both for visionOS and for your streaming endpoint. Your OpenXR app should implement Apple's Foveated Streaming Protocol, which manages pairing with the device.\n\nAlso, your app should use the OpenXR runtime provided by the NVIDIA CloudXR™ SDK.\n\nOn visionOS, your app uses the FoveatedStreaming framework to connect to the streaming endpoint.\n\nAnd you can integrate with other visionOS frameworks such as ARKit, SwiftUI, and RealityKit.\n\nLet's begin by setting up your Streaming endpoint. We've provided an open source, end to end example on Apple's Github page.\n\nOur windows sample code contains a reference implementation of Apple's Foveated Streaming Protocol. We have also provided an example OpenXR application and helpful guides to help you set up the NVIDIA CloudXR™ runtime.\n\nSo to get started, check out our Github page. And feel free to copy our reference implementation! This can help you set everything up in an afternoon. Next, let's talk about how to build a visionOS receiver app.\n\nThe visionOS app you create welcomes people into your experience.\n\nIt uses the FoveatedStreaming.framework to connect to streaming endpoints.\n\nAnd most importantly, it adds unique features to your experience by leveraging the great frameworks in visionOS.\n\nTo get started, download our visionOS sample on developer.apple.com. You can use our sample code, both for visionOS and for Windows, to get your application streaming in an afternoon.\n\nFor the remainder of the talk, I'll dig into how these samples work. To begin, let's talk more about the receiver app. The Foveated Streaming framework uses a session-based API. So your app should create a FoveatedStreamingSession.\n\nWhen you call connect, the framework will automatically present a list of endpoints your app can connect to. Apple Vision Pro must pair with your endpoint before beginning the stream. To do this, the endpoint presents a QR code with pairing information. We'll talk more about how to properly present this later. When the code is on screen, the framework automatically presents a user interface to scan. You scan the code by looking at it. Once you're connected, it's time to present the streamed content. You can do this with SwiftUI.\n\nOn visionOS, you present spatial content with an ImmersiveSpace. If you pass a FoveatedStreamingSession to your ImmersiveSpace, it will include the streamed content.\n\nYou can add additional windows, just like any other SwiftUI application. For example, our sample app adds a window to the scene to pause and resume the session.\n\nIt also adds views to the ImmersiveSpace itself, like this widget to re-open the main window.\n\nAnd finally, it configures the immersive space to have a progressive immersion style. Progressive immersion is a great fit for Foveated Streaming. It allows people to view your experience through a portal, grounded in their physical environment. In fact, you have access to all of SwiftUI's features when using Foveated Streaming. So your streaming client can use native spatial gestures, and the visionOS look and feel.\n\nYou can add SwiftUI windows to your app, including volumetric windows. And you can use immersion styles, like progressive immersion. If you're new to SwiftUI, there are a ton of resources to help you get started. I like the talk \"Get started with building apps for spatial computing\" from WWDC23. Now let's look at what your streaming endpoint needs to do to integrate with Foveated Streaming. I mentioned before that your OpenXR app needs to do two things.\n\nFirst, it should implement the Foveated Streaming Protocol, which handles authentication and pairing.\n\nSecond, it should use the OpenXR runtime provided by the NVIDIA CloudXR™ SDK.\n\nLet's begin by talking about the Foveated Streaming Protocol. This is a lightweight, TCP-based connection which is established separately from the streaming connection. It helps to authenticate the secure foveated stream. It also communicates session state between visionOS and the endpoint. For details on the protocol format, see our article on developer.apple.com.\n\nWe've also provided a reference implementation of the protocol on our Github page, so you can use that to get started.\n\nFor example, here's what happens when your app calls connect().\n\nEndpoints become visible to your local network with Bonjour.\n\nWhen someone selects it, visionOS establishes a connection.\n\nThen, pairing occurs. And once that's done, the stream begins, and connect() returns.\n\nLet's dive in to the specific messages that are sent and received during barcode pairing. Messages in the Foveated Streaming Protocol are JSON-encoded. The protocol uses a request-acknowledge pattern. First, visionOS requests a connection, and if unpaired, it requests a pairing barcode. The barcode is also JSON-encoded.\n\nIt contains two things: a client token, and a hash of the secure connection's certificate. Both of these are provided by the NVIDIA CloudXR™ SDK.\n\nvisionOS will update your endpoint on the session status.\n\nAnd once the endpoint reports the content is ready, streaming begins.\n\nIt's important that you monitor the session status to have a stable user experience.\n\nFor example, when you take off the device, it goes to sleep.\n\nJust before, it informs your endpoint that the streaming session is paused.\n\nWhile asleep, all connections are severed.\n\nYou should keep the endpoint available so that someone can reconnect when they put the device back on. Now, let's talk about how NVIDIA CloudXR™ integrates with your OpenXR client. NVIDIA CloudXR™ provides an OpenXR runtime for windows. Your OpenXR application automatically connects to this runtime, and CloudXR™ handles the streaming details.\n\nCloudXR provides visionOS input data to OpenXR automatically. For example, you can use the OpenXR extension for hand tracking.\n\nPlayStation VR2 Sense Controllers also pass through.\n\nWe recommend using a depth buffer for the best experience. Also, provide an alpha channel to mix your content with the user's surroundings. For more information, see our article on developer.apple.com. You can also consult our sample code on Github to understand how to set up NVIDIA's runtime. Now that you have everything set up, let's talk about how to evaluate the performance of your streamed experience. We've provided an instrument in Xcode to measure statistics of the stream. The Foveated Streaming instrument informs you about the stream's bandwidth, pose latency, frame rate, and more. You can use this to diagnose issues with the streamed content.\n\nTo learn more, see our article on developer.apple.com. Now that you have your streaming client up and running, I'll talk about some ways that you can enhance the experience with features you can only find on visionOS.\n\nAs we've seen, your streaming experience is composed of two apps: the receiver app on visionOS and the host app on your streaming endpoint.\n\nThe Foveated Streaming framework allows them to communicate with the message channels API.\n\nOn visionOS, message channels are an API on FoveatedStreamingSession.\n\nAnd CloudXR provides an OpenXR extension for message channels.\n\nThese messages are completely Opaque Data blobs, so you can send whatever you want! For example, you can present a SwiftUI interface to select a level in your game, and send a command to the OpenXR application to initiate the load.\n\nAnd while it's loading, the OpenXR app can report its progress.\n\nOr, your visionOS app can send ARKit data, to align the scene in the users's real space.\n\nX-Plane 12 is a great example of this. The visionOS app uses ARKit to find the location of a physical flight simulator. It uses message channels to sync this to the PC, so the real and the virtual are perfectly aligned.\n\nIt's easy to use ARKit and message channels to enhance your OpenXR experience. FoveatedStreamingSession offers an API to convert between OpenXR and ARKit coordinate frames. As a reminder, controller and hand tracking are already built in! And use an alpha channel, to blend your content with a person's environment. You can also compose your streamed content with native RealityKit rendering! To do this, simply add a RealityView to your ImmersiveSpace. RealityKit content seamlessly composes with your streamed content. And if you supply depth to your OpenXR scene, the content will even occlude each other! And that concludes our session. I invite you to download and review our sample code, which makes it super easy for you to get started. Try to set up your own receiver app, and integrate your OpenXR client with our protocol.\n\nHave fun, and enjoy the rest of WWDC!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "6:03",
+ "title": "Connect to a streaming endpoint",
+ "language": "swift",
+ "code": "// Connect to a streaming endpoint\n\nimport SwiftUI\nimport FoveatedStreaming\n\nstruct ConnectView: View {\n let session: FoveatedStreamingSession\n\n var body: some View {\n Button(\"Connect\") {\n Task {\n try await session.connect()\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "6:44",
+ "title": "Display a Foveated Streaming session in your immersive space",
+ "language": "swift",
+ "code": "// Display a Foveated Streaming session in your immersive space\n\nimport SwiftUI\nimport FoveatedStreaming\n\n@main struct FoveatedStreamingSampleApp: App {\n private let session = FoveatedStreamingSession()\n\n var body: some SwiftUI.Scene {\n ImmersiveSpace(foveatedStreaming: session)\n }\n}"
+ },
+ {
+ "timestamp": "6:55",
+ "title": "Compose SwiftUI content with Foveated Streaming",
+ "language": "swift",
+ "code": "// Compose SwiftUI content with Foveated Streaming\n\nimport SwiftUI\nimport FoveatedStreaming\n\n@main struct FoveatedStreamingSampleApp: App {\n private let session = FoveatedStreamingSession()\n private let appModel = AppModel()\n\n var body: some SwiftUI.Scene {\n Window(\"Main\", id: appModel.mainWindowId) {\n ContentView(session: session)\n .environment(appModel)\n .environment(session)\n // ...\n }\n \n ImmersiveSpace(foveatedStreaming: session) {\n SpatialContainer {\n ReopenMainWindowView().environment(appModel)\n TransformStreamWidgetView().environment(session)\n }\n }\n \n }\n}"
+ },
+ {
+ "timestamp": "13:42",
+ "title": "Compose RealityKit content with Foveated Streaming",
+ "language": "swift",
+ "code": "// Compose RealityKit content with Foveated Streaming\n\nimport SwiftUI\nimport RealityKit\nimport FoveatedStreaming\n\n@main struct FoveatedStreamingSampleApp: App {\n private let session = FoveatedStreamingSession()\n private let appModel = AppModel()\n\n var body: some SwiftUI.Scene {\n ImmersiveSpace(foveatedStreaming: session) {\n RealityView { content in\n // ...\n }\n }\n\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Analyzing the performance of a foveated streaming session",
+ "url": "https://developer.apple.com/documentation/FoveatedStreaming/analyzing-the-performance-of-a-foveated-streaming-session"
+ },
+ {
+ "title": "Establishing foveated streaming sessions with Apple Vision Pro",
+ "url": "https://developer.apple.com/documentation/FoveatedStreaming/establishing-foveated-streaming-sessions-with-apple-vision-pro"
+ },
+ {
+ "title": "Streaming a CloudXR application to Apple Vision Pro with foveation",
+ "url": "https://developer.apple.com/documentation/FoveatedStreaming/streaming-a-cloudxr-application-to-apple-vision-pro-with-foveation"
+ },
+ {
+ "title": "Creating a foveated streaming client on visionOS",
+ "url": "https://developer.apple.com/documentation/FoveatedStreaming/creating-a-foveated-streaming-client-on-visionos"
+ },
+ {
+ "title": "Foveated Streaming",
+ "url": "https://developer.apple.com/documentation/FoveatedStreaming"
+ },
+ {
+ "title": "StreamingSession: Streaming immersive content from a CloudXR™ application to visionOS and iOS",
+ "url": "https://github.com/apple/StreamingSession"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/286/4/fa302edd-f95a-49f4-b51c-3899d49c6dec/downloads/wwdc2026-286_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/286/4/fa302edd-f95a-49f4-b51c-3899d49c6dec/downloads/wwdc2026-286_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "287",
+ "year": "2026",
+ "title": "Build next-generation experiences with visionOS 27",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/287"
+ },
+ {
+ "id": "8004",
+ "year": "2026",
+ "title": "visionOS Group Lab",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8004"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:18.765Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-287.json b/data/wwdc/videos/2026-287.json
new file mode 100644
index 0000000..c22973a
--- /dev/null
+++ b/data/wwdc/videos/2026-287.json
@@ -0,0 +1,99 @@
+{
+ "id": "287",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/287/",
+ "title": "Build next-generation experiences with visionOS 27",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hi, my name is Norman, and I'm a director in the Vision Products Group. In this session, I'll take you through powerful new ways to build next-generation experiences with visionOS 27. Looking back over this past year, we've been blown away by what you've built on the platform. We've seen apps like YouTube, introducing world-class content in your living room, Valve, bringing Steam Link to Apple Vision Pro to seamlessly stream your 2D Mac and PC games on the infinite canvas, and Resolution Games, reimagining new ways to relive classic experiences. We've also seen companies like Kia and Innoactive using Autodesk VRED, Nvidia's CloudXR SDK, and the new Foveated Streaming framework to design next generation vehicles, and Laminar Research, blending professionals' digital and physical worlds to deliver powerfully immersive training solutions with X-Plane. Bringing these experiences to life is just the beginning, and the insights you've shared along the way have been invaluable. Your feedback continues to shape the future of visionOS, guiding the features we release and accelerating the next era of computing. Today, people aren't just working faster, they're inventing entirely new experiences and workflows. To make these breakthroughs possible, the latest generation of Apple Vision Pro is powered by the M5 chip — delivering desktop-class compute for real-time rendering. Ultra high-resolution displays provide over 4K pixels per eye, enabling stunning visual fidelity. The system tracks your hand movements at 90Hz for highly responsive, low-latency control.\n\nAnd with deep integration across the Apple ecosystem, your apps and games feel seamlessly connected across all your Apple devices. Along with powerful hardware and platform capabilities, visionOS provides multiple ways to render your app's content… In the Shared Space, you can choose to render your app in a window or a volume, where your experience can coexist simultaneously with other applications. This allows you to organize your workspace across the infinite canvas, taking productivity to the next level.\n\nOr, you could launch an Immersive Space where your app runs exclusively, allowing you to render 2D and 3D content anywhere in your field of view.\n\nIn the Immersive Space, your content blends with the physical world, providing three immersion styles to choose from: The Mixed immersion style allows you to anchor 3D objects in real-world surroundings, like placing an object on a table.\n\nThe Progressive style lets your audience dial in the level of immersion that feels right for them. Or you can enable Full immersion, to transport them into a completely virtual world.\n\nTogether, these scene types offer a spectrum of immersion for your content.\n\nWhen building experiences on visionOS, there are three main paths to choose from, depending on your workflow. The first path is for existing iOS or iPadOS apps. If you already have an app or a game running on iPad or iPhone, it can very likely run on Apple Vision Pro today, with minimal changes to your code.\n\nCompatibility is the simplest way to get onto the platform quickly — you can just check a box in App Store Connect! Or, you can recompile your iOS or iPadOS app to visionOS by adding visionOS as a deployment target in your Xcode settings.\n\nBoth of these paths offer low-friction ways to bring your experiences to visionOS.\n\nThe second path is for apps designed from the ground up for spatial computing. This allows your experience to seamlessly blend with and react to people's environments.\n\nIn this vertical, you have 2 options: You can build for the platform using native frameworks like SwiftUI, RealityKit, and tools like Reality Composer Pro, or use your own custom rendering engine with CompositorServices. Or, if you prefer to use a third-party game engine like Unity, Unreal, or Godot, these all support visionOS as a platform.\n\nIn addition to these options, we are introducing a third path that offers a way for you to bring your existing experiences on macOS or PC, to visionOS.\n\nIf you have a Mac app that renders spatial content today, visionOS 27 adds a powerful new tool with the Spatial Preview framework , allowing you to extend images, documents, and 3D content directly from your Mac into Apple Vision Pro. Alternatively, if you are bringing a 3D experience from a PC, the Foveated Streaming framework lets you display native spatial content alongside streamed content. For example, a flight simulator crew rendered a highly detailed cockpit using our native rendering framework, RealityKit, while streaming a processor-intensive landscape from a remote computer. I'll dive deeper into both of these technologies later in this session. Together, these paths give you flexibility to build incredible experiences for visionOS, no matter where you are starting from. I'll share some of the latest updates to these pathways.\n\nFirst, I'll cover new capabilities in RealityKit and Reality Composer Pro that help you bring rich 3D experiences to life, with accelerated workflows.\n\nI'll share updates on third party game engines like Unity, Unreal, and Godot.\n\nThen, I'll take you through Spatial Preview, a feature that extends 3D content from your Mac app, into the immersive world of visionOS.\n\nI'll also introduce the new Foveated Streaming framework that enables you to stream immersive content from a PC or cloud instance.\n\nI'll cover incredible new ways to interact with your content. And finally, I'll share updates to the immersive media pipeline to help you tell even more engaging stories on Apple Vision Pro.\n\nLet's begin with updates to RealityKit and Reality Composer Pro.\n\nRealityKit is the rendering engine behind some of the best native experiences on visionOS. Reality Composer Pro is where you author content visually, letting you design, iterate, and preview without ever leaving the editor.\n\nWith visionOS 27 we're bringing the two even closer together.\n\nWith RealityKit you can build interactive, high-fidelity spatial experiences that seamlessly blend with your real world and visionOS 27 adds powerful new features in RealityKit to make your experiences more immersive than ever.\n\nLet's take a look at a few of them: RealityKit's new physical space lighting deepens immersion by blending your virtual lighting with the real world. Here's a virtual planetarium projector, built in RealityKit. The stars and nebulae shine across the space, and as the projector spins, its light seamlessly conforms to every surface. The stars and nebulae are powered by the new Projective Textures API, enabling you to add textures to your spotlights. With this feature, you can simulate stunning effects like stained glass projections or underwater caustics.\n\nNext, let's look at RealityKit's powerful new Cloth Simulation. Here, a virtual mannequin wears a flowing dress made of highly realistic, simulated clothing. As the mannequin walks, the fabric moves and folds naturally, all rendered in real time. That same simulation also brings the virtual bed cover to life. As it's pulled back, the fabric responds with realistic weight and drape. RealityKit doesn't stop at visual realism. With the new Custom Reverb Mesh, it brings that same authenticity to spatial audio. Take this virtual band playing in a museum. With RealityKit, the sound doesn't just originate in front of me, it fills the space from every direction. The reverb system accurately simulates how sound is absorbed and scattered by the wood, metal, and stone in the hall, giving your virtual world an even richer, more believable sense of presence. Another powerful new feature in RealityKit is Gaussian Splatting. Here's a small potted plant, scanned and rendered as a 3D Gaussian splat. Every detail comes through, down to the texture of the soil. With RealityKit, you can now scan real-world objects and bring them straight into your virtual experiences. It's a great way to capture objects that are hard to model by hand and use them to build incredibly realistic worlds.\n\nTo learn about even more features coming to RealityKit, check out the \"Explore advances in RealityKit\" session. While RealityKit APIs enable you to create stunning, interactive 3D content, the process of putting that content together is greatly enhanced by Reality Composer Pro. We're adding brand new tools and features in a major refresh, enabling fast, AI-powered, and collaborative workflows to the new Reality Composer Pro 3, so you can move faster and go further, without always needing to touch Xcode.\n\nThe new Reality Composer Pro brings a huge selection of new features to transform your workflows.\n\nI'll take you through some of these brand-new capabilities, like Reality Composer Pro Assistant, Animation Graph, Script Graph, and Navigation Meshes. Let's start with Reality Composer Pro Assistant, which integrates AI-capabilities into your creative workflow, right inside the editor. Here's an example that shows how the AI assistant can be used to generate a dried fruit assortment, and then place them in a pre-existing empty bowl in the scene.\n\nIt even generated 3D candles and placed them on the table. With Reality Composer Pro Assistant, you describe what you want, and it generates 3D models with detailed textures and materials, ready to place in your scene. It's a great way to start telling your story, long before your final game assets are ready to ship.\n\nOnce your final assets and animations are ready, Animation Graph in Reality Composer Pro lets you control how they transition between states.\n\nWith a state machine, you can easily transition between idle and walking states at runtime, and visualize that transition live in the editor. And — to help your character navigate around an environment while avoiding obstacles — you can generate and tweak a navigation mesh, right inside Reality Composer Pro! Simply start with an auto-generated mesh, as shown in blue, then add additional features such as jumps, ladders, and obstacles to complete the course.\n\nTo connect these pieces and make your scene interactive, Reality Composer Pro also brings Script Graph. With Script Graph, you can add nodes that catch events like taps, driving where your character should move. You can make real-time edits with live preview on Apple Vision Pro, all without opening Xcode.\n\nNow, with everything in place, your character can find its way to its destination, avoiding obstacles and using its full animation set to walk, run, or climb.\n\nReality Composer Pro's node-based editor called Script Graph lets you build logic without ever leaving your visual workflow. Here, Devs United Games uses a handful of nodes to animate their Aquascape character as it enters the scene. As the fish swims through the environment, its animation playback even adjusts to match its exact speed. Shader graph materials in Reality Composer Pro are also getting a major upgrade.\n\nLast year we introduced a system environment featuring Jupiter's moon Amalthea, showcasing a subsurface scattering effect that makes the ice on the moon's surface look strikingly real. Reality Composer Pro 3 now exposes subsurface scattering through Shader Graph, so you can bring a similar effect to your own scenes.\n\nAnd with additional shader graph capabilities, you can now create lifelike skin, eyes, and hair for your characters, or even craft the otherworldly look of a portal. Reality Composer Pro 3 brings so much more to explore, including Prototypes, Behavior Trees, Compute Graphs, and custom script graph nodes. Check out these sessions to learn more. Native frameworks like RealityKit and SwiftUI give you the deepest integration with visionOS. But if you've already built your game in another engine, like Unity, Unreal, or Godot, it's easier than ever to bring it to Apple Vision Pro.\n\nUnity has been compatible with visionOS since we first launched.\n\nHere's LEGO Builder's Journey that was made with Unity and runs in a volume.\n\nIf you have a Unity Pro license, you can bring your Unity game to visionOS. Windowed games made with Unity use RealityKit for native rendering; while immersive games on visionOS can be rendered either with RealityKit or CompositorServices framework, depending on your rendering needs. We've released plugins to support spatial accessories such as the PSVR 2 Sense controller so that you can design tactile interactions for your Unity apps and games.\n\nUnreal Engine is also available in immersive mode. And Polyarc, the developer of Glassbreakers, brought their Unreal engine game to Apple Vision Pro with static foveation, for noticeably sharper visuals.\n\nGames built with Godot, like DogWalk from Blender Studios, also run on Apple Vision Pro. We've also added support for Godot rendering through CompositorServices, along with a plugin for rendering with RealityKit. And we've published a PHASE audio plugin, so your Godot games can take full advantage of Apple's spatial audio. You can download these game engine plugins from our GitHub page.\n\nvisionOS also supports experiences built on custom rendering engines.\n\nWith the CompositorServices framework, you can connect your proprietary engine to the system and render your content directly in an immersive space.\n\nThat was a sneak peek at supported games engines. Next, let's talk about extending content from your Mac to the infinite canvas of Apple Vision Pro, using the Spatial Preview framework.\n\nMac Virtual Display transforms your workspace offering you the ability to stay in headset while you work on your Mac with complete privacy, wherever you are.\n\nWith visionOS 27, we're taking this capability even further with Spatial Preview. This is a new macOS framework that lets you preview spatial content from your Mac directly on your Apple Vision Pro, and collaborate with others through SharePlay, — all without ever building a visionOS app! Leveraging Quick Look on visionOS, you can immediately preview and update content like spatial photos and Apple immersive video as well as edit 3D content live using USD.\n\nYou can move freely around 3D scenes, refine your content's placement, adjust material overrides, and share feedback with annotations, all within a spatial environment. With real-time asset editing, apps like Cinema4D and SketchUp transform the creative process — unlocking real-time, collaborative 3D workflows whether you're working side by side, or across the globe.\n\nThis capability is built directly into Preview on macOS 27, so customers can experience these powerful features out of the box. We're also giving Preview new 3D editing tools making 3D content as easy to work with as images and PDFs.\n\nFor a deeper dive, check out the \"Discover the Spatial Preview framework\" session.\n\nThe Spatial Preview framework is a great way to stream immersive Mac content to visionOS. Next, I'll introduce a new way to bring immersive PC content to our platforms with Foveated Streaming.\n\nFoveated Streaming enables Apple Vision Pro to connect to external devices — like a PC — to stream OpenXR content.\n\nvisionOS automatically sends input data, like hands, controller positions, and microphone.\n\nAnd the device streams your OpenXR content, like video and audio with full-scale immersion, just like a native app.\n\nFoveated Streaming launched in visionOS 26.4, and it's already unlocking incredible experiences. X-Plane 12 from Laminar Research delivered a best-in-class flight simulation experience. The X-Plane app on visionOS uses ARKit to understand your space and equipment, and streams the simulation from a PC. This means you can fly using a physical flight simulator while being fully immersed in a virtual world. It's an integrated experience only Apple Vision Pro can deliver.\n\niRacing, a motorsport racing game for PC, also streams to Apple Vision Pro using Foveated Streaming. The iRacing Connect app matches the position of your physical racing wheel with the virtual cockpit. Precise ARKit tracking makes this possible, delivering an experience that is immersive and exhilarating.\n\nInnoactive also brings Autodesk VRED to Apple Vision Pro, letting designers visualize massive, high-fidelity models with ray tracing, at a 1:1 scale. PC-based rendering pairs seamlessly with SwiftUI, so you can build advanced streaming applications quickly and intuitively.\n\nAnd the quality is exceptional, because it uses advanced technology to optimize the video stream. Foveated streaming intelligently compresses video based on where a person is looking.\n\nAreas in focus are streamed at higher quality, while areas in your visual periphery use less bandwidth. It happens so quickly and seamlessly, that you don't even notice.\n\nThe streaming protocol is powered by NVIDIA CloudXR. This is a top-ranked streaming technology, offering high quality and low latency. CloudXR is performant enough to stream over Wi-Fi, with no dongles or cables needed, whether from a local PC or a cloud instance.\n\nAnd it is incredibly easy to use. We've found that just in one day you can start streaming your OpenXR applications to Apple Vision Pro, and in a week you can enhance your application with features you can find only on visionOS.\n\nTo learn more, see our dedicated session on Foveated Streaming.\n\nThose are the many ways you can bring your content to Apple Vision Pro. Next, let's look at some exciting new ways in which people can interact with your content in visionOS.\n\nLet's start with enhancements to object tracking.\n\nWe introduced object tracking in visionOS 2.0, which lets you turn physical objects into virtual anchors. To track an object, you can start with its USDZ model and train a reference object in Create ML on your Mac.\n\nYou pass that reference object to the object tracking API, and your app receives updates about the position and orientation of the physical object, enabling immersive spatial experiences.\n\nObject tracking now supports high-frame-rate tracking, giving your app more frequent pose updates as objects move through space.\n\nWe've added an extended training option in Create ML that improves accuracy and robustness, particularly for objects held in hand.\n\nThere's a new API for obtaining the object pose in metric space, without display corrections. This unlocks spatial measurement use cases that require high-precision poses.\n\nAnd these features are now available on both visionOS and iOS. These enhancements to object tracking let you create dynamic experiences that instantly react as people pick up and interact with objects in the physical world.\n\nFor example, you can now accurately track and measure physical spaces using handheld items such as this medical probe. This opens up use cases like surgical navigation training.\n\nTo bring object tracking to iOS, we're releasing an ARKit API that supports the same functionality as visionOS. We designed the ML model training in Create ML to be platform agnostic. This means once you create a reference object, you can use it in both your iOS and visionOS app and get the same level of tracking quality.\n\nTo learn more about developing with the object tracking API, you can watch the WWDC24 session or explore our documentation.\n\nWhile object tracking is great for tracking regular objects, visionOS also supports spatial accessories.\n\nIn visionOS 26, we introduced our first set of spatial accessories: the Logitech Muse and the PSVR2 Sense controller.\n\nThese devices bring a new level of interactivity and immersion to your apps through spatial tracking, button input, and haptic feedback. You can connect to them using the Game Controller framework, and use RealityKit or ARKit to track each accessory's movement and orientation in space. Now in visionOS 27, we are expanding support to enable you to build your own accessory. A spatial accessory is an electronic device, containing a board with the following components: A constellation of LEDs visible to Apple Vision Pro for tracking. An IMU to capture the orientation and acceleration of the accessory.\n\nAnd a Bluetooth chip to send the signals to Apple Vision Pro.\n\nSpatial accessories can also host any variety of inputs like buttons, touchpads, along with haptic feedback.\n\nYou can turn any object into a compatible spatial accessory by just installing these components in it.\n\nTo help you get started, manufacturers like DFRobot and MikroE will release off-the-shelf reference hardware and development kits, later this year. These can be a great starting point to include custom spatial accessory input in your visionOS apps. For example, this is a 3D printed flashlight, mounted with a DFRobot seeMote Cap. Thanks to low-latency tracking, the virtual light it casts looks completely natural on the physical walls around it. As the physical flashlight moves, the virtual beam follows smoothly and accurately. Here's another example: a MikroE Spatial Anchor R1 is mounted inside a physical steering wheel, seamlessly anchoring a digital vehicle to it. When you grab the wheel, you feel like you're inside the car. This unlocks use cases like immersive racing simulations and vehicle interior design. Apple Vision Pro tracks these devices at the highest possible frequency with extremely low latency — matching the display's native refresh rate for a seamless experience. They support use cases that demand fast motion. Spatial accessories will continue to track robustly, even when temporarily occluded.\n\nAnd they are also trackable under low-light conditions.\n\nLastly, the physical buttons and haptics allow you to make your experiences even more interactive and immersive.\n\nTo learn more about integrating custom spatial accessories into your spatial experiences, check out the session \"Explore enhancements to visionOS object tracking\".\n\nNext, let's talk about the workflow to bring immersive media experiences to visionOS.\n\nvisionOS supports many forms of spatial and immersive video types, but Apple Immersive Video, or AIV, is the highest fidelity immersive video experience, available on visionOS. It has an extremely large field-of-view and fully immersive audio that places you in the experience, as if you were there. It represents a fundamental advancement in video media engineering, built on high-resolution, high-frame-rate, stereoscopic 180 degree capture that delivers unprecedented fidelity and dimensional accuracy.\n\nApple Immersive Video supports both video-on-demand and live broadcast streaming in visionOS. Achieving such a high quality experience requires video that meets demanding specifications. Real-world stereoscopic scale is maintained through metadata-driven lens calibration, which provides accurate projection during playback. Video is captured and streamed at 90 frames per second with a near-human visual acuity greater than 100 megapixels per frame.\n\nThat's over ten billion pixels per second. To handle all that video, AIV already has a growing ecosystem of production and post-production tools that are now available with industry-leading broadcast hardware and software developed for iOS, macOS, and visionOS.\n\nMany of those tools were built by developers like you with the Immersive Media Support framework, or IMS.\n\nIMS enables reading and writing of rich metadata for Apple Immersive Video, and provides support for authoring and modifying immersive content.\n\nYou can get started with integrating IMS in your apps by watching these sessions: \"Learn about Apple Immersive Video technologies\" and \"Support immersive video playback in visionOS apps\" from last year.\n\nIn addition to recently introduced iOS support, important new features have been added to IMS in visionOS 27, including camera presentation override commands, an ImmersivePreviewRenderer API, and wide-aspect-ratio portals. We have also published some new sample code that implements static foveation in dual track QuickTime, and updates to the Apple Spatial Audio Format production suite. Let's first look at the impact of new camera presentation override commands. In live and complex production scenarios, you may need to introduce new camera parameters to override default camera configurations, and in real time. That's where the new Set Camera Command Overrides come into play.\n\nIf you would like to learn more about the practical application of IMS in the AIV live production workflow with SMTPE 2110, check out the WWDC 26 session, \"Build live production tools for Apple Immersive Video\". The new ImmersivePreviewRenderer enables real-time previewing of Apple Immersive Video on Apple Vision Pro directly from a Mac during editorial or live production workflows.\n\nThis gives editors, colorists, and directors a more accurate representation of how immersive content will look, and be experienced, at final delivery.\n\nWe're also adding wide-aspect-ratio portal support for Apple Immersive Video. This lets you keep a very wide portal of the immersive video experience in viewing when switching from full immersive mode to portal mode.\n\nThe implementation for your apps is straightforward. Custom aspect ratios can be set when using AVPlayerViewController in AVKit-based apps, or VideoPlayerComponent in RealityKit-based apps.\n\nNext, I want to introduce you to a new sample that uses static foveation to deliver a high-quality immersive video while keeping it streamable. It would be impractical, even for a high-speed home internet connection, to stream full resolution AIV, in stereo, at 90 frames-per-second. But simply scaling the image down to 4K would sacrifice too much pixel density.\n\nInstead, a smooth static foveation function can be applied to the image before it is encoded for streaming. The new sample project demonstrates one way this technique can be used to achieve high-acuity immersive video in a streamable frame size. And we can't forget that any good immersive video experience, must also have an immersive spatial audio soundtrack. So we have released important new updates to the ASAF Production Suite. The suite of AAX plugins now has the ability to position objects relative to a reference video, with improvements to the ambisonics workflow that include a new Scene Compressor plugin, and enhancements to the heat map drawing and the spatial filtering algorithm.\n\nThe new sample project and the ASAF Production Suite are both available for download on developer.apple.com.\n\nFrom deeper immersion, to richer media, and powerful new ways to add interactivity, visionOS 27 gives you so much to build with.\n\nTo round things out, let's spotlight a few final additions, starting with the Spatial Web. In Safari on visionOS 27, windows can now be adjusted to a wider aspect ratio, letting you take full advantage of the space around you. Larger windows will naturally curve, bringing more of your content into comfortable view. And, Web Environments are now enabled by default allowing websites to have backgrounds, just like apps. Here's an example showing a virtual environment from the Apple TV show, Severance.\n\nWe've streamlined notifications, system status, controls, and environments all in one place via a newly improved Control Center, along with High Quality Capture enabling you to capture your apps, in stunning 4K video from right inside Apple Vision Pro, no Mac required.\n\nAnd with accessory widget support on visionOS, you can extend your app to Apple Vision Pro with smaller, glanceable widgets that surface your most relevant information, right where you need it.\n\nvisionOS 27 brings so much more to explore, including Siri enhancements, an all-new Iceland environment, Spatial Panoramas, Personal Environments, and Freeform updates, to name a few. Together, they give you even more powerful tools to bring your ideas to life. We cannot wait to see what you'll build next.",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/287/4/979d9278-8250-46f9-ac82-79669ba7b479/downloads/wwdc2026-287_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/287/4/979d9278-8250-46f9-ac82-79669ba7b479/downloads/wwdc2026-287_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "338",
+ "year": "2026",
+ "title": "Build live production tools for Apple Immersive Video",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/338"
+ },
+ {
+ "id": "252",
+ "year": "2026",
+ "title": "Design no-code games with Reality Composer Pro 3",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/252"
+ },
+ {
+ "id": "282",
+ "year": "2026",
+ "title": "Discover the Spatial Preview framework",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/282"
+ },
+ {
+ "id": "285",
+ "year": "2026",
+ "title": "Discover USDKit and what’s new in OpenUSD",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/285"
+ },
+ {
+ "id": "279",
+ "year": "2026",
+ "title": "Explore advances in RealityKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/279"
+ },
+ {
+ "id": "283",
+ "year": "2026",
+ "title": "Explore enhancements to visionOS object tracking",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/283"
+ },
+ {
+ "id": "281",
+ "year": "2026",
+ "title": "Extend Reality Composer Pro 3 functionality with Xcode",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/281"
+ },
+ {
+ "id": "280",
+ "year": "2026",
+ "title": "Iterate your spatial scenes faster with Reality Composer Pro 3",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/280"
+ },
+ {
+ "id": "393",
+ "year": "2026",
+ "title": "Supercharge your spatial workflows with Reality Composer Pro 3",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/393"
+ },
+ {
+ "id": "286",
+ "year": "2026",
+ "title": "Use foveated streaming to bring immersive content to visionOS",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/286"
+ },
+ {
+ "id": "403",
+ "year": "2025",
+ "title": "Learn about Apple Immersive Video technologies",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/403"
+ },
+ {
+ "id": "296",
+ "year": "2025",
+ "title": "Support immersive video playback in visionOS apps",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/296"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:18.887Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-289.json b/data/wwdc/videos/2026-289.json
new file mode 100644
index 0000000..bee2a88
--- /dev/null
+++ b/data/wwdc/videos/2026-289.json
@@ -0,0 +1,139 @@
+{
+ "id": "289",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/289/",
+ "title": "Modernize your AppKit app",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Machine Learning & AI",
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi! I'm Ujjaini, a Mac UI frameworks engineer, and this is \"Modernize Your AppKit App\".\n\nA modern app takes advantage of how AppKit interfaces with Mac, so that its form and function feel in harmony with the rest of the system. That harmony shows up in three places: in how people drive your app, in how the system manages it, and in how it looks and feels on the screen. Today I'll share tips for all three.\n\nI'll begin with modern precision input methods that your app should adapt to. Then, I'll discuss the importance of preserving continuity across launches. That is, how your app can terminate gracefully when the system needs it to, and come back to where it was left off.\n\nAnd finally, I'll go through updates to the look and feel in macOS 27, many of which you'll notice without having to rebuild your app.\n\nI'll start where the Mac started: at the input device.\n\nPrecision input devices have been at the heart of the mac since the very beginning. The first Mac shipped with a keyboard and mouse! Over time, the APIs for handling these various kinds of input have evolved and become far more convenient. mouseDown and tracking loops have been the go-to pattern, for implementing interactive behaviors in AppKit apps. However, AppKit isn't the only framework on Mac. SwiftUI, UIKit through Mac Catalyst, and AppKit work together to create the Mac experience. They rely on Gesture Recognizers, to provide a common event handling language across all three frameworks. Gesture recognizers empower AppKit to provide advanced behaviors, without having to build that yourself. The modern way of dealing with events is gesture recognizers. I'll explore three solutions that interface well with them: view-based APIs, control events, and custom gesture recognizers themselves. These offer the same customizability as tracking loops and mouseDown overrides, and support cross-framework compatibility. I'll tell you when to reach for each.\n\nCommon mouseDown overrides enable tracking selection, showing context menus, drag-and-drop, and text selection. If you're currently overriding mouseDown, AppKit has dedicated APIs that handle these behaviors more reliably, and take full advantage of the modern Mac platform.\n\nmouseDown is often overridden to track selection. Instead, observe the selected property on types like NSCollectionViewItem and NSTableRowView. Or, use the delegate callbacks that are notified when selection changes, like NSTableViewDelegate, and NSOutlineViewDelegate.\n\nTo show context menus from a view, you have a few options.\n\nUse the class property .defaultMenu on NSView where all instances of the view will show the same menu. Use the instance property .menu on NSResponder, to provide a different menu for every responder.\n\nOr, use the instance method .menuForEvent on NSView, to dynamically create the menu based on the event. If your app uses collection container views, use modern dragging delegate methods, like tableView pasteboardWriterForRow.\n\nCreate a pasteboardItem, set the data on it, and return it. Similar methods exist on NSCollectionView, NSOutlineView and NSBrowser.\n\nIf you need text selection behavior outside of NSTextView, use NSTextSelectionManager. It's a new API in macOS 27, that takes advantage of gesture recognizers and brings classic macOS text selection behaviors to any view. Attach it to a view and set up a text selection data source. You'll then have support for bidirectional selection, drag and drop with text, toggling, and much more.\n\nThe next solution might seem a little familiar, if you know control events from UIKit. Control events are now in AppKit! Control events can be added to standard Mac controls, like buttons or sliders. They enable your code to react to user-driven tracking state changes, rather than having to implement complex mouseDown tracking logic. AppKit calls the registered target and action when the control event is triggered. Most of these control events have been made available from OS 10.11! Here is an example of NSControlEvents. Instantiate the button and register a target and action for a control event. Note that you don't even have to subclass NSButton to get this to work! For more control over interactions in your views, add standard gesture recognizers. For even more flexibility, create your own custom gesture recognizer subclasses. Learn more in the \"Gestures\" documentation.\n\nBecause gesture recognizers operate on a view and its sub-views, overlapping sibling views can silently block mouse events. If a control doesn't seem to be responding to a click, make sure you don't have some sibling view that overlaps that button. To address this issue, re-size the view so it doesn't overlap.\n\nIf the view should not be re-sized because it is an overlay, override hitTest and return nil, so hit testing can fall through to content underneath.\n\nNow, I'll focus on keyboard navigation. Your app needs to respond to keyboard input seamlessly, to enable speed and accessibility.\n\nKeyboard navigation for controls can be turned on in system settings. When it's on, focus moves between controls using tab or shift-tab. The key view loop is the order in which controls are cycled through, when the Tab key is pressed. To automatically recalculate the loop, every time a view is added or removed in the hierarchy, enable .autorecalculatesKeyViewLoop on the window. If you do not set this value, you are in charge of creating and maintaining the key view loop.\n\nKeyboard navigation also reaches beyond your app's windows, into the menu bar and status items. Navigating across status items is a little different from main menu items. Status items that show a menu when clicked, already behave like menus on the menu bar. But status items can also be triggers, for actions or display some kind of transient UI.\n\nTo trigger an action, modify NSStatusItem's button property to include a target and action, and optionally an image. This behaves like a regular button and the action fires automatically when Return is pressed during keyboard navigation. To use a custom view for your status menu item, use status items view property to set the view. Then add a target and action to the status item, to enable performing that action.\n\nStatus items can also be triggers to show custom windows, for example! When a status item shows its window, AppKit needs to know when that UI is active, so that keyboard focus can behave correctly. Track the life cycle of your custom UI, using the expanded interface session API. First set a delegate, when the item is created, that will receive begin and end calls to display or dismiss your window.\n\nIn the delegate, implement statusItem didBegin ExpandedInterfaceSession and statusItemDidEnd_ ExpandedInterfaceSession. These methods are called by AppKit, to manage the life cycle of an expanded interface session. In the didBegin call, show the window. In the didEnd call, order the window out.\n\nWhen it is time to dismiss the session, for example, because an action has been selected, call .cancel on the .expandedInterfaceSession?. Note that the session might be canceled for you, if focus naturally moves somewhere else.\n\nSwiftUI menu bar extras do a lot of this work for you! Check out the WWDC26 video \"Use SwifUI with AppKit and UIKit\" to learn how an AppKit app can use a SwiftUI menu bar extra. Making sure your app works just as well with the keyboard as with a mouse, is especially important for the many power users who choose the Mac. Providing a seamless transition, within and outside of your app, is one more way to enable them.\n\nSpeaking of seamless transitions, a great Mac app seamlessly quits and quickly restores. It quits without pushback, and comes back as if it was never quit in the first place! People should be able to quit their apps at any time. Sometimes because they want to, sometimes because the system needs to reboot, which might happen during an overnight software update. So your app should only block quit when it genuinely needs to. When your app is presenting a sheet, the window might not be able to close. And when a window can't close, the app can't quit. The NSWindow property preventsApplicationTerminationWhenModal defaults to true, and for good reason! It's important to make sure your app doesn't lose data, when a document needs to be saved, for example.\n\nSet this property to false, for all modals or sheets that don't strictly require intervention, to allow more graceful application termination.\n\nWith graceful termination handled, the next step is restoration. Use NSWindowRestoration to customize how your app comes back. State restoration requires 3 steps: opting into state restoration, encoding the UI state, and decoding the state to restore windows and UI. I'll go through some code that uses NSWindowRestoration. First, set an identifier for the window in the window controller. For common windows, like your main window or a preferences window, set an autosave name. This helps restore your window to an active space with the same frame. There is no need to set an autosave name for document windows.\n\nThen ensure window.isRestorable is set to true, so AppKit can call encodeRestorableState and restoreState on your windows.\n\nThis also lets Appkit automatically restore window state, like which window was minimized, which was frontmost, and which was full screen.\n\nAlso, set a window.restorationClass, which will be invoked when the app is re-launched, to restore the window itself.\n\nUse encodeRestorableState to preserve everything you need to recreate your window's state.\n\nCall super's implementation as well, so your state is restored correctly.\n\nIn this example, the selected item's identifier is encoded with the productIdentifier key.\n\nAvoid encoding data that lives in your document or database. The goal of state restoration is to be able to reconstruct the state of the UI, not to re-serialize the whole app. All NSResponders have an encodeRestorable_ State method that you can override, so manage state for your views as well.\n\n.encodeRestorableState is only called when state for the object has been invalidated. Every time there is a change to your view hierarchy that should alter the saved state, call .invalidateRestorableState( ). In this example, this method is called when a different product is selected in the sidebar. At a later time, encodeRestorableState will be called on everything that was invalidated.\n\nThat's what your app needs to save the state of its UI before it has quit. When the app is re-launched, you'll need to decode all that information to restore the UI. First restore your windows, and then restore the state on those windows! In the window restoration class, implement the method restoreWindow withIdentifier, to recreate the windows in your app. This method is called for every window that's being restored. Its parameters include the window's identifier, and the completionHandler that needs to be called with the corresponding window.\n\nUsing the identifier, recreate the window controller and windows. The .mainWindow is already available on the app delegates .mainWindowController. Call the completionHandler with the existing .window.\n\nFor other windows, instantiate the window controller, and pass in its .window to the completionHandler.\n\nIf window creation fails, still call the completionHandler with the error. AppKit waits on every restorable window, so always call the completionHandler. If you can't call it from within this method, save the handler and call it later. Bu be absolutely sure to call it! Once the windows have been restored, the last step is to restore the UI for each window. In the window controller's restoreState method, AppKit will hand you the same coder object containing the keys you encoded before. This is the place to fetch any data required to reconstruct your app's state.\n\nDecode the identifiers and hand them to the corresponding view controllers. When you're done, your windows should be in the same state as before.\n\nEnhancing quit and relaunch to feel uninterrupted, helps people pick up right where they left off, whether they quit the app or restarted their Mac.\n\nTo learn state restoration in practice, check out the code sample \"Restoring your app's state with AppKit\".\n\nWith input and restoration in hand, there's one more area where your app and the Mac meet: the UI. The Liquid Glass material, introduced in macOS 26, continues to evolve. Your app will benefit from many of the updates automatically. If you adopted Liquid Glass in macOS 26, your app will pick up a few changes when you run it on macOS 27. The automatic NSScrollEdgeEffectStyle resolves to a hard-edge effect, when there is free-floating text, like the window title in the title bar.\n\nSidebars extend to the window's edges, selection in the sidebar uses a semi-bold text style for emphasis.\n\nAnd content still flows behind them.\n\nBordered toolbar items over the sidebar adopt Liquid Glass as well.\n\nNew in macOS 27, there is an effect that can be added to glass. Where the glass subtly bounces when clicked, giving a sense that the control is responding to interaction. Maps uses this for a few of their custom controls.\n\nThis would not apply to all uses of glass in your app: use this effect with controls and buttons, or glass containers of interactive controls. A little goes a long way! Rounded rectangles have been a signature of the Apple ecosystem for decades, from hardware bezels to controls and containers across macOS. AppKit has new API for concentricity. Content meant for a corner can adapt to the shape of its container, instead of feeling at odds with the rest of the window.\n\nFor example, the local weather view in Maps is concentric with the window.\n\nWhen a view sits near the corner of its container, its own rounded corners should follow the curve of that container. The closer the view is to the container's corner, the more its radius should match.\n\nTo make your button or view concentric, use the cornerConfiguration API.\n\nFirst, create a custom view subclass.\n\nOn the custom view, override cornerConfiguration, to return an NSViewCornerConfiguration?.\n\nFor the radius, use .containerConcentric on NSViewCornerRadius. This calculates a radius based on the container view.\n\nSet a minimum value as well, so that every corner is always rounded.\n\nYou can choose from many different kinds of factory methods for the configuration. To maintain a roundedRect with the same radii across all 4 corners, use .uniformCorners.\n\nThese are a few pointers that can help make your app harmonious on modern macOS. I'll leave you with a quick recap of where to start.\n\nIdentify places in which you are overriding mouseDown in your app, and instead use view APIs, control events, or gesture recognizers. Prioritize user intent over tracking loops.\n\nMake sure your app works just as well from the keyboard as from the mouse.\n\nMake quit and relaunch feel seamless, so your app picks up exactly where people left off. And evaluate your view hierarchies to adopt concentricity in views and buttons.\n\nThank you so much for watching. Whether your app is for the students at school learning how to use a computer or the power users who build some of the world's most important tools and art, your apps have played a central role in this experience. Keep on creating!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:35",
+ "title": "Modern dragging delegate",
+ "language": "swift",
+ "code": "// Modern dragging delegate methods\nfunc tableView(_ tableView: NSTableView,\n pasteboardWriterForRow row: Int) -> (any NSPasteboardWriting)? {\n let pasteboardItem = NSPasteboardItem()\n pasteboardItem.setString(..., forType: .string)\n return pasteboardItem\n}"
+ },
+ {
+ "timestamp": "4:55",
+ "title": "Control events",
+ "language": "swift",
+ "code": "// Use control events\nlet button = NSButton()\nbutton.addTarget(\n self,\n action: #selector(trackingEndedOutsideHandler),\n for: .trackingEndedOutside\n)"
+ },
+ {
+ "timestamp": "5:44",
+ "title": "hitTest override",
+ "language": "swift",
+ "code": "override func hitTest(_ point: NSPoint) -> NSView? {\n return nil\n}"
+ },
+ {
+ "timestamp": "6:24",
+ "title": "autorecalculatesKeyViewLoop",
+ "language": "swift",
+ "code": "window.autorecalculatesKeyViewLoop = true"
+ },
+ {
+ "timestamp": "7:37",
+ "title": "Expanded interface delegate — setup",
+ "language": "swift",
+ "code": "// Set the expanded interface delegate\n@main class LightAppDelegate: NSObject, NSApplicationDelegate {\n lazy var lightStatusItem: NSStatusItem = { ... }()\n\n func applicationDidFinishLaunching(_ notification: Notification) {\n // ...\n lightStatusItem.expandedInterfaceDelegate = self\n }\n}"
+ },
+ {
+ "timestamp": "7:52",
+ "title": "Expanded interface delegate — methods",
+ "language": "swift",
+ "code": "// Implement the delegate methods\nextension LightAppDelegate: NSStatusItemExpandedInterfaceDelegate {\n // ...\n func statusItem(_ statusItem: NSStatusItem, didBegin session:\n NSStatusItemExpandedInterfaceSession) {\n // Show window\n }\n func statusItemDidEndExpandedInterfaceSession(\n _ statusItem: NSStatusItem, animated: Bool) {\n // Hide window\n }\n func selectedAction() {\n // Take the action\n // Cancel session to request window dismissal\n lightStatusItem.expandedInterfaceSession?.cancel()\n }\n}"
+ },
+ {
+ "timestamp": "8:16",
+ "title": "Expanded interface delegate — cancel",
+ "language": "swift",
+ "code": "// Cancel the session when dismissing\nextension LightAppDelegate: NSStatusItemExpandedInterfaceDelegate {\n // ...\n func statusItem(_ statusItem: NSStatusItem, didBegin session:\n NSStatusItemExpandedInterfaceSession) {\n // Show window\n }\n func statusItemDidEndExpandedInterfaceSession(\n _ statusItem: NSStatusItem, animated: Bool) {\n // Hide window\n }\n func selectedAction() {\n // Take the action\n // Cancel session to request window dismissal\n lightStatusItem.expandedInterfaceSession?.cancel()\n }\n}"
+ },
+ {
+ "timestamp": "9:45",
+ "title": "preventsApplicationTerminationWhenModal",
+ "language": "swift",
+ "code": "window.preventsApplicationTerminationWhenModal = false"
+ },
+ {
+ "timestamp": "10:18",
+ "title": "Set window identifiers for state restoration",
+ "language": "swift",
+ "code": "// Set window identifiers for state restoration\n@MainActor class MainWindowController: NSWindowController, NSWindowDelegate {\n // ...\n convenience init() {\n let window = NSWindow( ... )\n // ...\n window.identifier = NSUserInterfaceItemIdentifier(WindowIdentifiers.mainWindow)\n window.setFrameAutosaveName(WindowIdentifiers.mainWindow)\n window.isRestorable = true\n window.restorationClass = WindowRestorationHandler.self\n // ...\n }\n}"
+ },
+ {
+ "timestamp": "11:04",
+ "title": "encodeRestorableState",
+ "language": "swift",
+ "code": "// Preserve state to recreate the UI\n@MainActor class MainWindowController: NSWindowController, NSWindowDelegate {\n // ...\n override func encodeRestorableState(with coder: NSCoder) {\n super.encodeRestorableState(with: coder)\n // ...\n coder.encode(selectedProduct?.identifier.uuid,\n forKey: RestorationKeys.productIdentifier)\n // ...\n }\n // ...\n}"
+ },
+ {
+ "timestamp": "11:50",
+ "title": "invalidateRestorableState",
+ "language": "swift",
+ "code": "// Invalidate restorable state when the view hierarchy changes\n@MainActor class MainWindowController: NSWindowController, NSWindowDelegate {\n // ...\n convenience init() {\n // ...\n splitViewController.onProductSelected = { [weak self] product in\n self?.invalidateRestorableState()\n }\n }\n}"
+ },
+ {
+ "timestamp": "12:26",
+ "title": "restoreWindow(withIdentifier:)",
+ "language": "swift",
+ "code": "// Restore windows\nclass WindowRestorationHandler: NSObject, NSWindowRestoration {\n static func restoreWindow(\n withIdentifier identifier: NSUserInterfaceItemIdentifier,\n state: NSCoder,\n completionHandler: @escaping (NSWindow?, Error?) -> Void\n ) {\n //...\n if identifier == .mainWindow, let window = appDelegate.mainWindowController?.window {\n completionHandler(window, nil)\n } else if identifier == .imageWindow {\n let controller = ImageWindowController()\n appDelegate.imageWindowControllers.append(controller)\n completionHandler(controller.window, nil)\n } else {\n completionHandler(nil, error)\n }\n }\n}"
+ },
+ {
+ "timestamp": "13:29",
+ "title": "restoreState",
+ "language": "swift",
+ "code": "// Restore window UI\n@MainActor class MainWindowController: NSWindowController, NSWindowDelegate {\n //...\n override func restoreState(with coder: NSCoder) {\n super.restoreState(with: coder)\n if let productId = coder.decodeObject(\n of: [NSString.self],\n forKey: RestorationKeys.productIdentifier) as? String {\n splitViewController?.selectedProductId = productId\n }\n //...\n }\n}"
+ },
+ {
+ "timestamp": "16:11",
+ "title": "cornerConfiguration",
+ "language": "swift",
+ "code": "// Subclass NSView to override cornerConfiguration\nclass LocalWeatherView: NSView {\n // ...\n override var cornerConfiguration: NSViewCornerConfiguration? {\n let radius: NSViewCornerRadius = .containerConcentric(minimumCornerRadius)\n return .uniformCorners(radius: radius)\n }\n // ...\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Use SwiftUI with AppKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2022/10075/"
+ },
+ {
+ "title": "Restoring your app’s state with AppKit",
+ "url": "https://developer.apple.com/documentation/AppKit/restoring-your-app-s-state-with-appkit"
+ },
+ {
+ "title": "Gestures",
+ "url": "https://developer.apple.com/documentation/AppKit/gestures"
+ },
+ {
+ "title": "TN3212: Adopting gesture recognizers for Sidecar touch support",
+ "url": "https://developer.apple.com/documentation/Technotes/tn3212-adopting-gesture-recognizers-for-sidecar-touch-support"
+ },
+ {
+ "title": "NSControl.Events",
+ "url": "https://developer.apple.com/documentation/AppKit/NSControl/Events"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/289/5/6a2a7cfa-56a1-4cbb-ae54-1f229e1708ae/downloads/wwdc2026-289_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/289/5/6a2a7cfa-56a1-4cbb-ae54-1f229e1708ae/downloads/wwdc2026-289_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "272",
+ "year": "2026",
+ "title": "Use SwiftUI with AppKit and UIKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/272"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:19.546Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-290.json b/data/wwdc/videos/2026-290.json
new file mode 100644
index 0000000..31dbfb5
--- /dev/null
+++ b/data/wwdc/videos/2026-290.json
@@ -0,0 +1,36 @@
+{
+ "id": "290",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/290/",
+ "title": "Craft clear names for features and labels in your app",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Design"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hi, I'm Heej, a UX writer on the Human Interface design team. Think about the last time you opened an app and just knew where to go. That feeling is designed. And one of the most powerful tools you have for creating it can also be easy to overlook — the name you give things.\n\nFor over 50 years, Apple has created names that have become part of everyday life for many. Names like Mac, iPhone and iCloud.\n\nBut naming happens at every level — from menus to tab bars to settings. And all of those choices, big and small, add up to how someone feels about your app.\n\nToday we're going to look at how to make those choices well.\n\nSo we'll start with the qualities that make a name work and what's at stake when a name falls short.\n\nThen we'll walk through an exercise, a way to get to the right name by understanding who you're building for and testing how a name actually works in practice.\n\nAnd we'll close with how to evaluate any name you come up with so you can walk away with something concrete to apply to whatever you're building.\n\nLet's start with the criteria.\n\nBelongs — this first one is about fit. A name that truly belongs doesn't just sound like your app, it works at every level. What users expect to find there. How it lives among everything else you've named.\n\nExpectations — the second is about clarity and trust. When someone reads a name, they're already predicting what they'll find. The right name delivers on that and when it does, trust builds.\n\nAnd finally — works everywhere. A name that travels — holding up across languages, markets, and the platforms and contexts where your app lives.\n\nKeep in mind that names won't always check every box. You may have criteria of your own, like trademark considerations or specific regulations in your industry. So think of these criteria as a guide, not a rulebook. The trade-offs are yours to make. Now let's see them at work with an example from Apple Cash.\n\nWhen I want to send money with Apple Cash, there's one thing I need to know right away — how much I have.\n\nThat amount is right next to the button I use to send it. And we need to label what it is.\n\nWhat if we called it Spending Power? It sounds compelling, but it's not concrete. And it raises more questions. Is it a credit limit? A score? In a financial context, ambiguity is the last thing you want. It also doesn't quite feel like something that belongs in an Apple service.\n\nAnd consider the tone. If my Spending Power is low, or at zero that stops being a label and starts feeling like a judgment.\n\nPayment services carry a lot of trust. A name that passes judgment, even unintentionally, breaks that trust.\n\nWhat about something more descriptive like Current Funds? It describes what's here — the funds currently in my account.\n\nBut Current Funds sounds like something out of a spreadsheet. Try saying it out loud — let me check my Current Funds. People don't really talk like that in natural conversation. That gut check alone tells you it doesn't belong in a service that people use to send and receive money.\n\nSo the best name here for this value is Balance. It's the industry-standard term for it because it's well-understood, clear, and neutral.\n\nClarity and trust come first here, not brand expression. Balance belongs in this app, sets exactly the right expectation, and travels without friction.\n\nSometimes the most obvious word is the right one because it's already doing the job.\n\nThere are times when the pull toward something more clever or branded is real. Consider a subscription screen in a gym app. Two plans - Basic Access and All Access. They're pretty clear and easy to choose between. But what if we got a little creative with the plan names? What if we called them Lightweight and Heavyweight plans instead? That's fun and seems on brand. But there's a learning curve. What do those names actually mean? That friction makes choosing harder. So how do you decide? Here's the thing about the criteria I mentioned at the beginning, about a name belonging, setting expectations, and working everywhere — you don't have to check every box. Maybe Lightweight and Heavyweight really do fit the gym brand better. That's totally fine, there's just more work to do to make sure those names are understood.\n\nSometimes you lean into clarity. Other times you lean into brand. Ask yourself what matters most given where a feature or setting lives and let that guide which criteria you prioritize.\n\nNow let's look at how to come up with a great name.\n\nWhen you're building something, it's natural to name it by what it does, the technology behind it, or the function. But the people using your app don't see it that way. They want to know what it does for them. Who's your app for? New parents? People training for a marathon? Start by getting clear on your audience first, and try this exercise.\n\nWith the audience in mind, ask yourself when they encounter this feature, what should they think, feel, and do? So let's walk through this exercise together. Imagine you're part of the Apple Maps team, and you're naming a feature that remembers the places you've been; that new café you discovered last week, or the park with the accessible trails.\n\nWe'll apply our criteria as we go. Because without the perspective of who you're naming for, the criteria can seem abstract. With it, they become specific and actionable.\n\nOn your own or with your team, write down a few thoughts in each of these sections, one idea per sticky note.\n\nThink. What does the team want people to think when using this type of feature? Maybe … that it's easy, or helpful, or even clever. When you do this, write down as many ideas as you can and don't filter yet.\n\nYou're not looking for the perfect word. You're looking for the themes that keep coming up.\n\nThen move on to \"feel\". What should people feel? Like when you find a place you couldn't remember the name of for the longest time. That can feel like a fun moment. And secure. Knowing your places are private and encrypted. Both matter here.\n\nAnd finally, \"do\". The team wants people to find this feature, use it, and share places with whomever they want. Now keep in mind that I'm only showing a few examples here. You might be surprised by how many you or your team actually come up with. More ideas are better here.\n\nOnce all the ideas are down, step back. You'll start to notice things repeating different words pointing to the same feeling. That's when you start to group them by theme.\n\nFor visited places, a few themes came up. Ease — finding what you're looking for without effort. Excitement — the fun surprise of rediscovering a place you loved. And security. Because this feature was built with privacy in mind, that has to come through.\n\nWhat name could capture ease? Excitement? A sense of security? We can probably cross out a few right away, because they don't meet the criteria we've been talking about. Some of these just don't fit Apple.\n\nSome feel a little vague, while others might not translate well.\n\nYou can narrow down the options even further with a simple test. Take the names and drop them into the sentences you might read or say to a friend.\n\nSomething like: Hey, check out Private Memories. Or just search for Private Memories. A name that reads and sounds natural — that's a name worth exploring further.\n\nThe Apple Maps team ultimately landed on Visited Places. Descriptive, clear, and already at home in an interface that uses the word places throughout.\n\nIt also sets the right expectation about ownership these are your places, that can't be read by Apple. And it works in different languages.\n\nWhen you're evaluating your own options, look for the name that honors the themes you identified and tests well against the criteria. Remember, it doesn't have to pass all 3 criteria, but it's great when it does.\n\nThe examples we've looked at lean straightforward and descriptive but that's not the only approach. The right name for your app might be something a bit more evocative or expressive.\n\nThe Photos app has a feature that looks at your library and automatically surfaces moments that matter. A birthday, a trip, a regular Tuesday that turned out to be worth remembering.\n\nIt's an algorithmic photo grouping. What would you call it? Think about who's on the other end of this feature. Someone who just opened up Photos and found something from five years ago they'd forgotten about. Or a video of a laugh they hadn't heard in years.\n\nThat person isn't thinking about algorithms. They're looking for a memory.\n\nMemories works because it meets them there in a way that a technical label never could. It fits the tone of the app and the relationship people have with their photos. And while it is still straightforward, the clarity comes from emotion, not explanation.\n\nEvery example we've looked at so far works but for different reasons. The question is how you know when yours does too.\n\nLet's walk through an example in Apple Podcasts. Audio quality in podcasts can vary wildly, some shows are polished, while others are recorded in kitchens or cars.\n\nSo here's where that feature lives, in a menu that also controls the playback speed.\n\nIt isolates voices, reducing background noise without altering the original audio.\n\nThere are a few ideas that pop up.\n\nRight away, vocal isolation comes to mind. But wait… For a feature like this, a verb works well because we're putting the person using it in control. The feature is something you do, not something you have.\n\nSo let's try Isolate Vocals instead.\n\nBut, that's an audio engineering term. It describes what the technology does, not what you experience.\n\nHow about Clarify Speech? Closer. But it only tells half the story. There's more to this feature than clarity alone.\n\nAnother obvious choice might be something like Enhance Playback, but it puts the feature before the person. What's being enhanced? For whom? How about Enhance Dialogue? It answers both questions — what's being enhanced and for whom before you even tap.\n\nAnd that's exactly what they went with. It fits the context. It sets the right expectation. And when you turn the feature on, it delivers exactly what it promised. All three criteria, working seamlessly.\n\nAnd the good news? The same name already appears on Apple TV for a similar feature — even more evidence that it belongs.\n\nThere's more than one way to be clear. Let's look at a name that takes a slightly different approach.\n\nAutoMix is a setting in Apple Music that does what a good DJ does — keeps the music moving. It handles the transitions between songs automatically, so playback never stops.\n\nAuto — it happens without you doing anything. Mix — it blends songs together.\n\nTogether they form a word that doesn't exist yet you can understand it immediately.\n\nNaming deliberately isn't just for hero features. And when you name with intention, you don't have to default to words that already exist. AutoMix earns its clarity from its parts so that the invented word doesn't have to explain itself.\n\nDescriptive like Enhance Dialogue, emotional like Memories, branded like AutoMix — the criteria don't change. How you weigh them is your call.\n\nSo what does this all add up to? Three things worth taking with you.\n\nNaming is as fundamental to the experience of your app as the layout, interactions, and visual scheme. It shapes how someone experiences your app. The next time you're naming something, come back to the criteria. Does it belong in your app? Set the right expectation? Will it hold up everywhere your app lives? The best names don't just describe what something does. They speak to the person using it. When those two things align — what the product is and what the person needs that's when the name feels like it truly belongs.\n\nEvery good name you choose makes the next one easier. Names build on each other — and over time, they become the language of your app.\n\nNaming is one of those decisions that can feel small in the moment. But the clarity it brings, the trust it builds, and the way it makes someone feel at home in your app — that accumulates. The next time you're wondering what to call something, you have a way to work through it. And your app or game will be better for it.\n\nIf you want to dig deeper, check out some of our other UX writing sessions from previous years. We can't wait to see what you build and what you name it. Thanks for watching!",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Human Interface Guidelines: Writing",
+ "url": "https://developer.apple.com/design/human-interface-guidelines/writing"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/290/4/6a4ef6cd-2a95-432c-aac9-315cb3cb7ff6/downloads/wwdc2026-290_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/290/4/6a4ef6cd-2a95-432c-aac9-315cb3cb7ff6/downloads/wwdc2026-290_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "250",
+ "year": "2026",
+ "title": "Principles of great design",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/250"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:19.165Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-292.json b/data/wwdc/videos/2026-292.json
new file mode 100644
index 0000000..690c6fd
--- /dev/null
+++ b/data/wwdc/videos/2026-292.json
@@ -0,0 +1,44 @@
+{
+ "id": "292",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/292/",
+ "title": "Design intuitive search experiences",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Design",
+ "Essentials"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hi, I'm Rob, a designer on the Apple Design team.\n\nToday, I'll be talking about how to design an intuitive search experience for your app. Search is one of the most important tools for helping people find, navigate, and discover content.\n\nAnd with apps continuing to offer a wider range of content and experiences, Search gives users a superpower.\n\nThe ability to get to exactly what they need without the frustration of having to spend time looking for it.\n\nAnd search is often the first thing people look for when opening your app. Whether it's getting back to a document in Pages, finding a movie on Apple TV, or exploring new artists and genre's in Apple Music, search plays a key role in making these interactions, feel effortless. Which is why designing a great search experience, is an important part of your app.\n\nAnd with Liquid Glass, we introduced new patterns that make search more ergonomic on iOS and that take full advantage of the larger displays on iPad and Mac.\n\nIn this session, I'll start by introducing the Search Field and the core behaviors and interactions it supports.\n\nThen, I'll provide an overview of the different search patterns that are available. And how to best choose where search lives in your app.\n\nFinally, I'll talk about some general, best practices. And how to make common interactions like inputting text and filtering search results as intuitive and seamless as possible. Let's start by taking a look at the Search Field...\n\nApple provides developers a search component, that includes the core elements and interactions people have come to expect when searching. Such as… A leading Search icon that visually establishes the UI as a Search Field.\n\nPlaceholder text, that communicates where people can enter their search term. A clear button, that shows once text is entered.\n\nAnd on iOS, when Search is focused, there is an additional Cancel button that is presented. Allowing people to exit Search while also dismissing the keyboard. Depending on where your Search Field is placed, it will automatically adopt the correct presentation style. Such as using glass when placed in a Toolbar or using standard content styling when placed in the scroll region of your app. If your app has its own distinct brand and set of iconography, make sure to keep the core elements of a Search Field intact. Any custom icons should closely resemble the symbols they are replacing, And symbols like the magnifying glass, have become universally recognizable as Search.\n\nNow, let's explore some of the different search patterns that are available and how to choose which approach is right for your app. Starting with iOS. On iOS... Search can be placed as a field or button in the Toolbar.\n\nAs a tab, in the tab bar.\n\nOr as a field underneath the top Toolbar or in the content area of your app.\n\nIt's important to highlight that where you place search, directly impacts where the field animates to, when active. When search is placed in the bottom Toolbar, the field elegantly animates up over keyboard, optimizing for reachability, and keyboard input. When Search is placed inline, as a field, it remains at the top when active, avoiding any UI at the bottom of your app.\n\nWhen deciding where to place Search, there are two big questions to ask. The first... how are people navigating my app? This will help you understand if you need to accommodate certain components like a Tab Bar.\n\nThe second... what is the scope of my Search? Certain placements may impact people's perception of what content they are searching.\n\nLet's put this into practice with a few of Apple's own apps.\n\nIn the Mail app, most of the navigation happens through the mail list, and there are contextual toolbars presented over each view.\n\nThis is a great example of when to place Search in a bottom toolbar. It's the most ergonomic position, and it's adjacent to other primary actions in the app.\n\nThe placement here also clearly conveys that once active, you'll be searching your mail.\n\nAnd based on the number of adjacent items in the Toolbar, the width of the field automatically adapts. Accommodating both a leading, and a trailing action.\n\nIf you need more than two other toolbar items, search can also start as a button and then animate into a field when tapped.\n\nWhile the bottom toolbar position is preferred, Search can also be placed in a Top Toolbar. The Stocks app is a great example of where this is useful. Here, the bottom of the Stocks list is occupied by a sheet. Precluding the placement of a bottom Toolbar or field.\n\nHere, Search is placed in the Top Toolbar. And similarly pulls the field up over the keyboard when tapped.\n\nNow, let's take a look at a different kind of navigation. Tabbed apps. A tab bar helps people quickly switch between the top level sections of your app. And tabbed apps often have a variety of rich content and views that can be made searchable. Here, it is generally recommended to create a primary entry point for Search. A single space where people can expect to find all of the relevant content within your app. You can do this by creating a Search Tab. With a Search Tab, you have two options.\n\nThe first, is to use a standard tab that keeps search uniform with the rest of your bar.\n\nHere, tabbing over will navigate to a landing page with the Search Field presented at the top.\n\nThis leaves room to surface any content, or suggestions, that might be helpful before Searching.\n\nThe second option, is to use a button appearance by making Search a prominent tab.\n\nThis will convey to people that tapping, will immediately engage search, and bring up the keyboard.\n\nThe first approach can be useful when your app has a breadth of rich content, and people may be in a more exploratory mindset when searching. For example, Apple TV uses the Search Tab to present the various genres, and categories available before searching. This helps ground people in what's available. In cases where people generally know what they're looking for and need quick access to search, I recommend the second approach. Making Search a prominent tab. A great example is in the Phone app. Here, people just want to get back to a recent call or contact, and tapping search immediately brings up the keyboard.\n\nThis placement ensures search is always present and never more than a tap away.\n\nWhile a dedicated Search Tab is great for offering a more unified, global search within your app, there are cases where just searching a specific tab can be useful.\n\nYour Apple Music library is a great example. Here, search is placed directly inline with the content and underneath the title. Both, the title and more descriptive placeholder text in the Search Field, help to reinforce you'll just be searching the albums in your music library. And not the entirety of your app.\n\nThis pattern is particularly useful if your app has more than one Search Field and when location plays a critical role in the scope of your Search.\n\nNow, let's take a look at iPad, and Mac... With both platforms offering a wider display and sharing similar navigation models in apps, the way you approach search is generally similar. And, I recommend trying to keep your iPad and Mac search experiences, as closely aligned as possible.\n\nOn both platforms, you can place your apps primary Search Field... in the trailing position of the Toolbar at the top of the Sidebar, or at the top of a dedicated Search Tab or section.\n\nLet's take a look at a few examples, and I'll help you decide what's best for your app. For Splitview apps, where you need to search across multiple columns of information, such as Mail, place Search in the trailing position of the toolbar. It's a great use of space, as it lets people navigate results, while keeping the selected content visible in the detail view. This is also one of the most common and familiar search patterns for users. Leveraged in apps like Notes and Files.\n\nYou should also consider placing Search in the toolbar if you would expect search results to appear in the detail view of your app. For example, here in Freeform, where Search directly filters the boards below.\n\nIf your toolbar has multiple items and groups, the Search Field will scale, or collapse into a button, based on the space available.\n\nWhen activated, search expands to a width optimized for text input, and moves any overflow items into a menu.\n\nAnother common pattern is to place search in the sidebar. For example, in the Settings app. I recommend placing Search here, when wanting to filter content or navigation that is directly in the sidebar.\n\nThis can be particularly useful, if your app has a rich detail view and you need to draw a clean line between the list you're searching and an adjacent view. For a good example, let's take a look at the Stocks app. Here, you can use search to find, and add symbols to your stocks list, and the sidebar placement makes it clear where you'll be searching.\n\nIf we were to place Search over the Top Stories section, it could set the expectation you would be searching for news and stories. Like on iOS, on iPad and Mac you can make Search a dedicated tab or item in the sidebar. When you have a rich, multi-section app such as Apple Music, this can be helpful in giving people a single place to search, for all of the content available in your app.\n\nThis more immersive approach, also gives you a larger canvas to express search results.\n\nIn this last section, I'll cover some key, best practices that will help elevate your search experience. Starting, with how to leverage search suggestions. When people turn to Search in your app, it might mean they didn't initially find what they were looking for. That's why it's important to make getting to their result, as easy and frictionless as possible. And in many cases, users may be returning to something they previously searched for. Displaying recent searches, can help people get back to a previous result, without the need to even start typing.\n\nOn iOS, recent searches should be shown directly inline when the field becomes focused.\n\nOn iPad and Mac, if your Search Field is placed in the Toolbar, or Sidebar, recent searches can be shown in a menu.\n\nAnd if you have a Search Tab, they can be presented alongside other content suggestions on your page.\n\nConsider being selective in the recent searches that are shown here. In some apps, it can be helpful to only surface the specific results, users viewed, or engaged with. It's important to think about what's most helpful to people in your app.\n\nIt's also important to let people remove their recent searches. You can use a swipe gesture on individual items, as well as a button in the section header to clear all searches. Once someone starts typing, it's important to show relevant results, as quickly as possible. Alongside results, predictive suggestions can be integrated, that help reduce the need to type out an entire query. These suggestions should directly correspond with what the user is typing, and feel like a natural completion of their search. To keep people oriented, visually distinguish between user input, and the predictive part of the suggestion. I also recommend limiting the number of suggestions being shown, so that search results feel front, and center. Remember, when results and suggestions are ranked efficiently, people generally shouldn't have to type out their entire search. Another key aspect of Search is helping people refine, or filter their results. This is especially important when searching across multiple locations, categories, or accounts.\n\nFor your app's primary Search Field, it's generally recommended to start with a broader search, and then let users narrow down results as needed.\n\nFor lightweight filtering, Apple provides a control called a scope bar.\n\nIn the Mail app, a scope bar allows people to switch between seeing results across all of their Mailboxes, or just their current Mailbox. In apps like Mail, where you might be navigating multiple Mailboxes or locations, this is a great way to help people see only the results they're looking for, while also reinforcing where they're searching.\n\nFor apps that search across multiple categories, it may be helpful to offer a more robust range of options for narrowing results. To avoid overwhelming people, consider only showing filters that are relevant, and contextual, to what someone is looking for.\n\nA great example is in the Maps app. Here, filters are tailored to accommodate a wide range of location types, from restaurants to hiking trails. Another way people can refine their search, is with search tokens. Tokens filter results, by specific keywords that surface as they type. Once applied, tokens appear as highlighted text within the Search Field, and users can continue adding to their search. This makes it easy for people to narrow results, by a specific person, place, or type of content.\n\nBecause tokens live as text within the Search Field, they allow people to apply filters using more natural language, and in apps like Photos, they can even be combined to create personalized filters. For example, viewing your photos from Joshua Tree in 2021.\n\nWhile powerful, tokens can also be less discoverable. So don't use them to replace more visible filtering UI in your app. In fact, tokens work great when paired with a scope bar, or other filter controls. Finally, let's talk about how to fail gracefully. In cases where a search doesn't return any results, it's best to display a well-considered empty state, or no results view.\n\nA completely blank view can leave someone wondering if their search even went through. For this, Apple provides developers with a content unavailable view, that when configured for search displays a search symbol, title, and subtitle to communicate no results were returned.\n\nConsider displaying the current search text in this view, to help people quickly catch any typos or errors.\n\nAnd with that, this brings us to the end of our session today. I encourage you to take what I shared today, and think about the opportunities within your app. For example, are there places where search could be moved to the bottom, to help improve reachability? Or do you have a tabbed app that would benefit from a Search Tab? And finally, are you leveraging tools like suggestions and filters to make search feel as effortless as possible? To learn more about what I shared today, check out the Human Interface Guidelines.\n\nFor tools to help you get started, you can explore our design resources. And to go deeper on our design system, I suggest checking out these talks, from previous years.\n\nI'm so excited to see how you leverage these updates and guidelines, in your own apps. Thanks for watching!",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/292/5/05adbfdf-d9ba-4a6d-8d2f-f43593907f55/downloads/wwdc2026-292_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/292/5/05adbfdf-d9ba-4a6d-8d2f-f43593907f55/downloads/wwdc2026-292_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "359",
+ "year": "2025",
+ "title": "Design foundations from idea to interface",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/359"
+ },
+ {
+ "id": "356",
+ "year": "2025",
+ "title": "Get to know the new design system",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/356"
+ },
+ {
+ "id": "219",
+ "year": "2025",
+ "title": "Meet Liquid Glass",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/219"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:19.249Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-295.json b/data/wwdc/videos/2026-295.json
new file mode 100644
index 0000000..194b4b3
--- /dev/null
+++ b/data/wwdc/videos/2026-295.json
@@ -0,0 +1,77 @@
+{
+ "id": "295",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/295/",
+ "title": "Validate your App Intents adoption with AppIntentsTesting",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "Developer Tools"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi. I am Venkatesh, a Software Engineer on the App Intents team. Today I'm going to show you how to test your App Intents with a brand new framework called AppIntentsTesting. The App Intents framework is a core part of how your app integrates with the system. And every year, it powers more experiences, for example, App Intents enables people to interact with their apps using Siri. It enables people to use the Shortcuts app to build powerful automations.\n\nSpotlight uses it to surface your app's contents in system-wide search. Widgets use it to display your data on the Home Screen.\n\nIf you are new to AppIntents, check out this talk from WWDC25. And if you want to learn about new advances, check out this talk from WWDC26. With App Intents powering so many experiences, every intent, entity, and query needs to work correctly. AppIntentsTesting is a brand new framework that lets you test all of that, and more. Here is what we will cover today. First, I will introduce the sample app, and show you how to write your first test. Next, we'll look at how AppIntentsTesting runs your tests.\n\nWe'll then level up, by testing entity queries, and combining multiple intents.\n\nAnd finally, we'll test some parts of App Intents that reach beyond your app; Spotlight and view annotations I'll now dive right in and show you how easy it is to get started with AppIntentsTesting. This is CometCal, a sample calendar app. It's a SwiftUI app that manages calendars, events, and attendees, and it fully integrates with App Intents. My colleague Justin and I have been working on it to make it easier for our crew of space adventurers to manage our missions. In this session, we'll learn how to test it, using AppIntentsTesting.\n\nTo learn about how we made CometCal available to Siri, check out his code-along.\n\nI'll start off by writing a simple test that exercises the Create Calendar flow.\n\nAll I need to get started with AppIntentsTesting is a UI Testing bundle. I don't have one yet, so let me create one.\n\nLet me give it a descriptive name. Next, I pick a development team. AppIntentsTesting requires the test runner and the app to use the same development team for code signing, so I'll make sure to pick the correct Team here. Next, I'll put in a Bundle Identifier.\n\nAnd select the correct Target. Click Finish.\n\nThis creates my UI testing bundle for me. Let me create a new file for all my tests related to intent execution.\n\nGreat, I am now ready to start writing some tests.\n\nLet me navigate to the intent I'm testing.\n\nCreateCalendar creates a new calendar with a name, and color.\n\nTo begin, I need to import the AppIntentsTesting framework.\n\nI start with a standard XCTestCase and create a test method.\n\nI start the testing flow by creating an IntentDefinitions object.\n\nIntentDefinitions takes in the CometCal bundle identifier, and gives me access to every intent, entity, and query the app defines without importing the app. To access the definition of a particular intent, I can use the intents property and pass in the Intent name as a subscript.\n\nThis gives me an IntentDefinition object. I can call makeIntent on it, to create a populated instance of the Intent.\n\nI can pass in any parameters the intent expects.\n\nHere, CreateCalendarIntent takes in a name and a color. I am in charge of a critical mission for humanity, and I need a descriptive name so my crew and I can fulfill this mission.\n\nI'll call it Occupy Saturn. And give it a color. I'll make it red.\n\nHere, color is an AppEnum. All I need to do is pass in the AppEnum's raw string value, and the framework converts it to the correct type. The test code doesn't build against your app, therefore the parameter names and types do not offer auto-completion. I need to make sure to fill them out correctly based on the intent parameters. This type conversion works out of the box with most parameter types. However, if you want to pass in your own custom types, look up IntentValueConvertibleWrapper in the documentation. Now that I created the intent, I call the .run() method to execute it in app.\n\nIf I need the return value from an intent's perform method, I can capture the result of the run method, and use the value property to get it.\n\nHere, CreateCalendarIntent returns the new calendar as a CalendarEntity.\n\nThis is CalendarEntity.\n\nIt has a title property that we can check.\n\nSo, if I want to assert that the new event has the correct title, I first access the new calendar using result.value, and simply chain the required property name to it. This uses dynamic member lookup to get the title from the new calendar.\n\nI am now ready to run this test. But before I do so, let me switch over to my phone, and check out which calendars I currently have.\n\nThe app currently has three calendars: Deep Space, Mission Control, and Stargazing. After I run the test, I expect the Occupy Saturn calendar to be added to the list. Let me switch back to Xcode, and run this test.\n\nThe test succeeded.\n\nLet me check the phone to confirm that the new calendar exists. Yup, the test created the Occupy Saturn calendar with the color red.\n\nAll it took was five lines of code, and I executed an on-device App Intent using a test. Now that you've seen AppIntentsTesting in action, I'll talk about it in more detail.\n\nAppIntentsTesting is an integration testing framework for your App Intents. Similar to UI tests, it uses strings to find your intents, without any access to the app's internals.\n\nYour tests live in a standard XCUITest bundle, running in their own process. Your app runs in a separate process, and executes AppIntents on-device.\n\nThe test runner executes your App Intents across process boundaries. And receives execution results, without sharing any state.\n\nHere's what AppIntentsTesting means for you as a developer. Your tests run through the full App Intents stack; the same code path people hit. No mocks, no stubs. Create an XCUITest bundle, or add it to your existing XCUI tests. Your Continuous Integration pipeline picks it up automatically. Structure your app code any way you want. Your test target never imports any code from your application- you only pass a bundle identifier. That means you don't need to compile your app code into your testing target.\n\nAll this gives you stable tests across releases. They don't depend on any UI. Not your app's. Not your system's.\n\nWith that overview in mind, I'll move on to some more involved tests. Your app's actions, data and queries are the building blocks of your App Intents integration. They power Shortcuts, Siri interactions, and Spotlight search. When this code regresses, it can often lead to people experiencing unexpected system behaviors.\n\nWhenever your entities are looked up- in Shortcuts, through Siri, or anywhere else App Intents surfaces them- your entity queries are responsible for returning the right results. AppIntentsTesting helps you verify that behavior for string queries, identifier lookups, and suggested entities. Here's the situation. I have a mostly harmless device called a cosmic ray. Due to an unfortunate incident, I need to rename the Cosmic Ray Calibration event to Cosmic Ray Rebuilding. In fact, it's been happening so often, I should probably create a Shortcut for it.\n\nThe event already exists in the app. So I go to the Shortcuts app, go to my Shortcut, and search for the event. Initially, it shows a list of suggested events. If I want to search for some other events, I can use the search bar which calls EventEntity's string query to determine the results.\n\nNo Options Available; the string query isn't implemented yet. I'll use AppIntentsTesting to implement this with a test-driven approach.\n\nHere's my test. I start off by creating the entity and intent definitions.\n\nI want to ensure that the app contains a known set of events, so I run SeedSampleEventsIntent. This resets app data, and adds events. I will come back to this shortly. Ideally, I'd lift the Entity and Intent definitions out of the test function so other tests can reuse them, and move the data seeding to the setUp method. Let me do that. Going forward, I focus purely on test functionality.\n\nHere is the updated test. I call the Entity string query using the entities(matching:) method on the Event entity definition. This executes the string query on-device. The method returns an array of EventEntity representations, so you can assert on the count.\n\nSimilar to intents, I can use dynamic member lookup to get the value of any entity property. Here, I am getting the title of the event, and asserting on it to confirm that the query works correctly. I'll run the test. And it fails, just as expected. The failure tells me exactly what I need to build.\n\nNow I navigate to the EventEntityQuery. Currently, it can return events by identifier, and return suggested events. However, it has no EntityStringQuery capability. Let me add one.\n\nI'll add an EntityStringQuery conformance and implement the entities(matching:) method. Filter events by title and return the matched EventEntities.\n\nNow I go back to the test and run it, and it passes. I have confirmed that the Entity string query works as expected.\n\nNow, I verify the changes on device. First, I make sure the event exists. Next, I go to the Shortcuts app, open my Shortcut, and search for the same event again. This time, it finds the event.\n\nI wrote the test first, then built the feature to make it pass. That's test-driven development with AppIntentsTesting. Now, it's time to rebuild that Cosmic Ray! Time to level up, by combining multiple intents in one test. Often, to build complex Shortcuts, people take the results of an intent, and pass it as a parameter into another. This flow forms the basis of many useful automations. Here's the situation; I had set up a practice session for my team to get good at Asteroid Dodgeball. However, one of our teammates is new, so I ended up changing the purpose of the Event to go over the rules instead. Since this is a core workflow of the app, I want to write a test to simulate it.\n\nThis test builds on our previous examples. Let me walk you through it.\n\nStart off by running CreateEventIntent with the required parameters. I've gone through how makeIntent handles type conversion in the previous examples.\n\nThe first three parameters all take primitive types. The last one is more interesting. The calendar parameter takes a CalendarEntity. However, I just pass in a string. The AppIntents runtime automatically calls the EntityStringQuery associated with the CalendarEntity and fills in the first matching value.\n\nAfter creating the event, I check the title just to make sure.\n\nNext, I run UpdateEventIntent. This takes in an event, and any updates I want to make. Since I am just updating the title, I pass that in. For the event to update, I can directly send in the EventEntity from the original run method. This mirrors how people compose intents in Shortcuts.\n\nAnd finally, I assert that the event has the updated title.\n\nThis time, during test setup, I am launching the app, just so I can confirm it's working correctly. Time to run the test. The app launches, the event is created, and then updated.\n\nBoom! Entity creation, chaining, update, and assertion; all on device, in a single test. Now where did I keep my rulebook for Asteroid Dodgeball … AppIntentsTesting tests are lightweight, fast and can run on every commit. For the test results to be reliable, each test needs to be self-contained. Test-Only intents help with that.\n\nTest-only intents are simple, focused intents that only exist to help your tests. You can use them to improve test functionality. You can create exactly the data your tests need. No leftover data from previous runs causing flaky results.\n\nYou can jump directly to any view in your app without UI navigation. If you redesign a screen, these tests still work. And here's the broader idea; test-only intents can wrap any functionality in your app, even things you haven't adopted App Intents for yet. Internal navigation, data management, state manipulation- wrap it in a test-only intent, and you can test it through AppIntentsTesting. CometCal's source code includes a number of test-only intents. One example is SeedSampleEventsIntent from the earlier string query test. It creates a known set of events in your calendar. Here's how you can make any intent a test-only intent.\n\nMark it with isDiscoverable: false. This prevents the system from exposing it anywhere else.\n\nAnd Wrap it in #if DEBUG compiler directive so only your tests can access this intent.\n\nUp to this point, you've learned how to test your App Intents in isolation. The real power, though, is how App Intents lets your features reach people across the system, so they can use your app's functionality outside your app. I'll now show you how to test those system-level integrations.\n\nSpotlight lets people search across the entire system from one place. When you index your entities, they show up right in those results, so people can find your app's content without needing to open the app first. When CometCal creates an event, it should index the entity in Spotlight. However, I once got an urgent transmission from my fleet commander asking me when the Dark Matter Symposium was. I know that nanoseconds are precious, so I wanted to use Spotlight to get this information and reply pronto. However, the Spotlight search surfaced nothing. This information on Dark Matter is fascinating, but not what I'm looking for! I went to the app, and the event did indeed exist. I quickly sent them the details, and got to work debugging.\n\nI navigated to the event creation code to debug. The bug is simple; during development, I commented out the indexing code and never re-enabled it. The app's behavior didn't change, so the bug went undetected.\n\nAnd the fix is simple, just uncomment the indexing call.\n\nBut how to make sure this never happens again? I can write a test, not to find the bug, but to prevent a regression.\n\nI start by using the spotlightQuery() method to talk to Spotlight. This method takes a string, and returns a list of spotlight indexed EventEntity representations matching it. Initially, the event does not exist, so I ensure that spotlightQuery() returns no results.\n\nNext, I use CreateEventIntent to create an event, exactly the same as before.\n\nOnce created, I expect the event to exist in the Spotlight index, so I query for it again. I assert that there is exactly one result, and the title matches. Now I run the test… And it passes. This test can now run on every commit. So if the Spotlight integration ever breaks again, this test will catch it immediately.\n\nNow, to perform a quick check to confirm that the event is indeed indexed in Spotlight. Great! Now, my fleet commander gets their answer at the speed of Spotlight! View annotations tell the system which entity your view is currently showing, so Siri can understand what's on screen and act on it directly. CometCal tells Siri which EventEntity is on screen every time you navigate to an event. So, if I am viewing an event, and an unexpected wormhole jump yanks the phone from my hands, I can always call out and say: \"Siri. What time is this event?\".\n\nIdeally, the view annotation tells Siri exactly which event is on screen, and Siri answers right away. But if that annotation is broken, Siri has no idea what's on screen.\n\nNow, inter-dimensional travel happens to me more often than I'd like to admit, so I really need this feature working. Let me fix the the bug with the help of AppIntentsTesting.\n\nI start off by navigating to the event page of an event using OpenEventIntent.\n\nBecause this test exists in an XCUITest bundle, I can use XCUI Automation to confirm that the app indeed renders the correct event on the screen.\n\nI can then call the viewAnnotations() method on the Event entity. This method retrieves the list of EventEntity view annotations the system reports as currently on screen.\n\nNow for the assertions. First, since only one event is on screen, I assert that the system returns only one ViewAnnotation object.\n\nThen I assert that the Event entity is correct based on the title.\n\nA ViewAnnotation object has an entity property that holds the actual entity representation. So, I can use dynamic member lookup to assert on specific properties such as title. Now I run the test. And it fails.\n\nThen, I navigate to the EventDetailView to find out why. The issue is with the EntityIdentifier. I accidentally passed the event's calendar id as the identifier.\n\nThe fix is simple; I change it to the event's id.\n\nNow I rerun the test. And this time it passes.\n\nSo now, the next time inter-dimensional travel catches me off-guard, I can say \"Siri. When is this event?\", and get the correct response. I can even follow up with \"Where is it?\", and Siri responds with the location.\n\nI've covered a lot today. Now, take a step back and look at how AppIntentsTesting can augment your App Intents development workflow.\n\nStart your AppIntents journey by implementing the fundamental types. These are your app's actions, data and queries. Now with AppIntentsTesting, you verify the foundational behavior of these types.\n\nThese tests act as unit tests for your App Intents integration.\n\nOnce the fundamentals are tested, integrate your app more deeply with the system. Annotate your views with entities, donate to Spotlight and pass data between apps. AppIntentsTesting lets you cover these integrations too, validating your app's experiences across the many ways people use it.\n\nThese can be your App Intents integration tests. With coverage at every layer, your app is ready to unlock some truly powerful experiences. At this stage, be sure to test your intents manually with Siri and the Shortcuts app to experience them just the way people would. That's AppIntentsTesting. A single framework that lets you test your intents, entities, enums, queries, and system integrations- all out-of-process, all automated.\n\nDownload the CometCal sample project to try it yourself, and explore the complete set of tests for the app.\n\nThis talk only scratches the surface of what AppIntentsTesting can do. Check out the documentation for the full API reference. And if you want to learn how to build Siri experiences with App Intents, check out this talk. Now you are ready to go and build something that is out of this world! Thanks for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "6:48",
+ "title": "Your first test: execute an intent",
+ "language": "swift",
+ "code": "import AppIntentsTesting\n\nfunc testCreateCalendar() async throws {\n let definitions = IntentDefinitions(bundleIdentifier: \"com.example.apple-samplecode.CometCal\")\n let createCalendar = definitions.intents[\"CreateCalendarIntent\"]\n let result = try await createCalendar.makeIntent(\n name: \"Occupy Saturn\",\n color: \"red\"\n ).run()\n XCTAssertEqual(try result.value.title, \"Occupy Saturn\")\n}"
+ },
+ {
+ "timestamp": "12:25",
+ "title": "Test an entity string query",
+ "language": "swift",
+ "code": "// Testing Entity string queries\nfunc testEventStringQuery() async throws {\n let results = try await eventEntityDefinition\n .entities(matching: \"Cosmic Ray\")\n\n XCTAssertEqual(results.count, 1)\n XCTAssertEqual(try results[0].title, \"Cosmic Ray Calibration\")\n}"
+ },
+ {
+ "timestamp": "13:00",
+ "title": "Implement the EntityStringQuery under test",
+ "language": "swift",
+ "code": "// Updated query implementation\nstruct EventEntityQuery: EntityStringQuery {\n func entities(for identifiers: [EventEntity.ID]) async throws -> [EventEntity] {\n\n }\n\n func suggestedEntities() async throws -> [EventEntity] {\n\n }\n\n func entities(matching string: String) async throws -> [EventEntity] {\n try calendarManager.fetchEvents()\n .filter { $0.title.localizedCaseInsensitiveContains(string) }\n .map(\\.entity)\n }\n}"
+ },
+ {
+ "timestamp": "15:42",
+ "title": "Chain multiple intents in one test",
+ "language": "swift",
+ "code": "// Test event creation followed by update\nfunc testCreateAndUpdateEvent() async throws {\n let createResult = try await createEventDefinition.makeIntent(\n title: \"Asteroid Dodgeball Practice\",\n startDate: Date(),\n isAllDay: false,\n calendar: \"Deep Space\"\n ).run()\n\n XCTAssertEqual(try createResult.value.title, \"Asteroid Dodgeball Practice\")\n\n let updateResult = try await updateEventDefinition.makeIntent(\n title: \"Asteroid Dodgeball Rules Overview\",\n event: createResult.value\n ).run()\n\n XCTAssertEqual(try updateResult.value.title, \"Asteroid Dodgeball Rules Overview\")\n}"
+ },
+ {
+ "timestamp": "17:45",
+ "title": "Make an intent test-only",
+ "language": "swift",
+ "code": "// Test-only intent: SeedSampleEventsIntent\n#if DEBUG\nstruct SeedSampleEventsIntent: AppIntent {\n static let isDiscoverable = false\n\n func perform() async throws -> some IntentResult {\n // Create known list of events\n return .result()\n }\n}\n#endif"
+ },
+ {
+ "timestamp": "20:27",
+ "title": "Test Spotlight indexing",
+ "language": "swift",
+ "code": "// Testing Spotlight indexing\nfunc testNewEventIndexedInSpotlight() async throws {\n\n let before = try await eventEntityDefinition.spotlightQuery(\"Supernova Viewing Party\")\n XCTAssertTrue(before.isEmpty, \"Event should not exist in Spotlight yet\")\n\n // ... Create \"Supernova Viewing Party\" Event\n\n let after = try await eventEntityDefinition.spotlightQuery(\"Supernova Viewing Party\")\n XCTAssertEqual(after.count, 1)\n XCTAssertEqual(try after[0].title, \"Supernova Viewing Party\")\n}"
+ },
+ {
+ "timestamp": "22:33",
+ "title": "Test view annotations",
+ "language": "swift",
+ "code": "/ Testing view annotations\nfunc testEventViewAnnotation() async throws {\n try await openEventDefinition.makeIntent(target: \"Morning Launch Briefing\").run()\n\n // Confirm correct event page\n let app = XCUIApplication()\n let title = app.staticTexts[\"Morning Launch Briefing\"]\n XCTAssertTrue(title.waitForExistence(timeout: 5))\n\n let annotations = try await eventEntityDefinition.viewAnnotations()\n\n XCTAssertEqual(annotations.count, 1, \"Expected exactly one view annotation\")\n XCTAssertEqual(try annotations[0].entity.title, \"Morning Launch Briefing\")\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Testing your App Intents code",
+ "url": "https://developer.apple.com/documentation/AppIntentsTesting/testing-your-app-intents-code"
+ },
+ {
+ "title": "App Intents Testing",
+ "url": "https://developer.apple.com/documentation/AppIntentsTesting"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/295/4/cdcee6d3-e3e9-4201-b1ef-cd33e2d10e6f/downloads/wwdc2026-295_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/295/4/cdcee6d3-e3e9-4201-b1ef-cd33e2d10e6f/downloads/wwdc2026-295_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:19.343Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-297.json b/data/wwdc/videos/2026-297.json
new file mode 100644
index 0000000..a6422cd
--- /dev/null
+++ b/data/wwdc/videos/2026-297.json
@@ -0,0 +1,101 @@
+{
+ "id": "297",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/297/",
+ "title": "Best practices for integrating visual intelligence in your app",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm David, an ML engineer on System Experience.\n\nLet's build something with Visual Intelligence.\n\nIn this session, I'll take you step by step through integrating your app with Visual Intelligence and share some best practices along the way. Since Visual Intelligence was introduced, people have been using it to quickly learn more about what's around them whether in their physical surroundings or on their iPhone screen.\n\nThis year, we're adding new capabilities like adding to contacts, saving multiple calendar events, and medical device logging, as well as bringing Visual Intelligence to iPad and macOS.\n\nSo how do you bring your app into this experience? I'll show you by building one. I love listening to and discovering new music. So I want to create an app that helps me discover albums and find upcoming concerts. Here's what we'll build today. This is my music app. I can browse albums, check out upcoming concerts, and start listening to anything with a tap. If I take a picture or screenshot of some album artwork, and highlight to search, my app shows matching albums and concerts right in Visual Intelligence. I can even capture a post about an upcoming concert, use Visual Intelligence to add the event to my calendar and the concert shows up automatically in my app. By the end of this session, you'll know how to build all of this. There are a few steps to making the most of your integration with Visual Intelligence, which I'll go over today. First, we'll define the content we want to return from our app using App entities.\n\nNext, we'll implement a query so Visual Intelligence can find and return our app's content.\n\nThen, we'll take our integration beyond iOS — bringing it to Mac and iPad, covering some considerations for each platform along the way.\n\nAnd to wrap things up, we'll explore system store integrations where information extracted by Visual Intelligence can be read by your app automatically, from common data stores you may have already adopted. Let's start with the basics of Image Search. Integrating Image Search leverages both the App Intents and Visual Intelligence frameworks. If you're new to App Intents, I'd recommend checking out these sessions from WWDC25.\n\nThe first step to integrating Image Search is defining the content we want to return. We'll use App entities from the App Intents framework for this. App entities are the nouns within your app. In our app, I want our Image Search to first return visually similar albums, so I'll define an album entity.\n\nLet's see how this looks in code.\n\nWe'll start by defining an AlbumEntity that Visual Intelligence can display in its search results. First I'll add a default EntityQuery and a typeDisplayRepresentation, which are standard for any App entity.\n\nThen I'll define the content of the entity. Each AlbumEntity has an identifier, name, artistName, and thumbnail data for the album artwork.\n\nAnd I'll add a displayRepresentation which tells Visual Intelligence how to present each result.\n\nLet's talk about the display representation we just defined. This is the first thing people will see in the Image Search results. And there's not a lot of room — you get about three lines of text for a title and subtitle, as well as a thumbnail image.\n\nIt's good practice to put the most important identifying information here. In my case, the album name and the artist.\n\nAnd if you initialize a display representation with an image URL, I'd recommend serving a thumbnail-sized image when appropriate, rather than pointing to your full-resolution asset.\n\nFor example, if you always expect to return multiple results, using smaller images can help your results load faster and still look good in a two column layout.\n\nHowever, if you only return one result, keep in mind that this image will take up the full width of the results sheet. Now that I have my entity defined, how does Visual Intelligence actually query my app for results? That's where the Intent value query comes in. An Intent value query is a lightweight query protocol that provides entity values to the system.\n\nYou might already have one if you've adopted App Intents to make your app work with Siri.\n\nFor Visual Intelligence, the key difference is the input, the system passes a SemanticContentDescriptor containing information about the captured image. Let's jump back to the code to build this.\n\nI'll adopt the IntentValueQuery protocol and implement its values for requirement with a SemanticContentDescriptor as input.\n\nIn the body, I'll grab the pixelBuffer from the input and pass it to my catalog.search method, which returns matching albums.\n\nBut how does that search actually work? Let's look at that next.\n\nFor this app, I'll search on device using a local catalog of saved albums. I'll use the Vision framework for this, which provides pre-trained machine learning models for computer vision tasks.\n\nEach entry in our catalog will have a featurePrint, a compact numerical representation of the image, which we can use to compare image similarity.\n\nI'll define a function to compute feature prints using GenerateImageFeaturePrintRequest.\n\nI'll make sure to pre-compute these for albums in my catalog, so we don't need to do this computation at query time.\n\nFor our query, I'll first convert the pixelBuffer to a CGImage using VideoToolbox. Then, I'll generate a new feature print for this image.\n\nI'll compare that against the pre-computed feature prints in my catalog, applying a maximum distance threshold to filter out dissimilar results.\n\nFinally, I sort by similarity and return the top results.\n\nA few things to note. I pre-compute feature prints for my album catalog, to keep the query fast. And I sort results by similarity so the best match appears first. Whether you're searching on device or hitting a server, the same principles apply — return results fast and ranked.\n\nI'd also recommend limiting the number of results returned to ensure they're relevant. If you don't find any good matches, you can return an empty array. The system will handle displaying an empty response.\n\nAnd I encourage you to check out the Vision framework APIs to learn more about image processing techniques you can use in your app. We just scratched the surface with feature prints, but you can do so much more like extract text, scan barcodes, detect faces, and classify images, just to name a few. These can be incredibly useful techniques for extending the capabilities of your app's visual search.\n\nNow, how can we land people on the right screen of the app when they tap on a result? For that, we need an OpenIntent. When someone taps an album in the Image Search results, the system calls this intent with the selected entity.\n\nMy perform method navigates to the album detail page.\n\nYour OpenIntent should take people straight to the content they selected.\n\nIf you already have an OpenIntent for your entity from adopting App Intents to power other features, you can reuse it here too. You don't need a separate one just for Visual Intelligence.\n\nAnd it's recommended to keep this lightweight. This method runs as the app comes to the foreground, so do your navigation and save any heavy loading for after the view appears.\n\nAnd that's everything you need for a basic Image Search integration. Let's take a look at what we've built so far.\n\nMy friend sent me this recommendation, let's use Visual Intelligence to start listening to it in our app. I'll take a screenshot, highlight to search, and choose our app from the available providers.\n\nOur query worked, and we were able to find the album and return it as the top result.\n\nIt's worth mentioning that your app appears here alongside other adopting apps. The system decides the ordering based on which Image Search providers are available on the device. If I tap on this result, it takes me right to the album page in my app. That's our entity, our query, and our OpenIntent all working together. Now, let's bring this to more platforms. This year, Visual Intelligence is also available on iPadOS and macOS. The same APIs are available on these new platforms as well, with minimal changes needed to your app.\n\nYour IntentValueQuery, your entities, and your OpenIntent all work across iOS, iPadOS, and macOS. That's the same code we just wrote.\n\nThat said, there are a few platform differences worth keeping in mind. On iOS, people often use Visual Intelligence through the camera — capturing physical objects like vinyl records or concert posters.\n\nOn macOS and iPad, the primary entry point is screenshots — capturing digital media. Make sure your search handles both kinds of content well.\n\nAlso keep in mind that on Mac, the input pixel buffer can be much larger than what you'd encounter on iPhone. Consider if resizing is necessary for your use case.\n\nLet's build our app for macOS and see how it looks.\n\nI'll take a screenshot of that same image, and with no changes to our query or entity code, our app's Image Search works on macOS. The result looks great.\n\nNow that we've covered the basics, I want to add even more capabilities to our app.\n\nWhat if we could search not only for visually similar albums, but also for upcoming concerts by artists of those albums? For that, we can use UnionValue. Since our app can only have one IntentValueQuery that accepts a SemanticContentDescriptor, I'll define a @UnionValue enum with a case for each entity type — album and concert.\n\nAnd since I have two entity types now, I'll need an OpenIntent for each one.\n\nThen I'll update my query to return this union type.\n\nI'll search for the top matching albums first, then use the artists from those albums to find nearby concerts, and combine them into a single results list. Consider if it makes sense for your app to return multiple types of results. And it's worth thinking about the different types of content your app can return beyond simply matching pixels. I found albums through image similarity, then used those artist names to surface nearby concerts, a completely different kind of result.\n\nFeel free to be creative about the type of content you return based on the context.\n\nAs a final touch, if people don't find the result they're looking for immediately, I want to provide an easy way for them to continue the search inside the app. We can use the semanticContentSearch schema to do that. I'll create an intent conforming to the semanticContentSearch schema. The system provides the semanticContent property automatically. That's the same SemanticContentDescriptor we saw before with the pixel buffer. In perform, I'll navigate to an in-app search view with some pre-populated search results.\n\nNow when someone taps More results, they'll land in my app's full search experience. It's good practice to use semantic content search to give people a way to continue into your full search experience.\n\nAnd you can pre-populate your search view based on the input context, rather than starting from scratch.\n\nYour app can show much more than the Visual Intelligence results view — filters, categories, the full depth of your content. Take advantage of that.\n\nLet's see everything we've built in action. I'll take another screenshot of this album, and my app returns matching albums and concerts right in Visual Intelligence.\n\nAnd if I want to browse more, tapping the More results button takes me into my app's full search.\n\nWe've talked about your app providing results to Visual Intelligence. But there's another side to this story. Your app can also receive data from Visual Intelligence through system store integrations. Providing results to Visual Intelligence is done through the Image Search integration, which is everything we've built so far.\n\nOther Visual Intelligence actions write data to system stores, which provide developers with a bridge to shared system data. Events can be read with EventKit, contact information with Contacts, and medical device readings with HealthKit. If your app already reads from the data stores in these frameworks, Visual Intelligence becomes a new source of input automatically.\n\nFor our app, I want to know upcoming concerts people are interested in so we can suggest songs for them to listen to beforehand. So let's add an EventKit integration to access these events.\n\nThis is my UpcomingConcertManager, which uses EKEventStore.\n\nI'll request read access to calendar, then query for upcoming events.\n\nFor our app, I'll simply filter for events in the near future that match artists in my catalog.\n\nI'll also add a notification observer so new events, including ones created by Visual Intelligence, appear automatically.\n\nNow let's see the final piece.\n\nWhen I capture this social media post about an upcoming concert, Visual Intelligence detects the event so I can add it to my calendar.\n\nWhen I open my app, it's already there in Upcoming Concerts, with a suggestion to start listening. The same pattern applies to other system stores. Contacts added through Visual Intelligence, for example from a business card, can be accessed through CNContactStore.\n\nAnd medical device readings captured by Visual Intelligence from displays on blood pressure monitors, glucose meters or weight scales can be queried using HKHealthStore. If your health or fitness app reads from HealthKit, Visual Intelligence becomes another way for people to log data without manual entry. We've covered a lot today. To recap, Visual Intelligence offers two powerful integration points for your app.\n\nYou can provide results to Visual Intelligence through Image Search, and you can receive data from Visual Intelligence through system store integrations. With Visual Intelligence now available on iOS, iPadOS, and macOS, your integration can reach people across their devices. If you want to learn more, check out the documentation available on the developer website.\n\nYou can also view these related sessions to explore further capabilities in App Intents and the Vision framework. Thanks for watching. I can't wait to see what you build with Visual Intelligence.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:21",
+ "title": "Define the content you want to return as an App Entity",
+ "language": "swift",
+ "code": "// Define the content you want to return as an App Entity\n import AppIntents\n \n struct AlbumEntity: AppEntity {\n var id: String\n @Property var name: String\n @Property var artistName: String\n var coverArtData: Data\n \n var displayRepresentation: DisplayRepresentation {\n DisplayRepresentation( \n title: \"\\(name)\",\n subtitle: \"\\(artistName)\",\n image: .init(data: coverArtData)\n ) \n } \n \n static let defaultQuery = AlbumEntityQuery()\n \n static var typeDisplayRepresentation: TypeDisplayRepresentation { \"Album\" }\n } \n \n struct AlbumEntityQuery: EntityQuery {\n @Dependency var catalog: AlbumCatalog\n func entities(for identifiers: [String]) async throws -> [AlbumEntity] {\n catalog.albums(for: identifiers)\n }\n }"
+ },
+ {
+ "timestamp": "5:39",
+ "title": "Adopt IntentValueQuery to return results",
+ "language": "swift",
+ "code": "// Adopt IntentValueQuery to return visual search results\n import AppIntents\n import VisualIntelligence\n \n struct SearchHandler: IntentValueQuery {\n @Dependency var catalog: AlbumCatalog\n @Dependency var concertFinder: ConcertFinder\n \n func values(for input: SemanticContentDescriptor) async throws -> [VisualSearchResult] {\n guard let pixelBuffer = input.pixelBuffer else {\n return []\n } \n \n let albums = try await catalog.search(matching: pixelBuffer)\n \n return albums.map { VisualSearchResult.album($0) }\n }\n }"
+ },
+ {
+ "timestamp": "6:24",
+ "title": "Build a catalog of albums with precomputed feature prints",
+ "language": "swift",
+ "code": "// Build a catalog of albums with precomputed feature prints\n import Vision\n \n @Observable\n class AlbumCatalog {\n static let shared = AlbumCatalog()\n \n struct CatalogEntry: Sendable {\n let album: AlbumEntity\n let featurePrint: FeaturePrintObservation\n } \n \n private(set) var entries: [CatalogEntry] = []\n \n private func generateFeaturePrint(\n for image: CGImage\n ) async throws -> FeaturePrintObservation {\n let request = GenerateImageFeaturePrintRequest()\n let result = try await request.perform(on: image)\n return result\n }\n }"
+ },
+ {
+ "timestamp": "6:45",
+ "title": "Search the catalog for albums matching the captured image",
+ "language": "swift",
+ "code": "// Search the catalog for albums matching the captured image\n func search(matching pixelBuffer: CVReadOnlyPixelBuffer, limit: Int = 10, maxDistance: Double = 1.0) async throws ->\n [AlbumEntity] {\n var cgImage: CGImage?\n _ = pixelBuffer.withUnsafeBuffer { VTCreateCGImageFromCVPixelBuffer($0, options: nil, imageOut: &cgImage) }\n guard let cgImage else { return [] }\n \n let queryPrint = try await generateFeaturePrint(for: cgImage)\n \n return try entries.compactMap { entry -> (album: AlbumEntity, distance: Double)? in\n let distance = try queryPrint.distance(to: entry.featurePrint)\n guard distance <= maxDistance else { return nil }\n return (entry.album, distance)\n } \n .sorted { $0.distance < $1.distance }\n .prefix(limit)\n .map { $0.album }\n }"
+ },
+ {
+ "timestamp": "8:27",
+ "title": "Create an open intent to land users on the right screen",
+ "language": "swift",
+ "code": "// Create an open intent to land users on the right screen\n import AppIntents\n \n struct OpenAlbumIntent: OpenIntent {\n static let title: LocalizedStringResource = \"Open Album\"\n \n @Parameter(title: \"Album\")\n var target: AlbumEntity\n \n @Dependency var appState: AppState\n \n func perform() async throws -> some IntentResult {\n await appState.openAlbum(id: target.id)\n return .result()\n }\n }"
+ },
+ {
+ "timestamp": "12:05",
+ "title": "Use UnionValue to return multiple visual search result types",
+ "language": "swift",
+ "code": "// Use UnionValue to return multiple visual search result types\n @UnionValue\n enum VisualSearchResult {\n case album(AlbumEntity)\n case concert(ConcertEntity)\n } \n \n struct OpenConcertIntent: OpenIntent {\n static let title: LocalizedStringResource = \"Open Concert\"\n \n @Parameter(title: \"Concert\")\n var target: ConcertEntity\n \n @Dependency var appState: AppState\n \n func perform() async throws -> some IntentResult {\n await appState.openConcert(id: target.id)\n return .result()\n }\n }"
+ },
+ {
+ "timestamp": "12:18",
+ "title": "Expand the IntentValueQuery to return the UnionValue",
+ "language": "swift",
+ "code": "// Expand the IntentValueQuery to return the UnionValue\n struct SearchHandler: IntentValueQuery {\n @Dependency var catalog: AlbumCatalog\n @Dependency var concertFinder: ConcertFinder\n \n func values(for input: SemanticContentDescriptor) async throws -> [VisualSearchResult] {\n guard let pixelBuffer = input.pixelBuffer else {\n return []\n } \n \n let albums = try await catalog.search(matching: pixelBuffer)\n \n let artists = albums.map { $0.artistName }\n \n let concerts = await concertFinder.findNearby(byArtists: artists)\n\n return albums.map { VisualSearchResult.album($0) }\n + concerts.map { VisualSearchResult.concert($0) }\n }\n }"
+ },
+ {
+ "timestamp": "13:13",
+ "title": "Provide a link to in-app search",
+ "language": "swift",
+ "code": "// Provide a link to in-app search\n @AppIntent(schema: .visualIntelligence.semanticContentSearch)\n struct SemanticContentSearchIntent: AppIntent {\n static let title: LocalizedStringResource = \"Search in app\"\n static let openAppWhenRun: Bool = true\n \n var semanticContent: SemanticContentDescriptor\n @Dependency var catalog: AlbumCatalog\n @Dependency var concertFinder: ConcertFinder\n @Dependency var appState: AppState\n \n func perform() async throws -> some IntentResult {\n guard let pixelBuffer = semanticContent.pixelBuffer else { return .result() }\n let albums = try await catalog.search(matching: pixelBuffer)\n let artists = albums.map { $0.artistName }\n let concerts = await concertFinder.findNearby(byArtists: artists)\n await appState.openSearch(albums: albums, concerts: concerts)\n return .result()\n } \n }"
+ },
+ {
+ "timestamp": "15:24",
+ "title": "Request calendar access and fetch upcoming concerts",
+ "language": "swift",
+ "code": "// Request calendar access and fetch upcoming concerts\n import EventKit\n \n @Observable\n class UpcomingConcertManager {\n private let eventStore = EKEventStore()\n var upcomingConcerts: [EKEvent] = []\n var authorizationStatus: EKAuthorizationStatus = .notDetermined\n \n func requestAccessAndFetch() async throws {\n let granted = try await eventStore.requestFullAccessToEvents()\n guard granted else {\n authorizationStatus = .denied\n return\n } \n authorizationStatus = .fullAccess\n await fetchUpcomingConcerts()\n\n // ...\n }\n }"
+ },
+ {
+ "timestamp": "15:42",
+ "title": "Filter for upcoming events that match known artists in our catalog",
+ "language": "swift",
+ "code": "// Filter for upcoming events that match known artists in our catalog\n class UpcomingConcertManager {\n func fetchUpcomingConcerts() async {\n let predicate = eventStore.predicateForEvents(\n withStart: .now,\n end: .now.addingTimeInterval(90 * 24 * 60 * 60),\n calendars: nil\n ) \n \n let events = eventStore.events(matching: predicate)\n \n upcomingConcerts = events.filter { event in\n AlbumCatalog.shared.entries.contains { entry in\n event.title?.localizedCaseInsensitiveContains(entry.album.artistName) == true\n }\n }\n }\n }"
+ },
+ {
+ "timestamp": "15:44",
+ "title": "Observe newly created events",
+ "language": "swift",
+ "code": "// Observe newly created events\n @Observable\n class UpcomingConcertManager {\n // ...\n\n func requestAccessAndFetch() async throws {\n // ...\n\n for await _ in NotificationCenter.default\n .notifications(\n named: .EKEventStoreChanged\n ) {\n await fetchUpcomingConcerts()\n }\n }\n }"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Integrating your app with visual intelligence",
+ "url": "https://developer.apple.com/documentation/VisualIntelligence/integrating-your-app-with-visual-intelligence"
+ },
+ {
+ "title": "Visual Intelligence",
+ "url": "https://developer.apple.com/documentation/VisualIntelligence"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/297/5/25343020-b502-4808-967a-6f6460789dc2/downloads/wwdc2026-297_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/297/5/25343020-b502-4808-967a-6f6460789dc2/downloads/wwdc2026-297_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:19.489Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-298.json b/data/wwdc/videos/2026-298.json
new file mode 100644
index 0000000..76f9f1b
--- /dev/null
+++ b/data/wwdc/videos/2026-298.json
@@ -0,0 +1,120 @@
+{
+ "id": "298",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/298/",
+ "title": "Meet the Evaluations framework",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Essentials"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Yada. And I'm Rob. We're excited to introduce the Evaluations framework. A new framework that measures the quality of your intelligent features so you can deliver your apps with confidence.\n\nLast year we introduced the Foundation Models framework, which helped you add intelligent features to your apps, using our on-device models.\n\nThe same models which power Apple Intelligence.\n\nBuilding app features with generative AI poses new testing challenges, because the same input can produce different outputs. These models break a contract that is fundamental to software testing.\n\nConsider traditional software, where a particular input always produces a particular output. You can easily verify this behavior with a unit test.\n\nYou're guaranteed the same input will produce the same output on any device, including your customers'.\n\nWith intelligent software, you cannot rely on functional consistency to verify behavior. Which means that unit tests are insufficient. Unverified behavior can erode customer confidence. Your customers expect intelligent features in your app, like any feature, to be safe, trustworthy, and reliable. Shipping a feature with unpredictable behavior, can have adverse consequences on your app's reputation.\n\nIt's important we measure our intelligent features and understand how they respond to different inputs. And since functional tests can't verify probabilistic behavior, we need new a form of test that is more robust.\n\nWe need to know: how often does my app produce unexpected results? How often does the agent take an unexpected path to generate answers? And under what circumstances does the feature produce unsafe results? The challenges of testing intelligence features powered by generative AI is exactly why we built the Evaluations framework.\n\nThe Evaluations framework is a flexible system of provided types and protocols. This video will focus on evaluating intelligent features powered by language models.\n\nBut you can evaluate any stochastic system, such as classifiers and linear regression models.\n\nYada and I will introduce you to several types in the framework.\n\nWe'll cover data loading and building a diverse dataset.\n\nBuilding quantitative metrics with Evaluator and Metric.\n\nAnd refining your measurements using model judges and score dimensions to create qualitative metrics. In this video, you'll get started with Evaluations. After building your first evaluation, we'll show you how to scale that evaluation, with more data, and more measurements.\n\nThen we'll teach you how to build powerful model judges using our simple API.\n\nLet's get started with Evaluations.\n\nYada and I are building an app called Book Tracker. We both love books and wanted an app to manage our libraries. Yada just added a new feature, called BookTaggingService. It automatically tags books based on a review we've written in Book Tracker.\n\nI can't wait to open Xcode and try it out.\n\nLet's add a #Playground macro to BookTaggingService.swift. Here's the review of \"Pride & Prejudice\" Yada added to Book Tracker. Have to say I'm a fan myself. Let's see what tags we get back.\n\nThis a good start, but as I'm reading some of the tags, I can see our service will need a little work.\n\n9 tags is more than I was expecting.\n\nAnd I don't want the book's name as a tag, either.\n\nMulti-word tags are gonna be a problem in the UI, so we should avoid those as well.\n\nLet's see if we have better luck with another review: \"Dracula\".\n\n7 tags is within our expected amount. Let's take a closer look at them. There are some behaviors that I'd like to see more of.\n\nIt identified literary genres, and some categories that would help me browse a larger library.\n\nOkay, we've just completed our first evaluation of the service.\n\nWe created a list of expectations and used our human judgement to measure how the service performed. Every evaluation measures how well an intelligent feature performs against our expectations.\n\nUnfortunately human judgement doesn't scale.\n\nBut we've created a way to automate and scale evaluations. All you have to do is add import Evaluations, and implement the Evaluation protocol.\n\nLet's build an evaluation in code.\n\nAnd we'll start with our first expectation: measuring that our service generates the correct number of tags. There are five steps to building and running an evaluation. You define what code you're measuring. Then, define what data you're sending the code. Next, define what measurements you're making and how.\n\nThen, summarize your measurements. And then, finally, create a test to run your evaluation.\n\nFirst, we add the call to the BookTaggingService, and return it's output inside of the subject(from:) method.\n\nThese generated tags are the subject of our evaluation. Next, define the input samples we'll feed the code we're measuring.\n\nThen, we'll use ModelSample to wrap the same reviews we tested in the #Playground earlier: \"Pride & Prejudice\" and \"Dracula\". Notice we define expected tags as well. These are the ideal tags we'd like to see from the service.\n\nNow, its time to define our measurements, using the Metric type.\n\nWe add a Metric called \"TagCount\", which will track the number of generated tags returned by the service. We need something to measure the generated tags. Evaluator takes a closure, that gets passed the output from the service, for a given sample. We can check the number of generated tags by using the count of the tags property.\n\nIf the length of the tags array is between 3 and 8, we return a passing metric from our Evaluator.\n\nIf not, we return a failing metric.\n\nEvaluators run over a single sample at a time. But we can measure trends and look for patterns measured over all of our samples in the aggregateMetrics(using:) method. Let's calculate the average number of times the service generates the correct number of tags.\n\nThen we'll have a ratio for how often the service behaves correctly.\n\nOkay, we've written our first evaluation. Next, let's write some code to run it.\n\nEvaluations integrates with Swift Testing, so you can run your evaluations in your app's test targets. Here we instantiate our BookTaggingEvaluation inside of a Test Suite. We add some notes to our evaluation run, so we can keep track of the configuration we're evaluating. This will be helpful later, when we compare across different evaluation runs.\n\nNext, we add a test function, using the @Test macro, and a new @Test trait .evaluates. This trait takes our evaluation and a notes dictionary, like the one we've created earlier in the @Suite.\n\nInside our @Test, we can access an evaluation results bundle. This contains all of the metrics and aggregate metrics from our evaluation run. Let's grab all of our tagCount metrics from the results, and assert against its average value. We'll use the aggregateValue method on the results bundle. Then, assert against the average in an #expect macro.\n\nHere, I expect the service to produce the correct number of tags 80% of the time.\n\nWhy 80%? If the service performance dips below 80%, I want to know and a failing test is great signal. But what if I want even more insight into what happened during the evaluation? We have a new test report for evaluations. It's a great way to dive into the details of your evaluation and analyze further.\n\nLet's run our test, and I'll walk you through the report. Based on what the service returned earlier in my #Playground, specifically how many tags it generated for \"Pride & Prejudice\", I don't expect the test to pass.\n\nOkay, the test didn't pass. Let's go to the report and review what happened. Click on the report navigator, and then select Evaluations in the test report.\n\nHere's the evaluation report for the test suite. Let's double click the row to find out more.\n\nAnd I see my TagCount metric only passed 50% of the time. And a quick look at the full results table shows me that my \"Pride & Prejudice\" sample produced a failure. But my \"Dracula\" sample produced the correct number of tags.\n\nI can select each row in the table to see more details, using the assistant editor in Xcode. The detail panel shows the prompt, and each measurement for the ModelSample. At the bottom, you see the entire response from the model.\n\nLet's recap a little. We built an evaluation for BookTaggingService. Ran that evaluation and it failed to meet our optimization target.\n\nRemember back in our test definition? This is where we defined our optimization target. We're saying the feature behaves as expected, if the correct number of tags were generated, 80% of the time.\n\nBeyond the automated check of our optimization target we need to analyze deeper into our results and gather insights. Specifically, think about the changes that could be made to improve the feature's performance.\n\nI have a hunch, so I look back at the @Generable type, BookTags, that the service is generating.\n\nWe already have a @Guide macro giving the model additional instructions for the tags property.\n\nI could specify a count property in that @Guide, which can take a range. That should instruct the model to only generate between 3 and 8 tags.\n\nThis is an interesting theory. Let's make that change.\n\nThen re-run the evaluation to see if I'm right. We call this process hill-climbing.\n\nAll right, I made the change and I re-ran the evaluation. My test passed, and my TagCount passes a 100% of the time. But I notice a potentially strange behavior: after my change, the service always generates eight tags. Hmmm.\n\nNow that we have the Evaluations set up, let's collect more measurements across more samples, and let's see if that strange behavior persists.\n\nWe started our evaluation with only two data samples. As we saw, that only gave us two measurements to extract trends.\n\nGood evaluations have thousands of samples to extract trends, but also to exercise your feature in many different ways.\n\nWe should consider variety in our dataset. For example… We want the service to recognize different genres. We can't assume every user will give it a verbose review, so our reviews should be different lengths.\n\nYou browse for fiction and non-fiction using different categories, your samples should represent that variety.\n\nFinally, you should consider different forms: novels, short stories, and essays.\n\nLet's makes it hard on the model too. Sprinkle in personal opinions, so we can measure how well the service ignores those in the reviews.\n\nIf you want to teach the feature how to write tags like you, start by including more of your personal style in the expected values of the samples.\n\nLet's look at a few examples in code. This review of \"The Secret Garden\" reads very different than the reviews we started with because we wrote it as though we were an avid gardener.\n\nHere we challenge the model, including a personal review from a mother reading \"Treasure Island\" to her son. Lots of personal opinions in this review.\n\nThis board game enthusiast needed multiple paragraphs for their review of the Chinese classic, \"Romance of the Three Kingdoms\".\n\nWhile this casual reader described a famous British detective's sidekick in a single sentence.\n\nThe game is afoot, when the model tries to decipher this one.\n\nAnd while it's fun to come up with these examples, human data creation doesn't scale, either.\n\nConsider these sentence completion pairs, where the output of the feature is compared directly to the expected answer. You need thousands of examples for this evaluation to be effective.\n\nFortunately, we include a SampleGenerator as part of the Evaluations framework. You can call it directly on an array of ModelSamples and it will synthetically generate more samples using a model of your choice.\n\nTo hear more about how you can synthesize larger datasets, and learn more about advanced uses of ModelSample, please check out our video \"Create robust evaluations for agentic apps\".\n\nBack to BookTagging. I'm going to update my dataset property to include all of the book reviews from our library, including the four we showed earlier.\n\nWhen I re-run my evaluation with the expanded dataset, my test passes, my TagCount average is still 100%, and the service generated eight tags for all of them. Now we know there's a weird behavior in the service.\n\nLooking back at my expectations, I've built an evaluator to track if the number of tags are in range. I think I still need to refine that a little. Here's my current Metric and Evaluators setup.\n\nFirst, I define a new Metric, \"TagTotal\", that will record the number of generated tags.\n\nThen I build a simple Evaluator, which records the length of the generated tags array. Then, we record a measurement using a scoring value, instead of a pass/fail value.\n\nUsing the \"TagTotal\" and \"TagCount\" metrics we evaluate range compliance and the distribution of generated tags.\n\nWe can follow a similar pattern for checking the number of words in tags. Here, we check each tag for a space, then returning a failing metric if it does. Identifying a literary genre is equally straightforward assuming you're looking for a known set of genres. We check the BookTaggingService for knownGenres. Then compare each of the generated tags for a match.\n\nOur evaluation is really filling out. We can already measure three of our original five expectations. And our evaluation report provides a rich picture of how our tagging service is performing. We track our three expectations using five aggregate metrics. Here, we can see the distribution of tags, along with range compliance and containing genre tags.\n\nUsing our hill-climbing methodology, we've iterated on our instructions for the service. Here's where we started at the beginning.\n\nAfter several updates to our evaluation and multiple runs through our loop.\n\nAnd we can track each change to our instructions, by an expectation we added to our evaluation to verify that change.\n\nWhen you take our hill-climbing feedback loop, and center your development process around it, we call it evaluation-driven development.\n\nBut we're not done getting our service up to spec.\n\nWe still expect our tags to be informative, relevant to the book and helpful for browsing your library. Here's Yada to tell you about model judges, and how they'll take your evaluation to the next level. Thanks Rob. Model judges are how we measure qualitative metrics at scale. Let me show you how to build and refine one. Let's take a look at a concrete example. Here's a review of \"Alice in Wonderland\" that Rob wrote in Book Tracker.\n\nAnd here are the tags that our service generated.\n\nSix tags, single word or hyphenated, with tags identifying genre. Every quantitative metric we built with Rob passed.\n\nBut look closer. 'Overrated' and 'pretentious' doesn't describe the book — they describe how the reader felt about it. And 'whodunit' isn't even the right genre. The model picked it up from 'riddles he never answers.' It latched onto the language of the review without understanding the book. Our metrics are passing, but they're not giving us the right signals back.\n\nBut, I think we can ask a model to help us here. If a person can read these tags and tell us which ones work, maybe a model can too.\n\nOh nice! The model actually captured that certain tags are not helpful.\n\nI think I can ask the model to evaluate all of the tags that my feature generated! And that's exactly what a Model Judge is. A Model Judge is a language model used to score your feature's output. It gives you a subjective rating — the kind of judgment call a person would make — but applied consistently across your entire dataset.\n\nSo let's talk about how this works. Here's the model powering your intelligence feature. Our BookTaggingService runs on-device because it needs to be fast and local for every user interaction. You can use a second model as a judge to evaluate your feature. Your judge should be at least as capable as the model you're evaluating. In our case, we can use a more capable model from Private Cloud Compute.\n\nThe model judge has a few key components.\n\nThe instruction tells the model it will be given book reviews, and how it should evaluate it.\n\nThe feature input is the prompt given to the feature being judged, in our case, its the book review.\n\nThe feature output is the tags our service generated.\n\nAnd finally, the scoring guide tells the model how to evaluate and score the feature. The Evaluations framework handles most of this for you, so you can focus on the scoring guide.\n\nPutting it all together, here's a simple model judge. We've defined a \"TagQuality\" metric on a 1 to 4 scale, with each level describing what that score means. An even number of options prevents the judge from defaulting to a neutral middle score.\n\nFour levels provides just enough distinction without diluting the meaning of each rating.\n\nAnd finally, we've specified Private Cloud Compute as our judge model, giving us a more capable evaluator than the on-device model we're evaluating.\n\nIn the Evaluations framework, a model judge is just another Evaluator. It conforms to the same protocol as the quantitative evaluators and produces the same Metric type. So you can mix them freely within a single evaluation. Alright, let's run it! Every sample received a 3 or 4 quality score.\n\nLets go back to our \"Alice in Wonderland\" sample.\n\nThe model judge gave this a quality score of 3.\n\nIf we look at the rationale, we can identify that the model flagged 'whodunit' and 'detective-fiction' as not relevant to the book. But, we also expected it to flag all of these other tags that either reflect the reader's opinion or are not helpful for browsing.\n\nWith model judges, rationales are essential. They give you a window into why the judge scored what it scored.\n\nAnd here's the thing: by the scale we wrote, the judge is actually right. Every tag connects to something that the user wrote. The judge is faithfully following the scoring guide we provided. We meant something specific by relevant and useful for browsing, and the judge interpreted those words differently than we did.\n\nBy asking the model to provide judgement for my feature, in my place, I expected it to provide a similar score to how I would have scored these tags.\n\nWhen there is a mismatch between the model judge and us, we can refine the model judge until it can stand in for our own judgement.\n\nLooking back, the problem with our first model judge was that it was too broad. It was asking two different questions.\n\nWhen you find yourself disagreeing with a score, you should try and see if you can split the questions. In our case, relevance and usefulness are actually two different metrics. Lets take a look at defining \"Relevance\" as a ScoreDimension.\n\nWhen we say the tags are relevant we mean that each tag describes a quality, theme, or tone of the book itself rather than small details or the reader's personal reactions.\n\nAnd we can write that as the description for our ScoreDimension.\n\nTo score these tags, you'd walk through each one. Identify which tags are bad and which are good, based on whether or not they meaningfully describe the book.\n\nYou'd repeat this for every tag. In this case, all of the tags are good, which earns a score of 4 on our 1 to 4 scale. You would repeat the same process to define each scale in the scoring guide.\n\nAnd that's our \"Relevance\" metric with the metric name, description, and scale that the model judge can use.\n\nI can use the same process to define \"Usefulness\". Now, I can add both dimensions to the ModelJudgeEvaluator.\n\nBut dimensions alone aren't enough. They tell the judge what to measure, but not how to think about your app. Without that context, a judge evaluating tags for Book Tracker might treat a reader's criticism as a valid book descriptor. It has no way to know that Book Tracker is a personal library, not a review platform. And that's where the ModelJudgePrompt comes in.\n\nThis is an example of a ModelJudgePrompt. We can tell the judge its evaluating tags for a personal library app in the instructions.\n\nFormat the response in the evaluationTarget, and pass the expectedTags as reference for the model to compare against.\n\nFor more details on ModelJudgePrompt please see our documentation. Now that our model judge has the context it needs, lets rerun our evaluation.\n\nIn place of Quality we now have a relevance and usefulness score. And here is the evaluation result of our \"Alice in Wonderland\" book sample.\n\nNotice how the two rationales separate the diagnosis. Relevance tells us what kind of tag is wrong. And Usefulness tells us how the wrong tags fail at browsing. With these results, I now have a clear path forward. I can update my BookTaggingService instructions, run the evaluation again, and watch the scores change. That's the feedback loop Rob walked us through, now powered by qualitative metrics. When are you uploading to TestFlight? Well Rob, I've been a little busy! Let's wrap-up with a few best practices for evaluating your apps.\n\nStart small. A focused dataset of 20 to 30 samples is a great place to get started. Spec out your app by thinking about how you want the model to behave.\n\nUse heuristics to measure quantifiable traits. These rule-of-thumb metrics are a great way to start understanding your feature. The rule-of-thumb is: if you can measure it in code, then it's quantitative. And if you can only describe it in words, then you need a qualitative metric, using a ModelJudgeEvaluator. Start simple with your model judge. Define your scoring dimension, run it, and read the rationales. You'll learn more from a single run than from hours of careful planning. Use rationales to drive your next change. If the scores are all the same, your question is too broad. If you can't isolate the problem, split the dimensions. And if the judge doesn't understand your app, add context. Well, I guess we should get back to work. Be sure to check out our documentation. And our sample code. And check out our other videos featuring the Evaluations framework: \"Improve your prompts by hill climbing with Evaluations\", and \"Create robust evaluations for agentic apps\". Later!\n\nBye!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "4:54",
+ "title": "Define an Evaluation",
+ "language": "swift",
+ "code": "// Evaluations\n import Evaluations\n\n struct BookTaggingEvaluation: Evaluation {\n \n }"
+ },
+ {
+ "timestamp": "8:02",
+ "title": "Run with Swift Testing and an optimization target",
+ "language": "swift",
+ "code": "// Optimization Target\n @Test(\"Book Tag Evaluations\", .evaluates(evaluation, info: evaluationInfo))\n func evaluateBookTagging() async throws {\n let result = EvaluationContext.current.result\n \n let rangeMetric = BookTagEvaluationTests.evaluation.tagCount\n #expect(result.aggregateValue(.mean(of: rangeMetric)) >= 0.8)\n }"
+ },
+ {
+ "timestamp": "10:09",
+ "title": "Constrain output with a Generable @Guide",
+ "language": "swift",
+ "code": "// BookTags.swift\n @Generable\n struct BookTags: Codable {\n @Guide(description: \"Descriptive tags capturing themes, genres, moods, and topics from the summary\", .count(3...8))\n var tags: [String]\n } snippet."
+ },
+ {
+ "timestamp": "11:15",
+ "title": "Define the dataset with ModelSample",
+ "language": "swift",
+ "code": "// BookTaggingEvaluation\n var dataset = ArrayLoader(samples: [\n ModelSample(prompt: \"okay I am OBSESSED and I need everyone to read this RIGHT NOW...\",\n expected: BookTags(tags: [\"classic\", \"romance\", \"wit\", \"regency\"])),\n\n ModelSample(prompt: \"Read this in one sitting between midnight and 4am and I cannot...\",\n expected: BookTags(tags: [\"classic\", \"gothic\", \"horror\", \"vampire\", \"suspense\"])),\n ])\n \n // Or load your whole library:\n var dataset = ArrayLoader(samples:\n Book.sampleBooks.map { book in\n ModelSample(prompt: book.review, expected: BookTags(tags: book.tags))\n }\n )"
+ },
+ {
+ "timestamp": "12:53",
+ "title": "Synthesize more samples with a SampleGenerator",
+ "language": "swift",
+ "code": "// Synthesizing more inputs\n let samples: [ModelSample] = [\n ModelSample(prompt: \"The largest planet in our solar system...\", expected: \"Jupiter.\"),\n ModelSample(prompt: \"The capital of Thailand...\", expected: \"Bangkok.\"),\n ModelSample(prompt: \"Swift is...\", expected: \"a powerful programming language.\"),\n ModelSample(prompt: \"All those moments will be lost in time...\", expected: \"Like tears in rain.\")\n ]\n \n for try await sample in samples.makeSamples(\n \"\"\"\n Generate diverse sentence completions about the listed topics:\n - The Solar System\n - World Capitals \n \"\"\",\n targetCount: 1000) {\n samples.append(sample)\n }"
+ },
+ {
+ "timestamp": "14:02",
+ "title": "More evaluators: word count and genre",
+ "language": "swift",
+ "code": "let wordCount = Metric(\"WordCount\")\n\n Evaluator { _, subject in\n for tag in subject.value.tags {\n if tag.contains(\" \") {\n return wordCount.failing(rationale: \"Tag \\(tag) contains multiple words\")\n }\n }\n return wordCount.passing()\n }\n\n let hasGenreTag = Metric(\"HasGenreTag\")\n \n Evaluator { _, subject in\n let tags = subject.value.tags.map { $0.lowercased() }\n let knownGenres = await BookTaggingService.knownGenres\n for tag in tags {\n if knownGenres.contains(tag) {\n return hasGenreTag.passing(rationale: \"Matched \\(tag)\")\n }\n }\n return hasGenreTag.failing() \n }"
+ },
+ {
+ "timestamp": "14:03",
+ "title": "Define a Metric and Evaluator",
+ "language": "swift",
+ "code": "let tagCount = Metric(\"TagCount\")\n\n var evaluators: Evaluators {\n\n // Tag count is within the required 3–8 range\n Evaluator { _, subject in \n let count = subject.value.tags.count\n if (count >= 3 && count <= 8) {\n return tagCount.passing(rationale: \"\\(count) tags\")\n } \n return tagCount.failing(rationale: \"Got \\(count) tags, expected 3–8\")\n }\n }"
+ },
+ {
+ "timestamp": "14:27",
+ "title": "Aggregate metrics across samples",
+ "language": "swift",
+ "code": "let tagCount = Metric(\"TagCount\")\n let tagTotal = Metric(\"TagTotal\")\n \n func aggregateMetrics(using aggregator: inout MetricsAggregator) {\n aggregator.computeMean(of: tagCount)\n aggregator.group(\"Distribution of Tag Totals\") { aggregator in\n aggregator.computeStandardDeviation(of: tagTotal)\n aggregator.computeMean(of: tagTotal)\n aggregator.computeVariance(of: tagTotal)\n }\n }"
+ },
+ {
+ "timestamp": "15:33",
+ "title": "Iterate the feature's instructions (hill-climbing)",
+ "language": "swift",
+ "code": "// BookTaggingService.swift\n let instructions = Instructions {\n \"\"\"\n You are a librarian and literary analyst. Given a reader's\n freeform summary of a book they read — describing their\n thoughts, feelings, and what stood out — generate a set of\n descriptive tags reflected in the summary.\n\n Rules:\n - Return between 3 and 8 tags.\n - Tags should be lowercase, concise (single word or hyphenated), and descriptive.\n - Tags should include the book's genre, chosen from the included list of known genres.\n \n Known Genres:\n - \\(Self.knownGenres.joined(separator: \", \"))\n \"\"\"\n }"
+ },
+ {
+ "timestamp": "18:53",
+ "title": "Build a model judge",
+ "language": "swift",
+ "code": "ModelJudgeEvaluator(\n \"TagQuality\",\n scale: .numeric([\n 4: \"Tags are relevant and helpful for browsing\",\n 3: \"Mostly relevant, one tag too vague or generic\",\n 2: \"Several tags are wrong or generic\",\n 1: \"Unhelpful or irrelevant\"\n ]), \n judge: PrivateCloudComputeLanguageModel()\n )"
+ },
+ {
+ "timestamp": "22:17",
+ "title": "Split into score dimensions",
+ "language": "swift",
+ "code": "// BookTaggingEvaluation.swift\n ScoreDimension(\n \"Relevance\",\n description: \"\"\"\n Whether each tag describes a quality, theme, or tone\n of the book itself rather than incidental details or\n the reader's personal reactions.\n \"\"\",\n scale: .numeric([\n 4: \"Every tag describes the book itself\",\n 3: \"Most tags describe the book\",\n 2: \"Some tags describe personal reactions\",\n 1: \"Tags don't meaningfully describe the book\"\n ]) \n )\n // Define `usefulness` the same way as a second ScoreDimension."
+ },
+ {
+ "timestamp": "22:32",
+ "title": "Add dimensions to the judge",
+ "language": "swift",
+ "code": "// BookTaggingEvaluation.swift\n var evaluators: Evaluators {\n\n Evaluator { } \n\n Evaluator { }\n\n Evaluator { }\n \n ModelJudgeEvaluator(\n judge: PrivateCloudComputeLanguageModel(),\n dimensions: [relevance, usefulness]\n )\n }"
+ },
+ {
+ "timestamp": "23:17",
+ "title": "Add app context with a ModelJudgePrompt",
+ "language": "swift",
+ "code": "// BookTaggingEvaluation.swift\n ModelJudgeEvaluator(\n judge: PrivateCloudComputeLanguageModel(),\n dimensions: [relevance, usefulness],\n prompt: ModelJudgePrompt( \n instructions: \"\"\"\n You are evaluating tags generated for a personal book-tracking app where users\n organize their library by browsing and filtering tags.\n \"\"\",\n evaluationTarget: { value in\n \"\\(value.tags.count) Generated tags: \" + value.tags.joined(separator: \", \")\n },\n reference: { input, _ in \n let expectedTags = input.expected?.tags.joined(separator: \", \")\n return [\"Expected Tags\": expectedTags ?? \"No expected tags defined\"]\n }\n )\n )"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Book Tracker: Using Evaluations to evaluate an intelligent feature",
+ "url": "https://developer.apple.com/documentation/Evaluations/book-tracker-using-evaluations-to-evaluate-an-intelligent-feature"
+ },
+ {
+ "title": "Designing datasets to test your feature",
+ "url": "https://developer.apple.com/documentation/Evaluations/designing-evaluation-datasets"
+ },
+ {
+ "title": "Designing effective evaluations",
+ "url": "https://developer.apple.com/documentation/Evaluations/designing-effective-evaluations"
+ },
+ {
+ "title": "Evaluating language model responses",
+ "url": "https://developer.apple.com/documentation/Evaluations/evaluating-language-model-responses"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/298/5/0ffb7161-1edb-4e6f-872d-55be82c4402d/downloads/wwdc2026-298_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/298/5/0ffb7161-1edb-4e6f-872d-55be82c4402d/downloads/wwdc2026-298_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:19.615Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-299.json b/data/wwdc/videos/2026-299.json
new file mode 100644
index 0000000..544eb1d
--- /dev/null
+++ b/data/wwdc/videos/2026-299.json
@@ -0,0 +1,114 @@
+{
+ "id": "299",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/299/",
+ "title": "Create robust evaluations for agentic apps",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Essentials"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Ada! And I'm Kyle! And we're engineers on the Evaluations team! Today, we are so excited to walk you through some of the advanced features in the Evaluations framework! The Evaluations framework introduces a way to assess intelligence-powered features in Swift apps, track improvements over time, and ensure quality in production.\n\nThis framework is new in Xcode 27 and supports macOS, iOS, watchOS and visionOS.\n\nIf you haven't already, check out the \"Meet the Evaluations framework\" video to learn about the building blocks of the Evaluations framework and our other video \"Improve your prompts by hill climbing with Evaluations\" to explore different strategies to improve your intelligence features.\n\nIn this video, we'll discuss how to address complexity and scalability with your evaluations. We'll begin by exploring how to grow your evaluation dataset by generating and validating synthetic data. And then we'll cover how to build robust evaluations for agentic workflows that incorporate a special kind of model behavior known as tool calling.\n\nIn the \"Meet the Evaluations framework\" video, we introduced the hill-climbing process. This illustrates the process of how we build, test and ship intelligent features.\n\nIn this video, we will primarily focus on the Develop and Evaluate step. In the Develop step, we often start with a handful of samples for our evaluations, but your feature is almost always more complex than your dataset can cover. It takes time to build, it's harder to scale, and it rarely captures the variety you need to truly understand how your feature behaves in the real world.\n\nThe quality of your evaluation results is only as good as the data behind them. And writing good evaluation data is hard. That's where synthetic data comes in. The Evaluations framework exposes APIs that let you define sample generation entirely in code, so you can build your own generation pipeline, run it from the command line, or plug it directly into your existing workflows. It supports text-based data and leverages the generable macro to generate structured synthetic data.\n\nMy colleagues and I have been working on BookTracker which is a personal library app that uses intelligence-backed features to auto-tag books based on written reviews. Let's examine how we define each book.\n\nWe have a class named Book that includes the title, author, review, tags, and rating. We define other variables used to support the cover design. We also define sampleBooks which is an array of 13 Book samples, Like this one here about Pride and Prejudice. These 13 samples might feel like a reasonable starting point, but this small dataset only give us a narrow window into how our feature performs. Our evaluation results could look great and still be completely misleading. Think about the variety of possible data used to evaluate our tag generation feature! There are countless books.\n\nHundreds of genres.\n\nAnd a wide variety of ways a user might review what they just read. We're also talking about the real world where summaries can be vague or incomplete. Thirteen samples can't capture all of that. We need more coverage, and we need it without spending days writing examples by hand! So let's discuss how to expand our dataset to capture more of that variety. We'll start simple. The makeSamples API requires three components: a prompt, a dataset, and a target count, which is the number of samples you'd like to synthetically generate including the dataset you provide.\n\nHere, I've defined a prompt that asks the model to suggest more diverse book review samples. To write a well-defined prompt, consider what information the model may need in order to best understand the task and handle the range of inputs your users might provide. For our dataset, I'm passing in our sampleBooks which includes our 13 initial samples.\n\nHere we leverage the new ModelSamples API which includes the book's review as the prompt and the book's tags as the expected output.\n\nAnd for the target count, I've set it to one hundred samples to start! Remember, the targetCount is the size of the full resulting dataset, including the samples we started with, so the model will actually generate 87 new ones. Now you might be wondering how much data is enough? And the answer is, it depends. For our BookTracker app, a target count of one hundred is just the starting point! Synthetic data generation is often an iterative process of defining an initial dataset, generating synthetic data, validating the samples, then, analyzing whether or not the data is representative enough and continuing this cycle until you are confident! So the right target count for your evaluation dataset depends entirely on your feature. What it does, who uses it, and how many different ways people interact with it.\n\nWhat matters far more than quantity is coverage! So instead of asking how many samples do I need? Ask yourself, have I covered the meaningful variety of ways this feature will actually be used? Now that I've defined the required variables, I can use the makeSamples method, which returns an async stream of newly generated samples.\n\nAs I iterate over it, each new sample gets appended to a variable called expandedDataset that I've initialized with the starting dataset. By default, the framework uses the on device model for generation. The on-device model is a great option in most cases, but you might want to bring your own model, or customize the instructions the model operates under.\n\nThe framework provides the flexibility to define your own configurations for sample generation. Lets go over how to do that! For more complex configurations beyond the prompt, dataset, and target count, the framework provides the SampleGenerator Which gives you full control over the generation process. Let's go over some of these configurations! The sessionProvider is a closure that returns a LanguageModelSession. This is where you control which model drives generation and what system-level instructions frame the task.\n\nFor our synthetic data generation, I'll use the PrivateCloudComputeLanguageModel since the context size is larger and then I'll add custom instructions to focus generation on specific books, genres and moods.\n\nI also specify a list of rules for expectations on the samples generated. I'll go over these later. Just a note about how the session is used. The framework handles batch size automatically which is the number of samples processed during generation.\n\nThe generator calls your sessionProvider once at the start of a run and then reuses that session across batches which helps the model maintain context as generation progresses.\n\nBut a session has a limit for how large it can grow. The one exception is if you're making a lot of requests, giving it a large prompt, or getting large outputs, You can exhaust the session's context window mid-run which will throw an error. In that case, the generator calls sessionProvider again to get a fresh one to continue generation but this won't contain context from the previous session. So make sure your instructions in your sessionProvider is self-contained and doesn't assume it'll only be called once.\n\nTo learn more ways to mitigate against context size limits, watch the video \"Build agentic app experiences with Foundation Models\".\n\nNow with the custom session provider, you can also use the SampleGenerator to customize samplingStrategy, which controls how the generator selects examples from your initial dataset to show the model as in-context examples. There are two types of sampling strategies you can specify, the first one is random sampling.\n\nThis strategy selects a random subset of your initial samples as examples to show the model making sure there are no duplicates. This keeps the output varied without requiring us to think carefully about the order of our initial samples. The second type of sampling strategy you can use is sliding window.\n\nThis strategy steps through your initial samples sequentially, skipping duplicates as it goes. If your dataset has meaningful order, consider using this sliding window strategy.\n\nFor our generator, we'll use the random strategy because our initial samples are not meaningfully ordered. And since it's the default strategy we don't need to explicitly define it here.\n\nSo, now that we've configured the generator with our custom sessionProvider, we can call the .run function, which returns a stream of newly synthesized samples.\n\nAs we iterate through each one, it gets added to our expandeDataset defined earlier.\n\nNow that we've setup our configuration, let's explore how we can ensure our synthetic data is the way we expect it.\n\nThat's where the validator closure comes in hand. The validator lets you define your own logic to accept or reject every generated sample. We've already defined a set of rules in the instructions in the session provider earlier, but that doesn't guarantee the output will actually follow the rules. Let's review them.\n\nThe first rule we defined is that the review must be at least 100 characters long Each review should also cover a wide range of genres, moods, and tones. And the review needs to vary in length.\n\nThe model should also generate between 3 and 8 book tags. And tags must be lowercase. In order to understand what to validate your samples on, we need to consider what we can systematically check based on these rules. Also, the validation closure validates per sample generation in isolation and doesn't have context to the other samples. Reviewing these rules, I can tell that the diversity of reviews will require more judgement beyond a simple validation check and the length of reviews requires assessing across all samples.\n\nFor the other rules, we can assess them systematically using the validation closure.\n\nFor the first rule, we can define a review length validation. Let's take a classic book we all know, \"Frankenstein\" by Mary Shelley, for example. We can check if the generated sample defines a review with at least a length of 100 characters. The model also generates tags for each review. This means we can validate when there are between 3 and 8 tags.\n\nAnd lastly, we can check if the tags are all lowercase.\n\nHere I've already defined these 3 validation metrics in the SampleGenerator to check that the samples meet our expected structure. So where do the results end up? Well, as generation progresses, valid samples are collected in the samples property on the SyntheticGenerator. Any sample that fails these validators gets set aside automatically as invalidSamples. Both are updated in real time throughout the run, so you can access them at any point. Either during iteration to check progress or after the loop completes. You can then use these results directly in your app or save the dataset locally. Now let's review our evaluation with the 13 initial samples. In Xcode 27, we introduced a new Evaluations Report to visualize your results. This is the BookTaggingEvaluation with the 13 initial samples.\n\nAs you can see we got pretty high scores for tag quality evaluating both relevance and usefulness.\n\nI've went ahead and ran the evaluation with our new dataset of 100 samples.\n\nNow, we can compare the two evaluations using the Compare button and we're expecting the scores to drop! And we were correct! The quality scores have dropped. Our tag generation feature looked like it was performing well earlier because we weren't testing it with a comprehensive dataset. By running our evaluation on a larger dataset, a drop in scores could signal many different things. Consider what this signal could suggest. Score changes could be due to problems with our prompt or instructions. You could refine one or both to better capture your needs.\n\nYou could also consider gaps in your intelligence feature. Or you may want to adjust your evaluation to understand what you are actually evaluating on.\n\nAnd lastly, your dataset may still not be representative enough and need to capture more variation. You can continue to increase the dataset or include more edge cases using the synthetic data APIs. These are the core ways to further improve your results.\n\nNow that we have a solid approach for building a robust evaluation dataset using synthetic data, I want to take it one step further.\n\nSo far we've been evaluating our book tagging feature, but what happens when our app becomes more complex and needs to take multiple actions to complete a task like search? That's where tool calling comes in. I'll hand it over to Kyle to show how that works! Thanks Ada! Now let's keep our evaluation driven development going and cover tool evaluations.\n\nSo far, we've been evaluating what the model generates — for our feature that's tags for books.\n\nBut intelligence features often take many behind-the-scenes steps to create their output. They perform multiple actions in your app that each contribute to the results.\n\nTools add structure to model workflows when they're completing a task for people using your app.\n\nYou use them to operate on real data that people use daily.\n\nThey can operate using any custom business logic you define.\n\nThey can call functionality a user can invoke directly or entirely new logic for your intelligence feature, or a combo of both.\n\nHere's the thing. A model might give you a reasonable-sounding answer without ever calling the right tool.\n\nThe final output can look correct while the path to get there isn't right. So let's talk about those challenges and how tool evaluations can help you handle them First, instruction following: you need to tell a model how to use each tool, and the attention you pay to the details matters.\n\nTry following the instructions word-by-word yourself to see if you miss a step.\n\nThen there's tool complexity, they can accept simple instructions or require fine-tuning parameter ranges.\n\nThen there are edge cases. A tool might seem to work well on common inputs, but behave surprisingly on the rare ones.\n\nThat's why we need tool evaluations. They let you verify the how, not just the what.\n\nThe model should call the correct tools, with the correct arguments in the order you expect.\n\nAnd along the way, you'll double check that there weren't any unexpected tool calls in the middle.\n\nLet's take a look at this in practice and build our first tool evaluation. In the BookTracker app, we've added a library assistant. A user can search for a book and instead of just filtering books based on the title and other strings, the model uses our app's custom tools to find relevant books.\n\nThere's a searchBooks tool to find books that might have similar tags. Then there's a getBookDetails tool to extract book metadata, like publication date from the searches.\n\nThen there's the findSimilarBooks tool that performs a semantic search for similar books, so we're chaining together multiple steps, each one a tool call. Here's SearchBooksTool.\n\nIt conforms to the Tool protocol, it has a name the model sees and a description that tells it when this tool is useful.\n\nThe arguments are a Generable struct. Notice these are all optional, the model decides which filters to use based on what the user asked for.\n\nIf you prompt a model with find gothic books, we'd expect it to populate the tag argument.\n\nIf you prompt a model with show me something cheerful, we'd expect to generate a mood search. These are exactly the kinds of decisions we want to evaluate.\n\nOK, so that's a refresher on the tools. Now let's write our first tool evaluation and see how they perform. The main component of a tool evaluation is a trajectory expectation. A session transcript has tool calls among the prompts and responses, A trajectory expectation checks the order and kind of each tool call in a language model session. You can think of a trajectory expectation check like going over the list of decisions you made when planning a route. Cars, bikes, and buses are all tools that have their time and place in getting somewhere, but you can evaluate their utility for each segment in a specific trip.\n\nThe expectation looks for all of the tool calls.\n\nThen for each one, runs it against the expectations you write into your evaluations.\n\nHere's a simple case in code form. Our prompt is \"Find books tagged gothic\". We expect one tool call \"searchBooks\".\n\nThis is a TrajectoryExpectation. It describes the tool calls we expect to see in the model's transcript. The unordered here means we don't care when this tool call happens, just that it happens.\n\nWe can further refine this by adding arguments to the expectation.\n\nHere I'm adding an argument to expect the tag \"gothic\". An exact match isn't always what you want.\n\nIf the prompt is \"Find something cheerful\", the model might pass uplifting, happy, cheerful — any of those are fine.\n\nThe .naturalLanguage matcher checks whether the value matches the intent, not the exact string.\n\nAnd there's a whole set of matchers for different situations — contains, oneOf, pattern, range, and more. Check out the developer documentation for more information. For multistep tasks, order matters.\n\nHere the model must first call \"searchBooks\", then call \"getBookDetails\".\n\nIf an agent tries to get details first, it doesn't have a bookId yet — that's a bug.\n\nTrajectory expectations catch it because we're checking the journey, not just the destination.\n\nSometimes what an agent shouldn't do is just as important.\n\nIf a prompt includes ideas like don't look for similar books, the model should follow instructions.\n\nThe disallowed parameter specifies tools that must not appear in the transcript. If an agent calls \"findSimilarBooks\" anyway — that's a failure. Here's where all of the trajectory expectations come together in the full evaluation.\n\nWe define a dataset of samples, each with a prompt and a trajectory expectation and use ToolCallEvaluator to score them.\n\nThe ToolCallEvaluator combines a LanguageModelSession with the tools, gets a response, and captures the structured transcript.\n\nTool call evaluation results show up in the Xcode assistant alongside the rest of your results, and you can get the whole picture of how your intelligence-based feature behaves. But wait! We can also use the Evaluations APIs to generate synthetic data for your tool evaluations!\n\nOh yes let's do that! Trajectory expectations are generable too. Expanding a dataset for your tool evaluations can be quite complex, and with the Evaluations framework we've made it a lot easier to do just that! Since our Tool Call evaluation leverages ModelSample and TrajectoryExpectation that are generable, we can synthetically generate more samples using Sample generator like before.\n\nI've went ahead and defined a prompt and custom instructions for the sessionProvider. Keep in mind when creating synthetic data for tool evaluations, the model doesn't know what tools you've defined or what order the tools need to be called in.\n\nSo here I've specified the available tools explaining their purpose, any order expectations, and other context the model might need. Then we can define the sampleGenerator and use our existing dataset as our initial samples, and a targetCount of 100.\n\nWe can also specify validation metrics here as well! Here I've made sure there's always an expectation and I've also made sure the synthetic samples include at least one tool. And lastly any tools called are actual tools we've already defined.\n\nAnd that's how you can generate and validate synthetic samples for your tool evaluations! The synthetic data APIs are a powerful way to expand your existing dataset beyond your capabilities! And the more representative your data, the more your scores reflect reality. Alright Kyle, back to you! This is where it all comes together. Earlier we built book tagging evaluation, it checks what the model produces. Tag count, genre coverage, quality scores.\n\nNow we have tool evaluations — they check how the model gets there. The right tools, right arguments and right order.\n\nRun both in the same evaluation suite and you'll have built end-to-end confidence in your feature. Now that we've covered some ways to make your evaluations even more robust, you can start applying these ideas to your apps and evaluation datasets.\n\nTo get started, try making your own synthetic data, evaluate the custom tools in your app and check out the sample app and other articles in the developer documentation.\n\nWow Ada, we've covered a lot today! Yeah, we definitely did! But the real plot twist is what you build with it. No spoilers though! And we hope you enjoyed learning about the Evaluations framework!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "5:16",
+ "title": "Generate synthetic data with makeSamples",
+ "language": "swift",
+ "code": "// Synthetic data\n let prompt = Prompt(\"\"\"\n Generate diverse range of book reviews and corresponding tags.\n Cover a wide range of genres, time periods, cultures, and\n reader personas. Do not repeat books already in the dataset.\n \"\"\")\n \n let dataset = Book.sampleBooks.map { book in\n ModelSample(prompt: book.review, expected: BookTags(tags: book.tags))\n }\n \n let targetCount = 100\n var expandedDataset = dataset\n\n for try await sample in dataset.makeSamples(prompt, targetCount: targetCount) {\n expandedDataset.append(sample)\n print(\"Generated \\(expandedDataset.count) samples so far.\")\n }\n\n 2. Configure a custom SampleGenerator — slides 30–43\n \n // Define your own configuration\n let generator = SampleGenerator>(\n prompt,\n samples: dataset,\n targetCount: targetCount,\n sessionProvider: {\n LanguageModelSession( \n model: PrivateCloudComputeLanguageModel(),\n instructions: \"\"\"\n You are a synthetic data generator for a book-tracking app's evaluation suite.\n Your job is to produce realistic, diverse book entries that will stress-test\n a tagging system.\n\n Rules:\n - Review must be at least 100 characters long.\n - Review should cover a mix of genre, mood/tone, and themes.\n - Reviews should vary in length.\n - Create between 3 and 8 tags.\n - Tags must be lowercase.\n \"\"\" \n )\n }\n )"
+ },
+ {
+ "timestamp": "5:53",
+ "title": "Configure a custom SampleGenerator",
+ "language": "swift",
+ "code": "// Define your own configuration\n let generator = SampleGenerator>(\n prompt,\n samples: dataset,\n targetCount: targetCount,\n sessionProvider: {\n LanguageModelSession( \n model: PrivateCloudComputeLanguageModel(),\n instructions: \"\"\"\n You are a synthetic data generator for a book-tracking app's evaluation suite.\n Your job is to produce realistic, diverse book entries that will stress-test\n a tagging system.\n\n Rules:\n - Review must be at least 100 characters long.\n - Review should cover a mix of genre, mood/tone, and themes.\n - Reviews should vary in length.\n - Create between 3 and 8 tags.\n - Tags must be lowercase.\n \"\"\" \n )\n }\n )"
+ },
+ {
+ "timestamp": "10:37",
+ "title": "Validate generated samples",
+ "language": "swift",
+ "code": "// Define validation metrics\n validator: { sample in\n guard let book = sample.expected else { return false }\n\n // Review must be at least 100 characters\n guard sample.promptDescription.count >= 100 else { return false }\n\n // Must have between 3 and 8 tags\n guard (3...8).contains(book.tags.count) else { return false }\n\n // All tags must be lowercase\n guard book.tags.allSatisfy({ $0 == $0.lowercased() }) else { return false }\n\n return true\n }"
+ },
+ {
+ "timestamp": "10:58",
+ "title": "Access valid and invalid results",
+ "language": "swift",
+ "code": "// Accessing results\n for try await sample in generator.run() {\n // During iteration\n expandedDataset.append(sample)\n }\n\n // After iteration\n let allSamples = await generator.samples\n let invalidSamples = await generator.invalidSamples\n \n print(\"Generated \\(allSamples.count) new samples. Total: \\(expandedDataset.count)\")"
+ },
+ {
+ "timestamp": "15:30",
+ "title": "Define a tool's Generable argument",
+ "language": "swift",
+ "code": "@Generable\n struct SearchBooksArguments {\n @Guide(description: \"A freeform search term to match against titles, reviews, or tags\")\n var query: String?\n \n @Guide(description: \"Filter results to books with this specific tag\")\n var tag: String?\n\n @Guide(description: \"Filter results by mood\")\n var mood: String?\n\n @Guide(description: \"Filter results by genre\")\n var genre: String?\n\n @Guide(description: \"Maximum number of results to return. Defaults to 5.\")\n var limit: Int? \n }"
+ },
+ {
+ "timestamp": "16:37",
+ "title": "A basic trajectory expectation",
+ "language": "swift",
+ "code": "// \"Find books tagged gothic\"\n TrajectoryExpectation(\n unordered: [\n ToolExpectation(\n \"searchBooks\",\n arguments: [\n .exact(argumentName: \"tag\", value: .string(\"gothic\"))\n ]\n )\n ]\n )"
+ },
+ {
+ "timestamp": "17:07",
+ "title": "Match arguments by intent (naturalLanguage)",
+ "language": "swift",
+ "code": "// \"Find something cheerful\"\n TrajectoryExpectation(\n \"searchBooks\",\n arguments: [\n .naturalLanguage(\n argumentName: \"mood\",\n criteria: \"Should relate to uplifting, hopeful, or positive feelings\"\n )\n ]\n )\n Other matchers available: .contains, .oneOf, .pattern, .range, and more."
+ },
+ {
+ "timestamp": "17:34",
+ "title": "Expect tool calls in order",
+ "language": "swift",
+ "code": "// \"Find gothic books and show details on the first\"\n TrajectoryExpectation(\n ordered: [\n ToolExpectation(\n \"searchBooks\",\n arguments: [\n .exact(argumentName: \"tag\", value: .string(\"gothic\"))\n ]\n ),\n ToolExpectation(\n \"getBookDetails\",\n arguments: [\n .keyOnly(argumentName: \"bookId\")\n ]\n )\n ]\n )"
+ },
+ {
+ "timestamp": "17:55",
+ "title": "Disallow specific tool calls",
+ "language": "swift",
+ "code": "// \"Show only sci-fi books. Don't look for similar ones.\"\n TrajectoryExpectation(\n unordered: [\n ToolExpectation(\n \"searchBooks\",\n arguments: [\n .naturalLanguage(\n argumentName: \"genre\",\n criteria: \"Should refer to science fiction\")\n ]\n )\n ],\n disallowed: [\n ToolExpectation(\"findSimilarBooks\")\n ]\n )"
+ },
+ {
+ "timestamp": "18:14",
+ "title": "Build a tool call evaluation",
+ "language": "swift",
+ "code": "// Tool call evaluations\n let samples = SampleArrayLoader(samples: [\n ModelSample(\n prompt: \"Find all the books tagged with 'gothic'.\",\n instructions: \"Help the user explore their book collection.\",\n expectations: TrajectoryExpectation( )\n )\n ])\n\n struct BookLibraryToolCallEval: Evaluation {\n var dataset = samples\n\n let pass = Metric(\"All Passed\")\n let percent = Metric(\"Percentage Passed\")\n\n var evaluators: Evaluators { \n ToolCallEvaluator(allPass: pass, percentagePass: percent)\n }\n }"
+ },
+ {
+ "timestamp": "19:20",
+ "title": "Synthesize tool-evaluation samples",
+ "language": "swift",
+ "code": "// Tool call evaluations\n let prompt = Prompt(\"\"\"\n Generate diverse user queries for a personal book library assistant.\n Each sample needs a prompt (what the user says), and a trajectory\n expectation describing which tools should be called and in what order.\n \"\"\")\n\n let instructions = \"\"\"\n AVAILABLE TOOLS:\n - searchBooks(query?, tag?, mood?, genre?, limit?): search the library\n - getBookDetails(bookId): full details for one book\n - findSimilarBooks(bookId, maxResults?): find books sharing tags\n ORDER REQUIREMENTS:\n - searchBooks must comes before getBookDetails or findSimilarBooks\n - Use TrajectoryExpectation(ordered:) when sequence matters, else (unordered:)\n USE THESE ARGUMENT MATCHERS:\n - .exact for precise values, .naturalLanguage for fuzzy matching\n - .keyOnly when any value is acceptable, .range for numeric constraints\n - .contains/.hasPrefix/.hasSuffix for partial string matching\n \"\"\""
+ },
+ {
+ "timestamp": "19:51",
+ "title": "Validate tool-evaluation samples",
+ "language": "swift",
+ "code": "// Tool call evaluations\n validator: { sample in\n // Must have expectations defined\n guard sample.output.expectations != nil else { return false }\n\n let expectations = sample.output.expectations!\n\n // Must reference at least one tool\n let totalExpectations = expectations.ordered.count + expectations.unordered.count\n guard totalExpectations > 0 else { return false }\n\n // All tool names must be from the valid set\n let validTools: Set = [\"searchBooks\", \"getBookDetails\", \"findSimilarBooks\"]\n let allExpectations = expectations.ordered + expectations.unordered + expectations.disallowed\n for expectation in allExpectations {\n guard validTools.contains(expectation.name) else { return false }\n }\n \n return true\n }\n\n ---"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Book Tracker: Using Evaluations to evaluate an intelligent feature",
+ "url": "https://developer.apple.com/documentation/Evaluations/book-tracker-using-evaluations-to-evaluate-an-intelligent-feature"
+ },
+ {
+ "title": "Generating synthetic datasets",
+ "url": "https://developer.apple.com/documentation/Evaluations/generating-synthetic-evaluation-datasets"
+ },
+ {
+ "title": "Evaluating tool-calling behavior",
+ "url": "https://developer.apple.com/documentation/Evaluations/evaluating-tool-calling-behavior"
+ },
+ {
+ "title": "Scoring with model-as-judge evaluators",
+ "url": "https://developer.apple.com/documentation/Evaluations/scoring-with-model-as-judge-evaluators"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/299/4/ef9fbc06-fc78-4896-9848-0f0fe2e75fb9/downloads/wwdc2026-299_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/299/4/ef9fbc06-fc78-4896-9848-0f0fe2e75fb9/downloads/wwdc2026-299_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:19.721Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-303.json b/data/wwdc/videos/2026-303.json
new file mode 100644
index 0000000..9ebe325
--- /dev/null
+++ b/data/wwdc/videos/2026-303.json
@@ -0,0 +1,101 @@
+{
+ "id": "303",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/303/",
+ "title": "Build a responsive camera app that launches quickly",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Audio & Video",
+ "Photos & Camera"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hello, my name is Jake. I'm an engineer on the camera performance team. Welcome to build a responsive camera app that launches quickly. When launch is slow, people notice. From years of optimizing the native camera app, I've learned that the single most important factor in making a camera launch feel fast is how quickly the preview frame appears on the display.\n\nI want to capture a cool shot of my dominoes, but I forgot to launch my camera before the dominoes were already falling. I've placed a red domino in the middle, so it's essential that I launch and capture the moment just before the red domino falls As the app launches, there's a period of blank preview. By the time preview begins rendering, I've already missed the red domino tumbling over.\n\nHaving the preview render shortly after the app has launched allows customers to capture a quick action shot, ensuring they don't miss the moment. I'll help you build a camera app designed for performance. In this video, I'll talk about four main topics to enhance performance. First, I'll discuss how to accelerate a camera launch experience. So preview is up and running without a hitch.\n\nThen, I'll talk about best practices for rendering preview, so no frames are dropped. Third, I'll touch on APIs that help sustain performance, even in challenging environments. Lastly, I'll introduce a new API, designed to offer deterministic file write performance for high data rate video captures. I'll start with Fast Launch.\n\nThere are four stages of a camera app launch sequence. First, the app launches.\n\nThis covers the time for the linker to load the binary, run static initializers, and create UI scenes, plus anything else the app does before creating a capture session. Second, the session is configured and started. Initializing the capture session, committing the configuration, and starting the session all take time and system resources.\n\nThird, once the session is started, all AV capture output objects initialize. This time varies with the number of outputs and their quality settings.\n\nFinally, preview begins streaming and frames start flowing to the app. I'll walk through specific optimizations for each of these stages. The app's UI plays an important role in the camera launch experience. When designing a launch flow, split the work into two phases. Resources critical for launching and displaying preview, and resources that can be created after preview is running.\n\nTake AVCam, for example, the classic sample camera app for AV Foundation. There are several UI elements. A camera preview, a shutter button, an image well, and a mode picker. The camera preview is the most critical UI element for someone the moment they launch the app Because this is what makes the camera feel like it's ready to use. The image well and the mode picker aren't needed before preview renders, so this work should wait until after preview starts.\n\nUI elements aren't the only factor in launch time. Any resource created before preview is rendered will influence launch time.\n\nApplying these two phases to AVCAM, I create the shutter button in preview on launch, but fade in all other UI elements after launch finishes. Now that the app's impact to launch is reduced, I'll focus on how AV capture session and its related objects impact the next stage, session configuration.\n\nConfiguring and starting AV capture session takes a lot of system resources and allocations that directly impact App Launch.\n\nA typical AV capture session consists of an AV capture device input, usually the camera or microphone. AV capture connection wires the capture device to the outputs. In this example, I want two outputs, one for preview and one for capture. The AV Capture Video Preview layer is the output for displaying the preview.\n\nWhile the AV capture photo output serves as the output for image captures. These objects together power an app's camera experience.\n\nBecause AV Capture Session coordinates all the capture objects, I want to create it first, as soon as the main thread finishes UI setup.\n\nCreating AV capture session blocks the main thread. To avoid a hang, create it in parallel with UI initialization.\n\nWhen displaying preview on launch, dispatch AV capture session creation off the main thread. This allows the session setup to run in the background while the apps UI scene is being created.\n\nCommitting multiple configurations extends launch time. Commit a single configuration up front to avoid lengthy reconfigurations during launch. Start running and stop running on A B capture session are blocking calls. Don't call them on the main thread, or the app will hang. Next, I'll cover the most expensive part of camera launch. Initializing AV capture outputs. Initializing AV capture outputs noticeably slows down launch.\n\nTo render preview, the app only needs a preview layer or one output initialized. Outputs like the movie file output and photo output aren't needed for preview. To reduce time spent initializing outputs, adopt the Deferred Start API, available in iOS 26 and later.\n\nDeferred start lets apps put off output initialization until launch has finished. In this launch sequence, all AV capture outputs initialize before the first preview frame renders.\n\nThe idea with deferred start is to postpone any output that isn't needed for launch until after preview has started. With Deferred Start, the launch sequence changes. The app launches, configures the session, and starts it. Now only the preview output initializes before the first frame displays. The system then either runs the deferred initialization automatically when conditions allow, or waits for the app to signal when it's a good time.\n\nEvery AVCapture output, an AV Capture Video Preview layer, has an isdeferred start enabled property. Set it to true to defer that output. To optimize for launch, defer all outputs except the output used to render preview.\n\nThere are two ways to specify when deferred start runs. Automatic start and manual start.\n\nApps recompiled against the iOS 26 in later SDKs use automatic mode by default.\n\nThe automatically runs deferred start property is set to true when in this mode. In automatic mode, the system picks the best time to initialize the deferred outputs.\n\nThis happens shortly after preview appears on the device.\n\nThe session sends two delegate callbacks so the app knows when deferred start begins and ends.\n\nSession will run deferred start fires before output initialization begins, and session did run deferred start fires after it completes.\n\nNow, I'll show how to adopt this.\n\nFirst, I'll create a class that handles the delegate callbacks from the deferred start API.\n\nSession Will Run Deferred Start is called Before Deferred Start Begins. This is a good place to create any background resources the app needs.\n\nSession Did Run Deferred Start is called After Deferred Start completes. At this point, all capture outputs are initialized and ready to use. Now, I'll add deferred start to the capture session. During configuration, set automatically runs deferred start to true on AV capture session Remember, if your app recompiled against iOS 26 and later, this is automatically set to true for you. Next, enable deferred start on every output that isn't required for launch. Here, I defer the photo capture output and use the video preview layer to render preview. Then, I'll attach the delegate callback class from earlier to the capture session. The session is now configured, so I'll commit the configuration and call start running.\n\nFor apps that want finer control, the Deferred Start API also offers a manual mode with Run Deferred Start when needed.\n\nIn manual mode, the app tells the system when to begin deferred start. This is useful for apps that want to read preferences or set up UI before the heavy initialization begins. Or for apps using video data output to render preview, which I'll discuss in more detail later in this video. With manual mode, the sequence changes.\n\nOnce the app finishes startup work, such as creating non-critical resources, call run deferred start when needed on the capture session.\n\nThis tells the system it could run deferred start. To opt into manual mode, set automatically runs deferred start on AV capture session to false.\n\nIn this example, I want to render preview myself, using AVCapture video data output. So I'll disable deferred start on this output. I'll leave the rest of the code the same as the previous example.\n\nNext, I need to decide when to run deferred start on the deferred output. To do that, I'll track whether the first frame has been presented Here, I'm using a CA meta layer. Once the first frame is presented, I'll set up any non-critical UI elements until AV capture session to run deferred start on the postponed outputs. After the first frame is presented, no special handling is needed.\n\nTo verify the launch is faster with Deferred Start, I set up a lightboard in the lab. My goal is to compare the difference in position of the LED pattern in preview. The phone on the right has deferred start enabled. The one on the left doesn't. I want to capture the pattern when both the red and green LEDs are on screen.\n\nI screenshotted the moment when one device successfully shows preview. The deferred start phone on the right is clearly able to capture that expanding pattern.\n\nBy the time the phone, without deferred start, finishes launching, the green LEDs have nearly faded out, missing that clear separation.\n\nI also timed the launch sequence on both phones. Without deferred start, the app launch was close to a second. With deferred start? Launch is cut in half. That's a two times faster launch. This is a massive step forward in launch times. Preview is up and running faster than ever. For complex capture sessions, apps may see an even bigger improvement.\n\nDeferring AV capture photo output does have a catch. Preview starts much sooner, but the time to the first capture stays the same.\n\nBecause the photo output is deferred, the system has to finish initializing it before a capture can begin. Preview is up quickly, but someone can still miss the shot. To solve this problem, set is responsive capture enabled to true on AV capture photo output This property adds buffering between starting a capture and when processing begins, so people can capture the moment, even if the photo output isn't fully ready yet. The green phone enables responsive capture in conjunction with deferred start. As the dominoes fall, I quickly launch and take a picture.\n\nThe green phone allowed me to get a perfect shot of the dominoes, while the purple phone missed the moment.\n\nTo learn more about how to use responsive capture and how to capture stunning, high-resolution images, watch Implement High Resolution Photo Capture from WWDC26.\n\nOnce preview is running, keeping a steady framerate and cadence is essential. Otherwise, the camera feels laggy. Next, I'll share best practices for rendering preview.\n\nRevisiting the session architecture from earlier, the easiest way to render preview is with AV Capture Video Preview Layer. It shows exactly what the camera sees directly in the app's UI. AV Capture Video Preview Layer is optimized for rendering preview. No need to process video frames in the app. AV Capture Video Preview Layer does this automatically, handling tricky situations such as HDR tone mapping. It also keeps CPU and GPU overhead low, which saves power and leaves more headroom for the UI.\n\nAnd it's tuned for low latency preview, so the app shows what the camera sees with very little delay.\n\nAs a trade-off for simplicity, AV Capture Video Preview Layer does not allow for per-frame access.\n\nFor apps that want more control over preview rendering, then AV Capture Video Data Output is the better choice. AV Capture Video Data Output takes the place of AV Capture Video Preview Layer in the session architecture and becomes the primary output for displaying frames on the device.\n\nAV Capture Video Data Output gives more control over the flow of preview, enabling apps to process individual frames.\n\nIt also lets the app apply a custom UI overlay on each frame.\n\nAnd per frame processing makes it easier to integrate with Metal and to analyze frame data.\n\nUse AV Capture Video Preview Layer when you just need to show the camera feed. And remember, apps using AV Capture Video Preview Layer are opted into automatic deferred start when recompiled against iOS 26 and later. Use AV capture video data output when per-frame processing is the priority.\n\nDeferred start doesn't apply automatically with AVCapture video data output, so adopt manual deferred start to get the same launch gains. When rendering preview, keep per framework short. This helps avoid frame drops and keeps the experience fluid. As the device heats up, performance gets harder to maintain, because the system throttles to adapt.\n\nMonitor the session's performance and adjust to system conditions for a sustainable experience. Next, I'll cover APIs that let your app monitor performance and adapt to system conditions. Revisiting the architecture from earlier, there's a capture session, a photo output, and a preview layer. This is a fairly basic setup, but it grows in complexity as an app adds more cameras or input devices.\n\nAs complexity grows, so does the performance cost. Understanding the capture session's cost helps you design for a sustainable experience. The hardware cost API returns a value between 0 and 1. It tells you what share of the session's hardware is actively in use.\n\nA value above one means the system can't support the configuration. Several things contribute to this cost.\n\nthe number of cameras used, the active formats of the source devices, such as using 1080p or 4K , The frame rate of the source device's formats? Hardware cost assumes the format's max frame rate. So if you're running at a lower frame rate, like 30 frames per second instead of 60 frames per second Use the framerate override property to reduce the cost.\n\nAnd lastly, the use of binned formats. Binned formats use less hardware bandwidth since these formats group pixels.\n\nThe system pressure cost API also returns a value between 0 and 1. It represents the cost of the session's current configuration. When it goes above 1, the configuration is unsustainable. To adjust to the current system state, monitor the system pressure state property of AV capture device As the system pressure state increases, consider reducing the capture device's frame rate, or throttling any use of the GPU or Apple Neural Engine, or minimizing UI work.\n\nUse the hardware cost in system pressure state API after initial session setup.\n\nAfter committing the configuration, check that the hardware cost doesn't exceed the device's capabilities.\n\nOnce hardware cost is at or below 1, observe AV Capture device's system pressure state and register a handler for state changes.\n\nUse this handler to adapt using the techniques I just covered. Video capture is also sensitive to performance issues once the device enters a pressured state Traditional file system input-output is variable because the system is juggling competing operations, memory fragmentation, and device storage wear.\n\nThis means file input-output behavior is non-deterministic. High data rate video captures, like ProRes , need sustained, high bandwidth input-output to record smoothly without dropping frames. To address this challenge, use AV Pro Video Storage, new in iOS 27. This class tracks and manages pre-allocated storage for high data rate video captures. It's a system-wide resource that all apps share.\n\nAV Pro Video Storage works with the existing movie recording APIs. Applications opt in by setting usespro video storage on AV CaptureMovie file output. or an AV asset writer when using AV capture video data output to record content.\n\nThe system handles allocation and file input output, so write performance stays consistent for high data rate codecs. Camera settings is updated so people can control how much storage to allocate. The remaining capacity method reports how much storage is left. That value decreases during a recording and stops decreasing when the recording stops.\n\nUse the open settings method to take someone from your app to the settings UI.\n\nTo use AV Pro Video Storage, first check that the storage is supported.\n\nAV Pro Video Storage is a singleton, so use the shared method to obtain the instance of this object.\n\nNext, create the Movie File Output, AV Capture Session, AV Capture Connections, and select the format for recording.\n\nUse the new isProVideoStorage supported method on AVCaptureMovie file output to check for compatibility.\n\nBefore recording, confirm the storage is not busy resizing or servicing file creation or deletion requests. Finally, turn on Pro Video Storage on the Movie File Output and start recording. During capture, The recording is written to the pre-allocated storage and then moved to the specified location once the capture finishes.\n\nAs I mentioned before, this feature also works great with AV Asset Writer.\n\nI covered ways to optimize a camera app for launch, best practices for rendering preview, APIs for sustained performance, and how to get deterministic file write speeds for ProRes captures. Adopt deferred start with the quality photo output. You'll keep launch fast and get gorgeous image quality too. Analyze performance in other parts of your camera app. Use instruments and Xcode to measure, identify, and fix performance issues. And remember, most of the time you're developing your app at a desk or in a controlled environment. But people use your app in the real world. Test and measure performance in all conditions, like on a hot sunny day. Lastly, watch Create a More Responsive Camera Experience from WWDC23 and implement high-resolution photo capture from WWDC26 To learn how to integrate capture responsiveness into your app. Performance isn't just a feature, it's the foundation of a great camera experience. Keep optimizing and keep capturing. Thanks for watching.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "9:14",
+ "title": "Automatic deferred start delegate",
+ "language": "swift",
+ "code": "import AVFoundation\n\nclass DeferredStartDelegate: NSObject, AVCaptureSessionDeferredStartDelegate {\n func sessionWillRunDeferredStart(_ session: AVCaptureSession)\n {\n // This is called before deferred start begins for the deferred outputs\n }\n\n func sessionDidRunDeferredStart(_ session: AVCaptureSession)\n {\n // This is called after deferred start completes for all outputs\n }\n}"
+ },
+ {
+ "timestamp": "9:46",
+ "title": "Adopt automatic deferred start",
+ "language": "swift",
+ "code": "import AVFoundation\n\nlet captureSession = AVCaptureSession()\ncaptureSession.beginConfiguration()\ncaptureSession.automaticallyRunsDeferredStart = true\n\nlet videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)\nvideoPreviewLayer.isDeferredStartEnabled = false\n\nlet photoOutput = AVCapturePhotoOutput()\nphotoOutput.isDeferredStartEnabled = true\ncaptureSession.addOutput(photoOutput)\n\ncaptureSession.setDeferredStartDelegate(deferredStartDelegate, deferredStartDelegateCallbackQueue: sessionQueue)\n\ncaptureSession.commitConfiguration()\ncaptureSession.startRunning()"
+ },
+ {
+ "timestamp": "11:30",
+ "title": "Adopt manual deferred start",
+ "language": "swift",
+ "code": "import AVFoundation\n\nlet captureSession = AVCaptureSession()\ncaptureSession.beginConfiguration()\ncaptureSession.automaticallyRunsDeferredStart = false\n\nlet videoOutput = AVCaptureVideoDataOutput()\ncaptureSession.addOutput(videoOutput)\nvideoOutput.isDeferredStartEnabled = false\n\nlet photoOutput = AVCapturePhotoOutput()\nphotoOutput.isDeferredStartEnabled = true\ncaptureSession.addOutput(photoOutput)\n\ncaptureSession.setDeferredStartDelegate(deferredStartDelegate, deferredStartDelegateCallbackQueue: sessionQueue)\n\ncaptureSession.commitConfiguration()\ncaptureSession.startRunning()"
+ },
+ {
+ "timestamp": "11:53",
+ "title": "Manage runDeferredStartWhenNeeded",
+ "language": "swift",
+ "code": "import AVFoundation\nimport QuartzCore\n\nprivate var firstFramePresented = false\nguard let drawable = layer.nextDrawable()\nif (!firstFramePresented) {\n drawable.addPresentedHandler({ drawable in\n // Set up postponed UI elements\n captureSession.runDeferredStartWhenNeeded()\n })\n firstFramePresented = true\n}"
+ },
+ {
+ "timestamp": "14:07",
+ "title": "Enable responsive capture",
+ "language": "swift",
+ "code": "import AVFoundation\n\nfunc configurePhotoOutput(for session: AVCaptureSession, device: AVCaptureDevice) {\n let photoOutput = AVCapturePhotoOutput()\n\n guard session.canAddOutput(photoOutput) else { return }\n session.addOutput(photoOutput)\n\n photoOutput.maxPhotoQualityPrioritization = .quality\n // Responsive capture lets the photo output capture immediately\n photoOutput.isResponsiveCaptureEnabled = photoOutput.isResponsiveCaptureSupported\n}"
+ },
+ {
+ "timestamp": "20:16",
+ "title": "Monitor for system pressure",
+ "language": "swift",
+ "code": "import AVFoundation\n\nlet captureSession = AVCaptureSession()\nlet device = activeVideoInput?.device\ncaptureSession.beginConfiguration()\n// ...\ncaptureSession.commitConfiguration()\n\nguard captureSession.hardwareCost <= 1.0 else {\n print(\"hardwareCost \\(captureSession.hardwareCost) — cannot start session. Reconfiguring.\")\n setupLowCostConfiguration()\n}\n\ncaptureSession.startRunning()\nlet systemPressureObserver = device?.observe(\\.systemPressureState,\n options: [.initial, .new],\n changeHandler: { /* Handle state change */ })"
+ },
+ {
+ "timestamp": "22:17",
+ "title": "Manage pro video storage",
+ "language": "swift",
+ "code": "import AVFoundation\n\nfunc configureProVideoStorage() {\n guard AVProVideoStorage.isSupported else { return }\n let storage = AVProVideoStorage.shared\n guard storage.remainingCapacity != 0 else {\n storage.openSettings()\n return\n }\n}"
+ },
+ {
+ "timestamp": "22:43",
+ "title": "Adopt AVProVideoStorage for deterministic file write speeds",
+ "language": "swift",
+ "code": "import AVFoundation\n\nguard AVProVideoStorage.isSupported else { return }\nguard let pvs = AVProVideoStorage.shared else { return }\n\n// Configure and set up AVCaptureSession, AVCaptureConnections and format\n// ...\nlet movieOutput = AVCaptureMovieFileOutput()\n\nguard movieOutput.isProVideoStorageSupported else { return }\nguard !pvs.isBusy else { return }\n\nlet movieFileURL = FileManager.default.temporaryDirectory\n .appendingPathComponent(UUID().uuidString)\n .appendingPathExtension(\"mov\")\n\nmovieOutput.usesProVideoStorage = true // Also available with AVAssetWriter\nmovieOutput.startRecording(to: movieFileURL, recordingDelegate: delegate)"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Build a responsive camera app that launches quickly",
+ "url": "https://developer.apple.com/documentation/AVFoundation/build-a-responsive-camera-app-that-launches-quickly"
+ },
+ {
+ "title": "Performance and metrics",
+ "url": "https://developer.apple.com/documentation/Xcode/performance-and-metrics"
+ },
+ {
+ "title": "AVCam: Building a camera app",
+ "url": "https://developer.apple.com/documentation/AVFoundation/avcam-building-a-camera-app"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/303/5/fb6dc55a-c026-4ce1-9902-7a744fef4c99/downloads/wwdc2026-303_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/303/5/fb6dc55a-c026-4ce1-9902-7a744fef4c99/downloads/wwdc2026-303_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "304",
+ "year": "2026",
+ "title": "Implement high resolution photo capture",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/304"
+ },
+ {
+ "id": "10105",
+ "year": "2023",
+ "title": "Create a more responsive camera experience",
+ "url": "https://developer.apple.com/videos/play/wwdc2023/10105"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:19.852Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-304.json b/data/wwdc/videos/2026-304.json
new file mode 100644
index 0000000..5b0413b
--- /dev/null
+++ b/data/wwdc/videos/2026-304.json
@@ -0,0 +1,96 @@
+{
+ "id": "304",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/304/",
+ "title": "Implement high resolution photo capture",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I am Mohit Setia - I'm an engineer on the Camera Software team. Welcome to: \"Implement high resolution photo capture.\" Advancements in camera hardware and computational photography make high-resolution captures possible. But taking full advantage of that quality means managing tradeoffs particularly between processing time and final image quality. I'll go over high-quality photo processing and how to manage those tradeoffs for your app. First, I will cover different types of high-resolution photos.\n\nThen, how to configure and capture them.\n\nAnd finally, I'll discuss how to keep your app responsive throughout the process.\n\nFirst up: high-resolution photos. The preview stream gives you a screen-resolution image, suitable for showing the camera's view. But in photography use cases higher level of detail and low noise is expected.\n\nAlso, high resolution image is required for photo cropping, zooming in on details, or image analysis.\n\nHigh resolution captures address this. Let me define what they are. A high-resolution photo — typically 12 megapixels captures the scene at a higher resolution than the preview stream.\n\n24 megapixels and 48 megapixels takes it a step further. The camera captures it at an even larger resolution. That means significantly more detail and clarity.\n\nStarting with iPhone 14 Pro and iPhone 14 Pro Max, the camera system features of 48 megapixel Quad sensor.\n\nA quad-pixel sensor design allows capturing photos at full resolution with the highest level of details or group pixels in two by two clusters of the same color to capture more light.\n\nThis is a 48 megapixel photo captured on an iPhone! It shows a sweeping coastal landscape cliffs, beach, and ocean stretching to the horizon.\n\nIt is four times the resolution of a standard photo, and it is captured using a single frame from the sensor.\n\nNotice the fine rocks on 100% crop of the coast.\n\nClearly visible in incredible detail. And the intricate patterns on the side of the mountain.\n\nThis is the power of 48 megapixels: more detail, more clarity, more to explore.\n\nStarting with iPhone 15, the camera supports 24 megapixel photos. A 24 megapixel photo capture consists of multiple steps. First, the camera uses combined pixels on the Quad sensor to generate a 12 megapixel, multi-frame fused, high dynamic range image.\n\nThen, the computational image processing pipeline, called photonic engine, combines that with a high resolution 48MP image captured at a full sensor resolution for rich details.\n\nResulting in a 24MP image with incredible quality double the resolution of a 12MP image with only about 50% increase in the file size.\n\nIt balances light and detail while keeping file size manageable for storing and sharing.\n\nStarting with iPhone 15, the Camera app defaults to this capture mode. 24MP and 48MP capture support has been extended to the telephoto camera on iPhone 16 Pro, and the ultra wide camera on iPhone 17. So these are the types of high resolution photos available across iPhone cameras. Now that I've defined the high-resolution photos available on iPhone, here are the types of high resolution captures that you can request in your app. First, the most common case — the fully processed photo.\n\nThis is a multi-frame fused image. It goes through Photonic Engine, that extends dynamic range and improves fine details.\n\nSecond, Exposure brackets multiple exposure frames of the same scene. This is useful for creating high dynamic range photo capture and scenarios where a selection from multiple exposures is required.\n\nThird, Bayer RAW photos. RAW gives you minimally processed data straight from the sensor. This is ideal for post-processing and editing use cases. And fourth, Apple's ProRAW. ProRAW combines the flexibility of RAW with iPhone's image processing.\n\nIt gives more flexibility when editing exposure, color, and detail. To learn more about RAW format, watch \"Capture and process ProRAW images\" from WWDC 2021. Next, let me share how to configure the capture session for high-resolution photo. I'll start by creating an AVCaptureSession and then configure the session.\n\nNote - only the photo preset supports 24 and 48 megapixel photos. Other presets do not support it.\n\nNext, I will pick the right prioritization for my app.\n\nA speed capture delivers fastest, with the least processing. A balanced capture gives medium delivery speed and good quality. A quality capture takes the longest, but delivers the best quality.\n\nThis is covered in more detail in \"Capture High-Quality Photos Using Video Formats.\" from WWDC 2021. So while configuring the AVCapturePhotoOutput. I'll set maxPhotoQualityPrioritization to quality for demonstration of a high res capture.\n\nThis tells the session to prepare resources for all three prioritization levels speed, balanced, and quality.\n\nThe availability of some high resolution depends on this setting and I'll cover that little later in this video. Next, I will select the largest dimensions that I would request in this session.\n\nStarting from iOS 16, you can check the supported max photo dimensions on the active format of the device.\n\nsupportedMaxPhotoDimensions lists all possible photo dimensions on the current format. Here, I'm selecting the largest available dimensions for demonstration but you should pick the dimensions that fit your use case.\n\nComplete photo output configuration before committing the session configuration.\n\nChanging these settings after commit triggers a lengthy pipeline reconfiguration.\n\nWhen ready to capture, set maxPhotoDimensions and photoQualityPrioritization on AVCapturePhotoSettings for the current capture.\n\nAnd then set up the delegate to capture the photo.\n\nSetting maxPhotoDimensions is a request, not a guarantee.\n\nThe system looks at light level, scene, and available processing, and picks the best path it can. The actual dimensions come back to your delegate inside AVCaptureResolvedSettings, which also notifies the delegate when the capture is complete. You can customize quality prioritization and maxPhotoDimensions on each capture. That means you can support multiple quality levels and dimensions in the same session no lengthy reconfiguration between captures.\n\nHigh resolution captures require specific resource allocations based on photoQualityPrioritization and maxPhotoDimensions.\n\nIf the system has not preallocated these resources, the allocation happens at capture time - which can slow things down.\n\nTo avoid this slow down, use the method setPreparedPhotoSettingsArray on AVCapturePhotoOutput to signal how you plan to capture future photos.\n\nFor example, if the app features of 48 megapixel mode, call setPreparedPhotoSettingsArray as soon as that mode is activated so resources are ready before the capture happens. Here is how you can implement this. As early as possible, create a prepareSettings object.\n\nSet the appropriate maxPhotoDimensions and photoQualityPrioritization.\n\nThen call setPreparedPhotoSettingsArray.\n\nLater, when ready to capture, create a new captureSettings object that matches the prepareSettings configuration.\n\nKeep in mind, you cannot reuse the prepareSettings object for the actual capture.\n\nCreate a new settings object, but make sure its configuration matches, so the capture aligns with the preallocated resources. Capturing high resolution photos requires processing of a large number of pixels.\n\nThis processing can take several seconds.\n\nI'll now provide some best practices to ensure that the app remains fast and responsive during this process. When a photo is requested, it first goes through a capture stage then moves to a processing stage. AVCapturePhotoCaptureDelegate receives notifications throughout this process. For example, didCapturePhotoFor resolvedSettings and didFinishCaptureFor resolvedSettings keep the app up to date as processing continues. Processing can take a different amount of time based on selected photo quality prioritization. A photo with quality prioritization is delivered with the best image by utilizing longer processing time. A photo with Balance prioritization delivers optimal quality with appropriate processing time for common cases. A photo with Speed prioritization is the fastest way to get a photo capture but without the improved quality that comes with longer processing.\n\nYou can capture 12 megapixel across all three prioritization levels.\n\n48 megapixel images, because they're only a single frame as covered earlier, are available with either balanced, or quality prioritization.\n\n18 megapixel and 24 megapixel are multi-frame fused images and take longer to process that only quality prioritization allows. The 18 megapixel in this table, is only available on the Center stage front camera on iPhone 17. To learn more about the Center stage front camera Check out the \"Support Center Stage front camera in your iOS app\" from WWDC 2026. AVCaptureResolvedSettings has a photoProcessingTimeRange property. It tells you how long to expect before the photo is delivered to your delegate.\n\nThe next photo can only be captured once the previous photo finishes processing. This delay between the two captures is referred to as shot to shot delay.\n\nEnabling responsive capture on AVCapturePhotoOutput allows overlapping captures.\n\nA new capture can begin once the capture stage of the previous photo finishes, no need to wait for processing. Observe the captureReadiness property on AVCapturePhotoOutput to know when the next photo can be captured. This reduces the shot to shot delay for the second photo. It helps you avoid missing the moment.\n\nBut keep in mind, each photo still takes the same processing time as before. To reduce shot-to-shot delay even further on high-quality captures, adopt deferred photo processing. When you enable deferred processing, the system delivers a lightly processed proxy photo immediately after capture. You receive it via the didFinishCapturingDeferredPhotoProxy delegate callback. Final photo processing happens in one of two ways. On demand, when you request the final photo through the photo library, or in the background, when the system decides that the conditions are favorable, such as the device being idle.\n\nWatch \"Create a More Responsive Camera Experience\" from WWDC23 for deep dive into deferred photo processing. Enabling deferred photo processing significantly improves responsiveness by reducing the processing time blocking the next capture.\n\nThis further reduces the shot to shot delay for all the subsequent photos.\n\nYou can now capture more high quality photos in less time.\n\nWith this approach, time spent on the processing stage shrinks significantly while the capture stage remains the same.\n\nDeferred processing lets the system take longer to finish processing a photo without blocking the next capture.\n\nAnd because that work happens in the background, it doesn't share memory with the capture session.\n\nThat's what makes multi-frame fusion captures possible, like 18 and 24 megapixel.\n\nTo prioritize responsiveness even further, turn on the fast capture prioritization property on AVCapturePhotoOutput. When you turn on fast capture prioritization on photoOutput, the system detects when someone takes multiple captures in quick succession.\n\nIt then adapts the photo quality from the highest quality setting to a balanced quality setting.\n\nSo in the photo capture timeline, as someone requests rapid captures, the system dynamically adjusts photo quality prioritization to balanced. Balanced captures require less time for both capture and processing.\n\nThe result is a speed boost when rapid captures are happening.\n\nStarting with iOS 27 on iPhone 16 and iPhone 17, the system also processes balanced fast captures later using deferred processing.\n\nThis further minimizes processing time, giving a faster capture experience for much longer. To illustrate this, I am out covering a very intense ball game between camera team members. On screen, a basketball court where the action unfolds. At first I start, without Deferred Processing, Responsive shutter or Fast capture prioritization.\n\nNotice the capture button as I try to take photos of the basketball shot.\n\nIt keeps spinning while processing the photo, preventing me from taking another shot. I end up with a single photo of the shot. But I can do better to capture the moment I want, using the APIs, to manage responsiveness.\n\nI enable Deferred Processing, Responsive Shutter and Fast capture for the next shot.\n\nThis time the capture button stays responsive while I capture the best moment.\n\nAs I press the capture button, AVCaptureSession starts capturing Quality photos. But as I continue, it detects fast captures and intelligently transitions into Balanced captures.\n\nThe difference is noticeable: one blocked capture versus five responsive shots of the same moment.\n\nThis made for a much more responsive capture experience. In this video, I shared best practices for building a responsive photo capture app that captures photos with highest quality and resolution.\n\nIf you are building a photo capture app, identify the resolution you want. Higher resolutions like 24 and 48 megapixels give people more detail to crop, zoom, and explore but they need more memory and processing time.\n\nPick the quality prioritization that fits your needs. Speed, balanced, and quality each trade off delivery time against image fidelity. Match the level to what your app demands. And if you need the highest quality captures, turn on deferred processing and responsive captures. Without them, each photo blocks the next and people using your app might miss moments they can't get back. To further optimize your camera app, watch \"Build a responsive camera app that launches quickly\" from WWDC 2026.\n\nBuild a camera experience that's fast when it needs to be. And uncompromising on quality when it matters. Thanks for watching.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "5:26",
+ "title": "Configure the AVCaptureSession",
+ "language": "swift",
+ "code": "import AVFoundation\n\nprivate let session = AVCaptureSession()\nprivate func configureSession() {\n session.beginConfiguration()\n session.sessionPreset = .photo\n}"
+ },
+ {
+ "timestamp": "6:11",
+ "title": "Configure AVCapturePhotoOutput",
+ "language": "swift",
+ "code": "import AVFoundation\n\nprivate let photoOutput = AVCapturePhotoOutput()\nprivate let configurePhotoOutput: () -> Void = {\n photoOutput.maxPhotoQualityPrioritization = .quality // or .balanced\n}"
+ },
+ {
+ "timestamp": "6:38",
+ "title": "Add maxPhotoDimensions to AVCapturePhotoOutput",
+ "language": "swift",
+ "code": "import AVFoundation\n\nlet supportedMaxPhotoDimensions = device?.activeFormat.supportedMaxPhotoDimensions ?? []\nif let largestDimension = supportedMaxPhotoDimensions.max(by: { lhs, rhs in\n Int(lhs.width) * Int(lhs.height) < Int(rhs.width) * Int(rhs.height)\n} ) {\n photoOutput?.maxPhotoDimensions = largestDimension\n}\n\nsession?.commitConfiguration()\nsession?.startRunning()"
+ },
+ {
+ "timestamp": "7:21",
+ "title": "Update AVCapturePhotoSettings",
+ "language": "swift",
+ "code": "import AVFoundation\n\nlet settings = AVCapturePhotoSettings()\nsettings.maxPhotoDimensions = dimension.cmVideoDimensionsValue\nsettings.photoQualityPrioritization = .quality\n\nvar delegate: AVCapturePhotoCaptureDelegate?\n\n// Configure photo request delegate\n\nif let delegate {\n photoOutput?.capturePhoto(with: settings, delegate: delegate)\n}"
+ },
+ {
+ "timestamp": "8:59",
+ "title": "Prepare resources for the capture",
+ "language": "swift",
+ "code": "import AVFoundation\n\nlet prepareSettings = AVCapturePhotoSettings()\nprepareSettings.maxPhotoDimensions = photoOutput.maxPhotoDimensions\nprepareSettings.photoQualityPrioritization = .quality\n\nphotoOutput.setPreparedPhotoSettingsArray([prepareSettings]) { prepared, error in\n if let error = error {\n print(\"Failed to prepare: \\(error)\")\n return\n }\n print(\"Pipeline prepared: \\(prepared)\")\n}\n\n// Later, when ready to capture — create NEW settings\nlet captureSettings = AVCapturePhotoSettings()\ncaptureSettings.maxPhotoDimensions = photoOutput.maxPhotoDimensions\ncaptureSettings.photoQualityPrioritization = quality\nphotoOutput.capturePhoto(with: captureSettings, delegate: self)"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Capturing photos in RAW and Apple ProRAW formats",
+ "url": "https://developer.apple.com/documentation/AVFoundation/capturing-photos-in-raw-and-apple-proraw-formats"
+ },
+ {
+ "title": "AVCam: Building a camera app",
+ "url": "https://developer.apple.com/documentation/AVFoundation/avcam-building-a-camera-app"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/304/4/7a18d6ee-a63d-4402-bfb6-85a21dfac7dd/downloads/wwdc2026-304_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/304/4/7a18d6ee-a63d-4402-bfb6-85a21dfac7dd/downloads/wwdc2026-304_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "303",
+ "year": "2026",
+ "title": "Build a responsive camera app that launches quickly",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/303"
+ },
+ {
+ "id": "341",
+ "year": "2026",
+ "title": "Support the Center Stage front camera in your iOS app",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/341"
+ },
+ {
+ "id": "10105",
+ "year": "2023",
+ "title": "Create a more responsive camera experience",
+ "url": "https://developer.apple.com/videos/play/wwdc2023/10105"
+ },
+ {
+ "id": "10160",
+ "year": "2021",
+ "title": "Capture and process ProRAW images",
+ "url": "https://developer.apple.com/videos/play/wwdc2021/10160"
+ },
+ {
+ "id": "10247",
+ "year": "2021",
+ "title": "Capture high-quality photos using video formats",
+ "url": "https://developer.apple.com/videos/play/wwdc2021/10247"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:19.963Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-305.json b/data/wwdc/videos/2026-305.json
new file mode 100644
index 0000000..0ccc995
--- /dev/null
+++ b/data/wwdc/videos/2026-305.json
@@ -0,0 +1,62 @@
+{
+ "id": "305",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/305/",
+ "title": "Enhance RAW image processing with Core Image",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Welcome everyone. My name is David Hayward and I am excited to talk about enhancements to Core Image, and its support for RAW image files.\n\nI will talk about four main topics. First, I will review how RAW image files from cameras, are supported by Core Image on Apple platforms. And show you significant RAW quality improvements that are coming in iOS, iPadOS, macOS, and visionOS 27.\n\nAfter that, I will discuss how to get optimal performance when rendering RAWs.\n\nAnd lastly, I will describe features, that Apple added to the CIImageProcessor class for RAW.\n\nTo start, here is a quick summary of how Core Image supports RAW. RAW files come from a diverse set of camera makes and models, but unlike HEIFs and JPEGs, they need special handling before they can be displayed.\n\nThe first step, is to parse the file's metadata, and unpack the RAW sensor values. At this stage, each pixel location only has a red, green, or blue value arranged in a mosaic pattern. This image is cropped way in, so that this pattern is visible.\n\nThe next step, is to demosaic the sensor data, so that each pixel contains red, green, and blue values.\n\nThe following stage is to denoise the pixels, so that the image is free of photon noise, read noise and thermal noise. After this, convolutions are applied to sharpen edges and add local contrast.\n\nThe last major step is to adjust white balance, exposure, color and tone to make a pleasing final image.\n\nThe algorithms for all these steps are built in to iOS, iPadOS, macOS, and visionOS.\n\nThis means that people can view RAW files in system apps like Finder, Preview and Freeform.\n\nIn fact, any app or framework that uses the Image IO API gets RAW support automatically.\n\nAnd beyond basic viewing, apps that use the CIRAWFilter API, can present advanced editing controls.\n\nThis API is used by apps like Photos, Pixelmator Pro, and others like Nitro, Acorn, and many more.\n\nStarting back in 2006, the system included hand-tuned calibrations for just 21 camera models. In the years since, that number has grown to 784 models, across all major camera vendors. This even includes support for Apple Pro RAW files for iPhone cameras.\n\nBut the key feature of RAW, is that a beloved photo you took years ago, can be reprocessed using the latest state-of-the-art algorithms. Apple's RAW processing pipeline has been updated eight times, each improving demosaic, denoise and color. Some older versions have been maintained, so that people can still use them if desired.\n\nNow, Apple has made its biggest update yet, RAW 9! This version dramatically improves the rendering of RAW files. It is built atop a tiled CoreML model, that combines demosaic with denoise for best quality. And the model is run on device using the Apple Neural Engine cores, for optimal performance.\n\nLet me demonstrate the improvements up close.\n\nThis is a zoomed-in crop of a low noise image using RAW 8. This Sony Alpha 7 II image of a vintage dial indicator actually looks quite good. However, when you explore that same image under RAW 9, the image is sharper, clearer, and the fine text is easier to read.\n\nThe differences are even more dramatic, when you view high noise images. First, observe the actual RAW data that is contained in this very noisy ISO 51,200 image.\n\nIn this example from a Canon 5D Mark III, the image is a 10x Crop of a box of crayons. There is so much luma and chroma noise in the RAW data, that it's impossible to discern the unique color of each crayon. Using our previous algorithms, this is the result! RAW 8 did an acceptable job of recovering the actual colors in the scene. But if you examine the results under RAW 9, the output is significantly better. The colors are accurate and well defined. Even the shiny specular highlights on the crayons are visible.\n\nThis last example is a crop of a photo of embroidery yarn, shot with a Fujifilm X-T5 at ISO 12,800. This camera has a non-traditional sensor pattern, which is challenging to demosaic. In the RAW 8 results, there are some color artifacts and loss of detail in the yarn. But if you observe the same image under RAW 9, the results are discernibly better. The small text is more legible, and the texture in the yarn much clearer.\n\nSo here is how you can enable RAW 9 in your application. First, use the CIRAWFilter API to load a RAW file. This API is well described in the \"Capture and process ProRAW images\" from WWDC 21.\n\nHowever, it is important to know that RAW 9 is not enabled by default. You should check that the supportedDecoderVersions property contains the version9 enumeration. If so, you can opt in by setting the decoderVersion property to version9.\n\nTo know what camera models support RAW 9, there's now a class method called supportedCameraModels. This API will provide your app, an array of all the models that are supported with a given version. In the release of iOS, iPadOS, macOS, and visionOS 27, there will be hundreds models that can use RAW 9, including all major professional camera vendors. This camera list will grow, via over-the-air updates to the operating system. RAW 9 is also automatically supported by cameras that shoot DNG natively, such as Apple iPhones.\n\nThe true power of the CIRAWFilter API is unleashed, when your app modifies its properties. This allows people to customize how RAW files are displayed.\n\nThere are currently 20 calibrated properties that can be adjusted. Adopt these adjustments in your app, to unlock the full power of editing RAW images. These are the most important controls for editing RAW: exposure, which controls how much to brighten or darken the image, luminanceNoiseReductionAmount, which adjusts how much fine luma grain is visible, sharpnessAmount, which determines how much the edges are sharpened, and contrastAmount, which alters how much local contrast is applied near edges.\n\nAll these controls work even better in RAW 9 than they did in prior versions.\n\nThere are some properties that are no longer needed in RAW 9. The colorNoiseReductionAmount property now has no effect, because the CoreML model handles color noise reduction automatically.\n\nAlso, the detailAmount and moireReductionAmount properties, are no longer needed nor supported in RAW 9.\n\nYou can call the is supported properties, to check if properties work for the filter instance.\n\nNow that you have learned how RAW 9 appears and behaves, I would like to discuss its performance. Fundamentally, in order to get such quality improvements, RAW 9 is more performance and resource intensive than prior versions. Even though RAW 9 runs the CoreML model hundreds of times per image, when an app edits CIRAWFilter properties, subsequent renders are fast and responsive.\n\nThis is because Core Image caches intermediate results.\n\nThe best practices to get optimal performance, depends on how your app uses RAW files. I will give some recommendations for the two most common use cases.\n\nFirst, here is advice on the interactive editing use case.\n\nInteractive editing, is when one RAW file is rendered multiple times at screen resolution. Typically this is in response to the app adjusting a CIRAWFilter property, such as exposure or sharpness.\n\nWhen interactive editing RAWs, use the scaleFactor property of the CIRAWFilter, when displaying the image at a reduced size. This is important because it reduces the work to render images that have more megapixels than the display.\n\nUse one CIContext per view and set its cacheIntermediates option to true. Caching allows the intensive CoreML work to be skipped, while the app is adjusting CIRAWFilter properties.\n\nCore Image will use more memory for caching between renders, if you add the \"Extended Virtual Addressing entitlement\" to your application. There is detailed documentation for this on Apple's website. Also, render directly to Metal-backed views. This improves performance of repeated renders, because Metal can start work on the next frame before, the previous frame is completed. For more information on rendering to a MTKView, consult the \"Display EDR content with Core Image, Metal, and SwiftUI\" from WWDC 22.\n\nAfter that advice on the interactive editing use case, here are the most important tips when exporting files: The exporting use case, is when multiple RAW files are each rendered once at full resolution to other formats like HEIF or JPEG.\n\nThe best practice for this case is to export with a CIContext, with the option cacheIntermediates set to false.\n\nAlso, you can tell Core Image to use more memory during each export, by setting the context memoryLimit option. On iOS, the default limit is a conservative 256 megabytes. Setting the limit to 512 or 1024 megabytes can significantly improve performance.\n\nAnd you can get extra memory savings by using the context methods heifRepresentation or jpegRepresentation, instead of calling Image IO directly.\n\nIn the last section, I want to talk about improvements to the CIImageProcessor API.\n\nRAW 9 uses the CIImageProcessor API, because it enables algorithms that use CoreML in conjunction with other CIKernels.\n\nAs a result of this effort, the Core Image team added two features to the CIImageProcessor API that you may want to use in your app. The first I'll talk about, is support for explicit output tile sizes.\n\nThis is an example of a typical CIImageProcessor class. It implements the region of interest callback, that defines how much of its input is needed for a given output rectangle.\n\nMost importantly, within the process callback, you must be aware of the input.region and output.region, that the processor should operate on. If there is plenty of memory, then CoreImage will call the process function, with an output.region for the entire image. But when memory is limited, the output.region might be considerably smaller.\n\nNow, a processor can explicitly control output tile sizes. The code in your processor class remains as before, but you tell CoreImage to use the output regions of your choice.\n\nFirst create an array, and then fill the array with all the desired tiles to cover your image. In this example, the tiling strategy takes the input image extent, and breaks it up into 512x512 pixel tiles. When you create an image which uses the processor, call its apply method and pass in the tile array that covers the image.\n\nSo now that I've described explicit output tile sizes, my last topic is the temporary buffers feature.\n\nIt is common for a CIImageProcessor that calls CoreML to use temporary buffers. This is because CoreImage uses interleaved image buffers, which must be converted to planar data for CoreML. When a processor callback is called for many tiles, the temporary buffers will be created and destroyed repeatedly. This can impact performance.\n\nThe CIImageProcessorOutput class, now has methods to help. Here's how this works! Here's an example of a simple CIImageProcessor class that uses the temporary buffer feature.\n\nInstead of processing directly, from input to output, this callback requests a scratch buffer by asking the output object for a temporary CVPixelBuffer.\n\nWhen doing so, provide an identifier. This is vital for process callbacks that use more than one temporary buffer.\n\nThen, this example processor copies from the input pixel buffer to the temporary buffer, alters the temporary buffer pixels in-place, and finally copies that to the output. Core Image will manage the lifecycle of temporary buffers for you. They will be released automatically and released at the correct time and recycled when the processor is called for the next tile.\n\nHere are the key points to take away.\n\nTry RAW 9 in your app. It's a major quality improvement and it's just a few lines of code to enable.\n\nFollow the performance best practices, I outlined for fast exporting and responsive editing.\n\nGive your app access to the powerful editing properties of CIRAWFilter, that let people finetune how their RAW images appear.\n\nAnd finally, for optimal performance in your CIImageProcessor, here use the explicit tiling and temporary buffer APIs. Personally, I have really enjoyed revisiting my library, of over 7000 family photos that I have shot in RAW over the past 20 years. It has been really great to see the improved quality of noise reduction. I think that people using your app, will also enjoy these improvements. Thank you for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "11:08",
+ "title": "Contact for exports",
+ "language": "swift",
+ "code": "let exportCtx = CIContext(options : [\n .cacheIntermediate : false,\n .memoryLimit : 512 ])"
+ },
+ {
+ "timestamp": "12:23",
+ "title": "CIImageProcessor with explicit output tile sizes",
+ "language": "swift",
+ "code": "import CoreImage\n\nclass MyProcessor: CIImageProcessorKernel {\n override class func roi(forInput input: Int32,\n arguments: [String : Any]?,\n outputRect: CGRect) -> CGRect { return outputRect }\n \n override class func process(with inputs: [CIImageProcessorInput]?,\n arguments: [String : Any]?,\n output: CIImageProcessorOutput) throws {\n guard let input = inputs?.first,\n let iBuffer = input.pixelBuffer,\n let oBuffer = output.pixelBuffer else { return }\n \n let iRegion = input.region\n let oRegion = output.region // controlled by Core Image\n \n // MyCopyBuffer(iBuffer,iRegion, oBuffer,oRegion)\n }\n}\n\nlet extent = inImg.extent\nlet tileSize = 512.0 // whatever tile size you want\nvar tiles: [CIVector] = []\nfor y in stride(from: extent.minY, to: extent.maxY, by: tileSize) {\n for x in stride(from: extent.minX, to: extent.maxX, by: tileSize) {\n let tile = CGRect(x: x, y: y,\n width: min(tileSize, extent.maxX - x),\n height: min(tileSize, extent.maxY - y))\n tiles.append(CIVector(cgRect: tile))\n }\n}\n\nlet result = try MyProcessor.apply(withTiledExtent: tiles, inputs: [inImg], arguments: [:])"
+ },
+ {
+ "timestamp": "14:24",
+ "title": "CIImageProcessor using temporary PixelBuffer",
+ "language": "swift",
+ "code": "import CoreImage\n\nclass MyProcessor: CIImageProcessorKernel {\n override class func process(with inputs: [CIImageProcessorInput]?,\n arguments: [String: Any]?,\n output: CIImageProcessorOutput) throws {\n guard let input = inputs?.first,\n let srcPixelBuffer = input.pixelBuffer,\n let dstPixelBuffer = output.pixelBuffer else { return }\n \n // Get a scratch buffer from Core Image's cache\n guard let scratch = output.temporaryPixelBuffer(identifier : \"myScratch\",\n format: kCVPixelFormatType_64RGBAHalf,\n width: Int(output.region.width),\n height: Int(output.region.height),\n pixelBufferAttributes: nil) else { return }\n \n // Step 1: copy input CVPixelBuffer → scratch\n // Step 2: process pixels in scratch\n // Step 3: copy scratch → output CVPixelBuffer\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Extended Virtual Addressing Entitlement",
+ "url": "https://developer.apple.com/documentation/BundleResources/Entitlements/com.apple.developer.kernel.extended-virtual-addressing"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/305/5/d8d5f3ce-0ff1-45a3-a630-436743477c62/downloads/wwdc2026-305_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/305/5/d8d5f3ce-0ff1-45a3-a630-436743477c62/downloads/wwdc2026-305_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "10114",
+ "year": "2022",
+ "title": "Display EDR content with Core Image, Metal, and SwiftUI",
+ "url": "https://developer.apple.com/videos/play/wwdc2022/10114"
+ },
+ {
+ "id": "10160",
+ "year": "2021",
+ "title": "Capture and process ProRAW images",
+ "url": "https://developer.apple.com/videos/play/wwdc2021/10160"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:20.060Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-309.json b/data/wwdc/videos/2026-309.json
new file mode 100644
index 0000000..d0429ea
--- /dev/null
+++ b/data/wwdc/videos/2026-309.json
@@ -0,0 +1,89 @@
+{
+ "id": "309",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/309/",
+ "title": "Explore Retention Messaging in App Store Connect",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Selling auto-renewable subscriptions on the App Store can be rewarding. It can also be challenging to reach customers at critical moments, such as when they're considering canceling a subscription. That moment is an ideal time to remind your customers of the value they'll receive from your subscription service. I'm Tori, an engineer on the App Store server team, and in this video, I'll share how you can retain subscribers with Retention Messaging in App Store Connect, a new feature built to help you connect with your customers. First, I'll share an overview of Retention Messaging, and how to set it up in App Store Connect.\n\nThen, I'll discuss how real-time Retention Messaging works alongside Retention Messaging in App Store Connect. Here in my Exercise app is an example of a customer canceling an active subscription. The cancellation page displays messaging to the customer with the impact of canceling.\n\nFor this subscription, the App Store informs the customer that their family will no longer have access to this subscription if they proceed with the cancellation. Here is an opportunity to provide messaging or offers to the customer. Retention Messaging enables you to do just that.\n\nRetention Messaging helps you add value to the cancellation flow by showing a compelling value proposition message to your customers when they are considering canceling their subscription. You can use Retention Messaging to remind customers of the benefits of your subscription or provide them an offer to entice them to remain subscribed. Retention Messaging supports three different views: a message only, a message and an image, or a message and an offer.\n\nYou can see what these three different views look like with my Exercise app. Here, the message is prominently displayed to the customer. I can add an image to add some visual appeal. Or I can offer the customer three months free for their current subscription, Yoga+ to provide an extra incentive to stay subscribed. Save rate is the % of subscribers who keep their subscription after reaching the cancel confirmation page in manage subscriptions. Subscriptions that have adopted Retention Messaging experience an average save rate increase of +1.4 points equivalent to an 82% increase, with promotional offer messages achieving the highest observed save rate at +5.5 points equivalent to a 223% increase. Note that we have observed varying results across developers. I'll show you how to set up Retention Messaging in App Store Connect.\n\nWith Retention Messaging in App Store Connect, you can configure retention messages for your app, which you can map to any of your subscriptions. You can select an image to provide alongside a message from Asset Library. You can configure retention offers to provide to your customer alongside a message. And, App Store Connect API will also support Retention Messaging, allowing you to set up retention messages and retention offers through the API.\n\nTo learn more about Asset Library, check out the session \"Enhance your presence on the App Store\" from WWDC26. Let's review how this works.\n\nIn App Store Connect, on the Subscriptions page, a new area prompts me to get started with Retention Messaging.\n\nWhen I tap the Get Started button... a modal appears for me to name the retention message. This message is for my yoga subscriptions, so I'll call this Yoga Message, then tap the Create button.\n\nA new view shows where I can edit the message text, the image and applicable subscriptions and offers on the left. On the right is a live preview of my retention message which updates as I edit.\n\nA retention message must always contain message text, but the image and offer are both optional. I'll continue creating a retention message for the Yoga+ subscription, starting with the text. I will create a message in English, but you can select other localizations as well.\n\nI'll type \"Launching Next Month: Guided Yoga into the text box\". Wow! The preview updated right away with the title.\n\nI'll keep going and add a description, Don't miss out on guided yoga classes, new metrics targeted for yoga, and more. Again, notice how the preview on the right immediately updates.\n\nNow I want to see how this looks with an image. I already have an image in my Asset Library, so I'll choose that.\n\nOur preview is looking really good now! Now I need to select the Yoga+ subscription so that all my customers with the Yoga+ subscription see this message. The Yoga+ subscription is selected now. You can also select other subscriptions as a retention message can be associated with many subscriptions.\n\nFinally, I want to add some offer options for my eligible customers. I have some retention offers already set up, so I'll select those. Here are all the offers that could appear with this message along with their eligibility and availability.\n\nThe App Store will automatically choose the best offer for eligible customers, so you can select multiple offers for a single subscription with confidence. It's also important to note that when the customer is eligible for an offer, the image will be replaced by an offer.\n\nIf you want to see how other offers appear, you can select a different offer from the offer dropdown. You can also select no offer from the dropdown to see what the preview looks like without an offer.\n\nLet's pivot to testing. Testing in sandbox verifies your subscription implementation before proceeding to production. I'm happy to share that retention messages are fully testable in sandbox. You can test retention messages when canceling your subscription in sandbox.\n\nIf you configured a retention offer, you can also verify offer related fields are properly configured in the signed transaction or renewal info.\n\nWe are also introducing retention offers, a new offer type for retention messages. The signed transaction and renewal info are updated with a new offerType value of 5 to indicate a retention offer was redeemed. Other offer related fields such as: offer identifier, offer discount type, and offer period will also appear as expected. By setting up Retention Messaging in App Store Connect, you are taking a significant step toward retaining your subscribers at a critical moment in the subscription lifecycle. If you want to more directly interact with your customer when they are about to cancel their subscription, you can take this one step further with real-time Retention Messaging.\n\nWith real-time Retention Messaging, you set up an endpoint to provide a real-time message preference in response to a server-to-server HTTP request from the App Store. With the Retention Messaging API, you can configure message and image pairs to show to customers in the cancellation flow. When you receive a real-time call from the App Store, you can tell the App Store which of these messages you want to show to the customer, or when Retention Messaging in App Store Connect is configured, you can also indicate one of the messages from App Store Connect as your preference.\n\nI can create the same views for the Yoga+ subscription using real-time Retention Messaging as I can when using retention messages in App Store Connect.\n\nHowever, real-time Retention Messaging also supports a message with a switch plan format, so you can offer your customer a different plan within the same subscription group as an alternative to canceling. For the Yoga+ subscription, I chose to offer an annual subscription as a switch plan.\n\nReal-time Retention Messaging is powered by the Retention Messaging API, a server-to-server driven set of endpoints for managing your messages and images. Using the Retention Messaging API in both sandbox and production, I can configure the URL for the Exercise App's endpoint, set up messages for the exercise app, choose default messages for each subscription in the exercise app, and upload and manage the Exercise App's images.\n\nIn the Sandbox environment only, the Retention Messaging API supports Performance testing, and provides endpoints to initiate a performanceTest and check results. Passing a performance test is required before using real-time Retention Messaging in production.\n\nWhen the App Store sends a request for real-time Retention Messaging, we will send the originalTransactionId to help identify the subscription, the customer's locale to identify the desired localization, plus a requestIdentifier for tracking, as well as other information. When responding to a real-time request, you can choose one of three response formats.\n\nRespond with a message to show a message or message with an image by providing a messageIdentifier. I chose a messageIdentifier that indicates a message about my yoga subscription that is paired with a corresponding image. Respond with an alternateProduct to offer the customer a switch plan in the same subscription group by providing a messageIdentifier and productId. I chose to offer an annual tier of Yoga+ as a switch plan.\n\nOr respond with a promotional offer to give the customer an offer by providing a messageIdentifier and a promotionalOfferSignature. I chose to provide my promotionalOffer for 3 months free of Yoga+. More information on what to provide in each of these scenarios, other supported scenarios, and the rest of the API can be found in our documentation linked in this session's resources.\n\nReal-time Retention Messaging requires a fast, responsive server. To ensure a great customer experience, you'll need to pass a performance test in sandbox before going live to production. That said, your server may not always respond in time. If it doesn't, the App Store will display fallback messaging to the customer.\n\nReal-time Retention Messaging always prioritizes your real-time response. If that's unavailable or malformed, the App Store will first fall back to your App Store Connect Retention Messaging preference, including any eligible offers. If App Store Connect messaging is not configured for your subscription, the App Store will then fallback to default messaging configured with the Retention Messaging API. Get started in sandbox for setting up real-time Retention Messaging. First, set up all your test messages and images in sandbox. When that is completed, set up your endpoint to begin receiving requests when a tester tries to cancel their subscription.\n\nAfter you are satisfied with your testing in sandbox, start a performance test.\n\nWhen your performance test passes, then set up messages and images in production followed by your production endpoint.\n\nAt this point you are ready to begin responding to requests from the App Store. Remember to always keep your production messages and images up to date.\n\nIOS 26.5 introduced monthly subscriptions with a 12-month commitment. For this new billing plan type the real-time Retention Messaging API is also updated to support this plan type as a switch plan. Simply provide the billingPlanType field with your alternateProduct selection in your response to the App Store to offer this plan to your customers using Retention Messaging. For more information on monthly subscriptions with a 12-month commitment, check out the WWDC26 session \"What's new in Apple In-App Purchase.\" Real-time Retention Messaging is a powerful tool and it performs best with a fast, responsive server. Let's compare it with Retention Messaging in App Store Connect to help you find the right fit for your use case.\n\nThe key difference between Retention Messaging in App Store Connect and real-time Retention Messaging is decisioning. Once Retention Messaging is set up for your subscription in App Store Connect, the App Store will show the message and any applicable offer to the customer without any further interaction with you. With real-time Retention Messaging, you choose what you want to show to each customer in real time.\n\nBoth Retention Messaging in App Store Connect and real-time Retention Messaging allow you to configure messages and images though the mechanisms for doing that differ. With Retention Messaging in App Store Connect, this can be done through App Store Connect or the App Store Connect API. For real-time Retention Messaging, this must be done server-to-server through the Retention Messaging API.\n\nBoth frameworks also support offers, with App Store Connect requiring specific retention offers linked to your subscriptions, while real-time Retention Messaging allows you to leverage promotional offers so you can choose the offer you want to deliver in real time. A signature is still required for promotional offers used in retention messages.\n\nApp Store Connect Retention Messaging supports three views: message, image or offer. Real-time Retention Messaging supports those same three views, and also supports a switch plan view.\n\nRetention Messaging in App Store Connect is an ideal choice if you don't have a server, or if you want to be able to configure your retention messages and then let the App Store choose what to display to the customer. App Store Connect messages are tied to specific subscriptions.\n\nIf you have a server and want to choose which message is shown to each customer, consider real-time Retention Messaging. Also consider real-time Retention Messaging if you want to take more control of offers and eligibility using promotional offers.\n\nReal-time Retention Messaging builds on top of Retention Messaging in App Store Connect. You should always consider setting up Retention Messaging in App Store Connect even when using real-time Retention Messaging for fallback messaging. Retention Messaging in App Store Connect is a wonderful way to provide value to your customers in the subscription cancellation flow and is open to all.\n\nReal-time Retention Messaging builds on top off Retention Messaging in App Store Connect. If you are interested in real-time Retention Messaging, please fill out the interest form linked in this session's Resources to request access.\n\nLet's recap what I covered and how you can make the most of Retention Messaging.\n\nConsider the types of Retention Messaging you want to set up in App Store Connect. To go further, explore the real-time Retention Messaging API documentation to determine if it's a good fit for your app. If you wish to pursue real-time Retention Messaging, submit the interest form to request access.\n\nThanks for joining me! I've loved teaching you about Retention Messaging, and I look forward to have you use retention messages in your app.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "6:08",
+ "title": "Signed transaction updates",
+ "language": "swift",
+ "code": "// Signed transaction updates\n\n{\n \"bundleId\": \"com.example.app\",\n \"productId\": \"Yoga_summer_2026\",\n \"type\": \"Auto-Renewable Subscription\",\n \"transactionReason\": \"RENEWAL\",\n \"inAppOwnershipType\": \"PURCHASED\",\n \"quantity\": 1,\n \"price\": 0,\n \"currency\": \"USD\",\n \"offerType\": 5, // retention offer\n \"offerIdentifier\": \"Yoga_2026_cancel_free_3m\",\n \"offerDiscountType\": \"FREE_TRIAL\",\n \"offerPeriod\": \"P3M\", \n \"transactionId\": \"1000098916194\"\n \"originalTransactionId\": \"1000011859217\",\n \"appAccountToken\": \"23a91ca7-06f3-425f-bff6-820904b510a9\",\n ...\n}"
+ },
+ {
+ "timestamp": "7:50",
+ "title": "Retention Messaging API",
+ "language": "swift",
+ "code": "// Retention Messaging API: https://api.storekit.apple.com/inApps/v1/messaging\n\n// URL configuration\nPUT /realtime/url\nGET /realtime/url\nDELETE /realtime/url\n\n// Message configuration\nPUT /message/{messageIdentifier}\nDELETE /message/{messageIdentifier}\nGET /message/list\nPUT /default/{productId}/{locale}\nDELETE /default/{productId}/{locale}\nGET /default/{productId}/{locale}\n\n// Image configuration\nPUT /image/{imageIdentifier}\nDELETE /image/{imageIdentifier}\nGET /image/list\n\n// Performance testing - Sandbox only\nPOST /performanceTest // initiate test\nGET /performanceTest/result/{requestId} // get results"
+ },
+ {
+ "timestamp": "8:34",
+ "title": "Real-time requests",
+ "language": "swift",
+ "code": "// Real-time requests\n\n// Request from the App Store\n{\n \"originalTransactionId\": \"123456789\",\n \"appAppleId\": 6745974591,\n \"productId\": \"Yoga_summer_2026\",\n \"userLocale\": \"en-US\",\n \"requestIdentifier\": \"c03248af-dd76-4e9b-9c1e-4489cd19a768\",\n \"environment\": \"Production\", // or Sandbox\n \"signedDate\": 1780920000000\n}"
+ },
+ {
+ "timestamp": "8:57",
+ "title": "Real-time requests with message",
+ "language": "swift",
+ "code": "// Real-time requests\n\n// Request from the App Store\n{\n \"originalTransactionId\": \"123456789\",\n \"appAppleId\": 6745974591,\n \"productId\": \"Yoga_summer_2026\",\n \"userLocale\": \"en-US\",\n \"requestIdentifier\": \n \"c03248af-dd76-4e9b-9c1e-4489cd19a768\",\n \"environment\": \"Production\", // or Sandbox\n \"signedDate\": 1780920000000\n}\n\n// Your response\n{\n \"message\": {\n \"messageIdentifier\": \n \"551ee7c0-c097-418e-9dd5-2a98533a7390\"\n }\n}"
+ },
+ {
+ "timestamp": "9:11",
+ "title": "Real-time request with alternate product",
+ "language": "swift",
+ "code": "// Real-time requests\n\n// Request from the App Store\n{\n \"originalTransactionId\": \"123456789\",\n \"appAppleId\": 6745974591,\n \"productId\": \"Yoga_summer_2026\",\n \"userLocale\": \"en-US\",\n \"requestIdentifier\": \n \"c03248af-dd76-4e9b-9c1e-4489cd19a768\",\n \"environment\": \"Production\", // or Sandbox\n \"signedDate\": 1780920000000\n}\n\n// Your response\n{\n \"alternateProduct\": {\n \"messageIdentifier\":\n \"ed7f25fc-5741-46a3-8502-062e0fb8afd0\",\n \"productId\": \"Yoga_summer_2026_annual\"\n }\n}"
+ },
+ {
+ "timestamp": "9:24",
+ "title": "Real-time request with promotional offer",
+ "language": "swift",
+ "code": "// Real-time requests\n\n// Request from the App Store\n{\n \"originalTransactionId\": \"123456789\",\n \"appAppleId\": 6745974591,\n \"productId\": \"Yoga_summer_2026\",\n \"userLocale\": \"en-US\",\n \"requestIdentifier\": \"c03248af-dd76-4e9b-9c1e-4489cd19a768\",\n \"environment\": \"Production\", // or Sandbox\n \"signedDate\": 1780920000000\n}\n\n// Your response\n{\n \"promotionalOffer\": {\n \"messageIdentifier\": \n \"80135e2b-ae15-4ec4-8c5c-9ecc8045c0dc\",\n \"promotionalOfferSignatureV2\": \"eyJhbGciOiJFUzI…\"\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Interest form: Real-time Retention Messaging",
+ "url": "https://developer.apple.com/contact/request/retention-messaging-api/"
+ },
+ {
+ "title": "Supporting monthly subscriptions with a 12-month commitment",
+ "url": "https://developer.apple.com/documentation/StoreKit/supporting-monthly-subscriptions-with-a-12-month-commitment"
+ },
+ {
+ "title": "Retention Messaging API",
+ "url": "https://developer.apple.com/documentation/RetentionMessaging"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/309/4/afa0aec8-f216-43ed-bcb1-1a3742e49dac/downloads/wwdc2026-309_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/309/4/afa0aec8-f216-43ed-bcb1-1a3742e49dac/downloads/wwdc2026-309_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "205",
+ "year": "2026",
+ "title": "Enhance your presence on the App Store",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/205"
+ },
+ {
+ "id": "210",
+ "year": "2026",
+ "title": "What’s new in Apple In-App Purchase",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/210"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:20.133Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-310.json b/data/wwdc/videos/2026-310.json
new file mode 100644
index 0000000..abf5ae8
--- /dev/null
+++ b/data/wwdc/videos/2026-310.json
@@ -0,0 +1,48 @@
+{
+ "id": "310",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/310/",
+ "title": "What’s new in Shortcuts",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "System Services"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi! My name is Duraid and I'm a member of the Shortcuts team.\n\nWith Shortcuts, you can combine actions that you perform in your apps every day and quickly run them from different places across the system, like Siri, Control Center, Action Button, and more.\n\nIn this session, I'm going to share some exciting improvements coming to Shortcuts and how you can make your app's actions and content fit right in.\n\nI'll start by going over updates to the way automations are set up, and a set of new automation types that are available. Next, I'll go over the Use Model action and a new way to debug its behavior. And finally, I'll cover storage, a feature that lets you persist data between runs of a shortcut. Let's dive in! With automations, you can make your shortcuts run automatically in response to events, for example, here's a shortcut that runs every time I leave work. It calculates my route home, and sends a message to my partner Helen with my estimated time of arrival.\n\nAutomations now live directly in the Shortcuts editor, alongside the actions that run in a shortcut and they're easier to set up than ever. To browse them and add one to a shortcut, you can visit the \"Automation\" section in the editor.\n\nIn addition to the current set of automations, there are also three new types available: the screenshot automation, which runs when a screenshot is saved, the keyboard automation, which triggers when an external keyboard is connected or disconnected, and the notification automation, which runs when a notification is received from a specific app. I'll dive into an example of how I can use the notification automation with an app I'm working on.\n\nThis is Soup Chef, an app that allows me to browse soups and order them for delivery.\n\nWhen a soup delivery is close, my app sends a notification with the driver's name, and how soon to expect them.\n\nI like to turn my porch lights on when I get that notification so the driver can find the door when it's late at night.\n\nI'd love for that to happen automatically! To do this, I've created a shortcut that turns on my porch lights, and has my living room HomePod announce that my soup is about to arrive. The notification automation is triggered when I receive a notification from the Soup Chef app.\n\nI don't want it running for every notification from the app so I've added a filter for the word \"arriving.\" That way, it only runs when the app is notifying me that my soup is arriving. Now, my delivery person will always have a safe, well lit walk up to my front door.\n\nThis works well because my app provides a concise, informative notification. The driver's name makes it distinct, the verb \"arriving\" makes it specific, and the time until arrival makes it actionable. These details make the notification easy to parse and interpret within a shortcut.\n\nIf you follow the best practices for designing notifications in the Human Interface Guidelines, your users will be able to harness your notifications to build powerful automations like this one. Next, I'll talk about the Use Model action. The Use Model action lets you tap into the power of large language models, directly within your shortcuts. Now, it's more powerful than ever. With access to new, more capable Apple Intelligence models that have the ability to go out to the web for up-to-date information.\n\nAll of these models can work with content from your apps, like this example, where the action is able to find the events related to my upcoming trip to Montreal, when a large list of upcoming events is passed in.\n\nLet's go back to the Soup Chef app. I'd like to build a shortcut that picks a soup I'll like and lets me quickly order it. I'll call it Soup of the Day.\n\nTo build this, my shortcut will need access to my app's soup data. Using an EntityPropertyQuery in App Intents, I've already exposed a Find Soups action that fetches all soups from my app with the ability to filter to the ones available today.\n\nI've also built an action called Order Soup, which takes a soup as a parameter, asks me to confirm, and places the order.\n\nI'll start building my shortcut by stringing these together with the Use Model action.\n\nThis shortcut finds today's available soups, then uses a model to pick one that matches my spice level preference. Let's run it! It picked a chicken tortilla soup. But in Soup Chef, that's one of the milder options on the menu. I'm looking for something spicier.\n\nWhen a model produces a result you didn't expect, you might want to know exactly what the model saw, so you can understand what went wrong. There's a way to do exactly that: you can now inspect the model transcript and see everything that was passed to the model, in its raw format. I'll add a Show Content action right after the Use Model action. Next, I'll select the Transcript property on its output in the Show Content action.\n\nAnd now, when I run my shortcut, I can see the transcript.\n\nHere, I can see the exact soup entities that were passed to the model and I can expand each one to understand exactly what the model saw.\n\nHere is the structured representation of a Soup entity that was passed to the model. And these are the properties exposed on that entity. With just the name and availability, the model doesn't have enough to accurately judge spice level. Adding an ingredients property, which lists each ingredient and its quantity, should give the model what it needs. Let's add that.\n\nHere's my SoupEntity. It exposes properties for the name and availability of a given soup.\n\nLet's add ingredients here as well as a simple array of strings.\n\nEach string will contain an ingredient and its quantity per serving.\n\nI've removed the \"Show Content\" action that was used to debug, and I'll try running the shortcut again with the change.\n\nNow that the model has access to each soup's ingredients, it picked a Tom Yum soup that has some real heat! I think I'll order that one! For more on building App Intents for the Use Model action, check out \"Develop for Shortcuts and Spotlight with App Intents\" from WWDC25.\n\nFinally, I'll talk about Storage! With Storage, you can now save content within a shortcut, to persist it between runs.\n\nIn the shortcuts editor, there's now a view that allows you to create, view, and edit the values that are stored in a shortcut. You can also create a global value which is shared across multiple shortcuts. This is useful for data you need to access in more than one shortcut, like an API key.\n\nThese three actions enable you to retrieve and update this data from within your shortcut, unlocking so many possibilities: from simple shortcuts that count or log items, like daily coffees, to advanced ones that track richer context across runs.\n\nLet's take a look at how I can use storage to improve a shortcut I use every day.\n\nI closely follow motor racing, and I have a shortcut that shows me a technical fact every morning! The Use Model action is designed to be deterministic which is great when you want predictable model output. But for this shortcut, I want fresh facts every morning.\n\nI can use the Storage actions to achieve this. I'll start by retrieving a stored value that I'll name \"Previous Facts\". That'll be my list that contains every past fact.\n\nNext, I'll pass it to the model, and ask the model to stay away from these previous facts.\n\nOnce the model responds with a fact, I'll use the \"Add to List\" action which outputs a list with the new fact appended. I can use the setter action to store the new list. Now, my shortcut will come up with something new every day! The great thing is that storage works with any type of data in Shortcuts, including App Entities. Using Storage, I'd like to make a final improvement to my Soup of the Day shortcut. I've noticed that when I run it, the model often picks the same soup several days in a row. I want to give the model a memory of recent selections so it can pick a different one every day. Just like my motor racing example, I'll use the Storage actions here.\n\nWith that, the Use Model action knows not to repeat past picks. And now, once I've run the shortcut a few times, when I open the storage view there's a list of previous soups! I've been building and running this shortcut on my iPhone, but the great thing is its stored values sync across my devices, so if I'm using my iPad, I can run the shortcut there and it'll remember my past soups. Because these values sync across devices, entities need a consistent identity across every device. An entity saved on iPhone should be identified by your iPad or Mac app as the same entity. For example, here's a simple shortcut that retrieves a stored soup and passes it into my \"Order Soup\" intent. Since the stored value may have originated on a different device, the order intent needs to recognize the soup no matter which device originally stored it.\n\nTo make this work, I ensured my identifier comes from a source that produces the same value on every device, not one that varies per device. My Soup app is backed by an online database of soups, so I use each soup's database row ID as its stable entity identifier.\n\nSo those are some of the new capabilities coming to Shortcuts! Next, try building shortcuts that integrate with your app to get a sense of what your users might want to automate. Refine your notifications so your users can build powerful automations. And finally, test your App Entities to make sure they play well with the Use Model action and Storage in Shortcuts.\n\n\"Your soup is almost here.\" Well, time to go pick up my soup! Thanks for joining me!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "6:12",
+ "title": "Soup Entity Example",
+ "language": "swift",
+ "code": "// MARK: - Soup Entity\n\nimport AppIntents\n\nstruct SoupEntity: AppEntity, Identifiable {\n static var typeDisplayRepresentation = TypeDisplayRepresentation(\n name: \"Soup\",\n numericFormat: \"\\(placeholder: .int) soups\"\n )\n static var defaultQuery = SoupEntityQuery()\n \n var id: Soup.ID\n \n @Property var name: String\n \n @Property(title: \"Available Today\")\n var isAvailableToday: Bool\n \n @Property(title: \"Ingredients\")\n var ingredients: String\n \n var displayRepresentation: DisplayRepresentation {\n DisplayRepresentation(title: \"\\(name)\", subtitle: SoupStore.description(for: id))\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Shortcuts",
+ "url": "https://developer.apple.com/shortcuts/"
+ },
+ {
+ "title": "Notifications",
+ "url": "https://developer.apple.com/design/Human-Interface-Guidelines/notifications"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/310/4/50ce70ab-88da-49ff-8c57-d9136d231e76/downloads/wwdc2026-310_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/310/4/50ce70ab-88da-49ff-8c57-d9136d231e76/downloads/wwdc2026-310_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "260",
+ "year": "2025",
+ "title": "Develop for Shortcuts and Spotlight with App Intents",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/260"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:20.218Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-312.json b/data/wwdc/videos/2026-312.json
new file mode 100644
index 0000000..529d7b0
--- /dev/null
+++ b/data/wwdc/videos/2026-312.json
@@ -0,0 +1,78 @@
+{
+ "id": "312",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/312/",
+ "title": "Meet the Now Playing framework",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Audio & Video"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, my name is Leo Formaggio, and I'm an engineer on the Media Frameworks team.\n\nMedia has such an important presence in our daily lives. It's a podcast on the drive home. A high-energy playlist during a workout. Or watching a movie on a long flight.\n\nWhether we are spending time by ourselves, or connecting with people, media is always around us. On iPhone, it appears right on the lock screen, Control Center, and in the Dynamic Island.\n\nWhen the iPhone is set down and charging, it's glanceable in StandBy.\n\nWhen getting in the car, it appears front and center in CarPlay.\n\nThat's the system now-playing experience, and it's available on all Apple platforms. Including Apple Watch, Apple Vision Pro, and Apple TV! I'll show you how NowPlaying framework makes it easy to bring media from your app into the system.\n\nI'll start with the media sessions API, showing how to surface your content in the system now-playing experience.\n\nThen, I'll explain how to bring content playing on other devices into the system using remote media sessions.\n\nAnd finally, I'll go over how Media Sharing Extensions simplify playing media from iPhone to other devices.\n\nI'll use the example of an app I developed. It plays ambient sounds to help people focus and relax. From my app, I can select different sounds, and I can also pause and resume the audio.\n\nTo surface my content in the system, I used the media sessions API from NowPlaying. Next, I'll show you how I did it. Here's my PlayerModel – an @Observable class with a reference to my app's audio engine, and a property that tracks which sound is currently playing. The MediaSessionRepresentable protocol is like a contract between my app and the system. When PlayerModel conforms to it, the system will be able to understand what my app is playing – and how to handle media interactions, like skip or pause. Each session representation needs a unique identifier. The content property is used to describe the sound that's playing. NowPlaying offers content-specific types, like Music, Podcast, and MovieContent, to describe what kind of media your app is playing. GenericContent is a good fit for my use case, so I used that.\n\nEach content is identified by the current sound.id. I used the sound.name as the content title and a short sound.description as the subtitle. Media type can be either .audio or .video, but my app only plays audio. I set the duration to .continuous, because my app plays ambient sounds indefinitely. I provide an Artwork with an async closure. The system calls it whenever it needs an image at a specific size.\n\nNow, I'll show you what this looks like on the lock screen. The sound name and description appear along with the artwork.\n\nThe PlaybackSnapshot property is used to display the current playback state. Since ambient sounds are continuous, I just need to indicate if content isPlaying. For content with a defined duration, you should also specify an elapsedTime parameter in the snapshot.\n\nThrough the commands property, I define all the actions my app supports. Each command has a closure that the system calls when someone performs that action. For example, when I tap the pause button on the lock screen, the pause command closure is called, and I can pause the player.\n\nNotice the button has changed to reflect the paused state. Now, if I tap play on the phone, the play command closure is called, and I resume the player. Similarly, when I tap the next button on the lock screen, the next command closure is called. I can skip to the next sound, and the lock screen updates to reflect the new content.\n\nAfter I adopted MediaSessionRepresentable, I had to do one more thing to make my content available to the system. MediaSession is what connects the session representation with the system. I initialize it with my PlayerModel, in the same place where I set up my audio engine. Once that's done, MediaSession starts observing the model, keeping the now-playing surfaces up to date automatically. That's how I integrated my app's content with the system now-playing experience using media sessions.\n\nFor more information, check out the article \"Publishing Media Sessions\" on Apple Developer Documentation.\n\nIn addition to playing on iPhone, I made my audio engine available on smart speakers, which can be controlled by my app.\n\nFrom my app's device picker menu, I choose the speaker I want to control. I tap the Living Room Speaker to start controlling it. And, through a web server, my app connects to the selected speaker to request the playback state, and send commands.\n\nTo surface the content playing on that speaker in the system, I used the remote media sessions API.\n\nThis API uses an app extension and push notifications to receive updates about the speaker. I'll show you what this interaction looks like.\n\nWhen someone interacts with a speaker, the speaker communicates the state is changed to the server. The server then uses Apple Push Notification service, APNs, to send a push notification to the iPhone with the updated state. The system launches the app extension with the updated state from the push notification payload. The app extension then provides the system with an updated representation of that session. For more information on sending push notifications with APNs, check out the article \"Setting up a remote notification server\" on developer.apple.com When the interaction originates from iPhone system UI, the system calls a command handler in the app extension. The app extension sends the command to the server. And the server notifies the speaker, which reacts to the change.\n\nHere is how I adopted remote media sessions in my app.\n\nFirst I created an app extension conforming to the RemoteMediaSessionExtension protocol. To set it up, I used NowPlaying's RemoteMediaSessionExtensionConfiguration and the remote-media extensionPoint identifier. The session(:) method is called by the system whenever it needs to interact with a remote session representation, for example, to update the user interface, or to handle an interaction. Here, I can use the RemotePlayerState to create my model, and return it.\n\nWith the app extension configured, I'll show how I used my model to represent a Remote Media Session. This is my RemotePlayerModel. It's an @Observable class with a reference to ServerClient, the class I use to communicate with my server. It also keeps track of the server state. I'll use this as a foundation to build my Remote Media Session representation. Each Remote Media Session representation needs a unique identifier. I used the sessionID from my server state.\n\nThe content property is used to describe the sound playing on the speaker. Once again, I used GenericContent by giving it the sound identifier, the sound name and the sound description.\n\nThe media type is .audio and the duration is .continuous.\n\nI provided an Artwork object that loads an image for the current sound.\n\nThe server state indicates if the speaker isPlaying. I can use that to create a PlaybackSnapshot with the corresponding state.\n\nSince I'm controlling playback on a remote device, each command closure sends a request to the server with the corresponding action.\n\nFor example, if I tap play on iPhone, the play command closure is called. I send a play request to my server, which resumes playback on the speaker. Similarly, when I tap the next button, a request is sent to the server, and the speaker moves to the next sound.\n\nSo far, the adoption of RemoteMediaSessionRepresentable feels very similar to what we saw in media sessions for local playback. Next, I'll cover the remaining properties and methods that are specific to remote sessions.\n\nThe devices property tells the system about devices playing in that session. I map my server's device list into MediaDevice values. Each one needs a unique identifier that is stable across different sessions. I provide the name of the device, a device type, like .speaker in my case, and a list of capabilities, such as the device's volume control type.\n\nThis is what that looks like in ControlCenter. The device name appears along with the volume level.\n\nWhen I change the volume using the system volume slider, the volume change closure is called with the updated volume level. Here, I can send a volume change request to my server.\n\nThe update(:) function is called when a push notification is received with a new state. For example, when the content changes on the speaker. RemotePlayerState is a struct I defined, that conforms to RemoteMediaSessionAttributes. It represents my server state and the push notification payload. Here, I update my state variable with the new data. Because my model is observable, NowPlaying detects the change and updates the system automatically.\n\nAnd that's how I integrated my app's remote media session with the system. For more information, check out the article \"Publishing remote media sessions\".\n\nI also want to talk about Media Sharing Extensions – a set of APIs for playing media from iPhone to other speakers and TVs, all through a unified system interface. Media Sharing Extensions allow you to use the system device picker for all the media protocols your app supports.\n\nThis simplifies media device selection in your app, and the selection is reflected on system surfaces, like Control Center.\n\nTraditionally, supporting a media protocol meant embedding its SDK into your app bundle.\n\nWith Media Sharing Extensions, the protocol implementations live outside your app and are managed by the system.\n\nYour app can focus on the media content rather than the playback technology.\n\nAs more protocols become available, apps built with Media Sharing Extensions can use them without adopting another SDK.\n\nThat covered how to bring local and remote media sessions into the system now-playing experience using NowPlaying framework, and how Media Sharing Extensions can simplify sending media to other devices.\n\nFor apps that play media locally or control playback on remote devices – adopt NowPlaying to bring your content into the Lock Screen, Control Center, and beyond. It's a straightforward integration that gives people control over media even outside your app.\n\nLearn more about Media Sharing Extensions. They let your app use the system media device picker and expand your reach when playing media to other devices. For more information about Media Sharing Extensions, check out the article \"Routing media to third-party devices\".\n\nI can't wait to see your app extended to the system now-playing experience. Thank you for watching, and blue skies!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "1:57",
+ "title": "Existing PlayerModel implementation",
+ "language": "swift",
+ "code": "import Observation\n\n@Observable\nfinal class PlayerModel {\n let player: SoundPlayer\n var sound: Sound { player.currentSound }\n\n init(player: SoundPlayer) {\n self.player = player\n }\n}"
+ },
+ {
+ "timestamp": "2:06",
+ "title": "Adopt MediaSessionRepresentable",
+ "language": "swift",
+ "code": "import NowPlaying\n\nextension PlayerModel: MediaSessionRepresentable {\n var id: String { \"ambient-sound-session\" }\n\n var content: (any MediaContentRepresentable)? {\n return GenericContent(\n id: sound.id,\n title: sound.name,\n subtitle: sound.description,\n type: .audio,\n duration: .live,\n artwork: Artwork(id: sound.id) { size in\n let data = try await self.artworkData(size: size)\n return try ArtworkRepresentation(data: data)\n }\n )\n }\n\n var playbackSnapshot: MediaPlaybackSnapshot? {\n MediaPlaybackSnapshot(\n state: player.isPlaying ? .playing() : .paused\n )\n }\n\n var commands: [MediaCommand] {[\n .play { self.player.play() },\n .pause { self.player.pause() },\n .previous { self.player.previous() },\n .next { self.player.next() }\n ]}\n}"
+ },
+ {
+ "timestamp": "4:31",
+ "title": "MediaSession initialization",
+ "language": "swift",
+ "code": "import NowPlaying\n\nstruct PlayerController {\n let player: SoundPlayer\n let model: PlayerModel\n let session: MediaSession\n\n init() {\n self.player = SoundPlayer()\n self.model = PlayerModel(player: player)\n self.session = MediaSession(model)\n }\n}"
+ },
+ {
+ "timestamp": "6:42",
+ "title": "App extension entry point",
+ "language": "swift",
+ "code": "import ExtensionFoundation\nimport NowPlaying\n\n@main\nfinal class SampleAppExtension: @MainActor RemoteMediaSessionExtension {\n var configuration: some AppExtensionConfiguration {\n RemoteMediaSessionExtensionConfiguration(extension: self)\n }\n\n var extensionPoint: AppExtensionPoint {\n AppExtensionPoint.Identifier(host: \"com.apple.nowplaying\", name: \"remote-media\")\n }\n\n func session(_ state: RemotePlayerState) async throws -> RemotePlayerModel {\n RemotePlayerModel(state: state)\n }\n}"
+ },
+ {
+ "timestamp": "7:23",
+ "title": "Existing RemotePlayerModel implementation",
+ "language": "swift",
+ "code": "import Observation\n\n@Observable\n@MainActor\nfinal class RemotePlayerModel {\n let client: ServerClient\n var state: RemotePlayerState\n\n init(state: RemotePlayerState) {\n self.client = ServerClient(sessionID: state.sessionID)\n self.state = state\n }\n}"
+ },
+ {
+ "timestamp": "7:40",
+ "title": "Adopt RemoteMediaSessionRepresentable in app extension",
+ "language": "swift",
+ "code": "import NowPlaying\n\nextension RemotePlayerModel: @MainActor RemoteMediaSessionRepresentable {\n var id: String { state.sessionID }\n\n var content: (any MediaContentRepresentable)? {\n GenericContent(\n id: state.sound.id,\n title: state.sound.name,\n subtitle: state.sound.description,\n type: .audio,\n duration: .live,\n artwork: Artwork(id: state.sound.id) { size in\n let data = try await self.artworkData(size: size)\n return try ArtworkRepresentation(data: data)\n }\n )\n }\n\n var playbackSnapshot: MediaPlaybackSnapshot? {\n MediaPlaybackSnapshot(\n state: state.isPlaying ? .playing() : .paused\n )\n }\n\n var commands: [MediaCommand] {[\n .play { try await self.client.send(.play) },\n .pause { try await self.client.send(.pause) },\n .previous { try await self.client.send(.previous) },\n .next { try await self.client.send(.next) }\n ]}\n\n var devices: [MediaDevice] {\n state.devices.map { device in\n MediaDevice(\n id: device.id,\n name: device.name,\n type: .speaker,\n capabilities: [\n .absoluteVolume(device.volume) { volume in\n // send volume change to server\n }\n ]\n )\n }\n }\n\n func update(_ state: RemotePlayerState) {\n self.state = state\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Routing media to third-party devices",
+ "url": "https://developer.apple.com/documentation/AVSystemRouting/routing-media-to-third-party-devices"
+ },
+ {
+ "title": "Publishing remote media sessions",
+ "url": "https://developer.apple.com/documentation/NowPlaying/publishing-remote-media-sessions"
+ },
+ {
+ "title": "Publishing media sessions",
+ "url": "https://developer.apple.com/documentation/NowPlaying/publishing-media-sessions"
+ },
+ {
+ "title": "Setting up a remote notification server",
+ "url": "https://developer.apple.com/documentation/UserNotifications/setting-up-a-remote-notification-server"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/312/5/3f128d25-f1c6-49d3-a9c0-0bdc22af5f95/downloads/wwdc2026-312_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/312/5/3f128d25-f1c6-49d3-a9c0-0bdc22af5f95/downloads/wwdc2026-312_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:20.295Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-314.json b/data/wwdc/videos/2026-314.json
new file mode 100644
index 0000000..a515a38
--- /dev/null
+++ b/data/wwdc/videos/2026-314.json
@@ -0,0 +1,137 @@
+{
+ "id": "314",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/314/",
+ "title": "Learn CSS Grid Lanes",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Design",
+ "Safari & Web"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Brandon, an engineer on the Safari team. Today, I'm excited to share CSS Grid Lanes, a new layout mode for the web. If you've ever wanted a design, where items of any size flow naturally into the space available, I'm going to show you how to build it — in just a few lines of CSS.\n\nYou may already know this pattern by another name: masonry layout. Think of a waterfall — content flows naturally down the page in columns, each item settling into place beneath the last. Flip the direction and it becomes a brick wall where items flow across the page in rows. And the best part: you don't have to wait for it. Grid Lanes is available today in Safari 26.4 and behind a flag in other browsers.\n\nIf you've ever tried to build this layout, you know it's not straightforward. You've probably reached for a JavaScript library, or improvised with floats or Flexbox that almost works — until it doesn't. CSS Grid Lanes is built for exactly this.\n\nTo understand Grid Lanes, it helps to zoom out and think about what a layout mode actually does. Every layout mode answers the same two questions: where do items go, and how much space do they get? Flexbox and Grid each answer those questions differently.\n\nGrid Lanes is a new mode that fits somewhere in between. Here's what I mean by that: Flexbox, for example, gives you one axis and a single lane of items flowing along it. When items wrap, they continue flowing in the same direction. You choose that direction: row or column, and items are placed one after another.\n\nSwitch to column, and the same rule applies — items flow top to bottom, wrapping into the next column over.\n\nGrid takes a different approach. Where Flexbox gives you one axis, Grid gives you two: columns and rows. Grid places items into cells at the intersection of those tracks. But when items have different aspect ratios, you end up with large empty areas where shorter items don't fill their cells.\n\nNow, if these boxes were images, we've really only got three options, and none are great. We could stretch each image to fill its cell, but that distorts them. Or you could zoom in and fill the space, but now the image is overflowing the bounds of its container.\n\nYou may also crop the image, but you risk losing important information.\n\nThat's where Grid Lanes comes in. It sits between Grid and Flex. Instead of structuring across two dimensions like Grid, it structures just one, and leaves the other free. But unlike Flex, where content flows in a single lane and wraps down the page, Grid Lanes distributes content across multiple lanes.\n\nThe result is a tightly packed, staggered layout that preserves each item's natural proportions. Items are placed one by one, and each one lands in whichever column leaves it closest to the top.\n\nThat's why earlier items sit higher up, and later items fill in below.\n\nSo far, everything's been images. But Grid Lanes isn't picky, it works with any kind of content.\n\nTry adding some text. Each block wraps to fit its column, and the browser sizes the heights for you. And if something needs to stand out, like a headline, you can span it across columns.\n\nAnd it doesn't matter what you change - different shapes, different sizes, or a totally different design. Grid Lanes handles it.\n\nSo you've seen what Grid Lanes can do. Now, let's build one.\n\nWe start with a new display type — grid-lanes. Then we define the columns. grid-template-columns sets the number of tracks and how wide each track becomes.\n\nYou'll notice the fr unit here. fr stands for 'fractional unit'. It tells the browser to take the available space in the container and divide it up into fractions. So, this piece of code says to divide the space, into three equal fractions, creating three columns all equally sized.\n\nAnd add a gap of 10px, just like you do with Grid.\n\nWant to flip this into a brick wall instead? Just swap the columns for rows.\n\nReplace the grid-template-columns property with grid-template-rows and watch the layout turn into a brick wall.\n\nThe only catch: you pick one direction - not both.\n\nThat's Grid Lanes in three lines of CSS. But those three lines give you more control than you might expect. Let me show you what I mean.\n\nI start here with three equal columns.\n\nBut my columns don't have to be equal. Here, the center column takes up twice the space of the left and right columns. Now, instead of picking the number of columns yourself, let the browser decide. auto-fill creates as many columns as will fit. minmax() says each one should be at least 200 pixels, but can grow to fill the space. Take that same idea, but now with a repeating pattern of narrow and wide columns.\n\nHonestly, what I love most is how much you can do with just a few lines of CSS. Once you start exploring, I think you'll be amazed too.\n\nSo that's the layout. But Grid Lanes also lets you shape individual items. Let's start again with a simple grid-lanes container with 3 equally sized columns. I'd love to give the orange colored item more space. With Grid Lanes, I can do that using properties that already exist in Grid.\n\nWith grid-column: span 2; the item stretches across two columns, and the rest of the layout adjusts around it.\n\nOr, place it exactly where you want it. Here I set the item to start in column 2, spanning columns 2 and 3. Notice you can control column placement, but not row. Grid Lanes decides the row for you.\n\nI've turned one of our items into a recipe card that spans two columns. Inside the card is an image and some text, neither of which participate in the Grid Lanes layout.\n\nAdd display: grid-lanes and grid-template-columns: subgrid to the card, and its contents join the parent layout as their own items. The image takes one column, the text takes the other, each sized to its content.\n\nAnd you can nest them however you like. A regular grid inside a Grid Lanes container, or the other way around. The same tools and syntax you already know carry over, so Grid Lanes just fits right in.\n\nGrid Lanes places each item in whichever column is the shortest. Most of the time, that looks great. But sometimes… it doesn't quite feel right.\n\nThat's where flow tolerance comes in. The browser normally picks the shortest column for the next item. Flow tolerance is the dial that loosens or tightens that rule. I'll break that down.\n\nLooking at this layout, the two items in a row here are almost the same height, but not quite. That second item is a few pixels shorter than item 1.\n\nThat means the area under item 2 is closer to the top of my container, so the second column is where the browser will place my next item, and item 4 will fill in the first column.\n\nThis makes our item layout go from left-to-right on the first row, and right-to-left on our last row. The difference between tab order and how the content appears visually will impact accessibility for people and can create a confusing experience. Now turn flow-tolerance on. We're back to two items in two columns, but flow-tolerance changes the rules. For each new item, the browser asks the same question: is the taller column less than the shorter column plus flow-tolerance? For item 3, yes - the gap is within tolerance, so it fills column 1.\n\nBut now column 1 is even taller. For item 4, the gap is too big to ignore, so it drops into the shorter column: column 2.\n\nBy default, Grid Lanes uses a flow-tolerance of 1em.\n\nTry different flow-tolerance values to find what works for your content.\n\nFlow-tolerance is great when it works for your content, but the flip side of giving the browser flexibility is that sometimes the result may surprise you. And when that happens, pop open Web Inspector to figure out what's going on.\n\nThe good news is, Web Inspector has full support for Grid Lanes.\n\nYou get lines showing your columns and rows.\n\nYou get order numbers projected right over each item, so you can see exactly how items are placed.\n\nAnd it even draws in the gaps between items.\n\nAll of that, just by turning on the overlay.\n\nCSS Grid Lanes gives you a layout that used to require JavaScript in just a few lines of CSS. It builds on what you already know from Grid, and it adapts to your content, not the other way around.\n\nIt's a great addition to the web platform, and I can't wait to see what you build with it.\n\nCheck out the Grid Lanes Field Guide the WebKit team created for you for detailed walkthroughs and interactive demos where you can experiment with every property we covered today.\n\nTry grid-lanes in your own projects. It's been available since Safari 26.4, and we think it can genuinely change how you approach image-heavy layouts. And share your feedback. We'd love to hear how grid-lanes is working for you and what you'd like to see next.\n\nTo learn about all of the other features coming to Safari, be sure to check out \"What's new in WebKit for Safari 27\". Thanks for watching and have a great WWDC!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:58",
+ "title": "Create a Grid Lanes Container",
+ "language": "swift",
+ "code": ".container {\n\tdisplay: grid-lanes;\n}"
+ },
+ {
+ "timestamp": "4:02",
+ "title": "Create a Grid Lanes Container",
+ "language": "swift",
+ "code": ".container {\n\tdisplay: grid-lanes;\n grid-template-columns: repeat(3, 1fr);\n}"
+ },
+ {
+ "timestamp": "4:26",
+ "title": "Create a Grid Lanes Container",
+ "language": "swift",
+ "code": ".container {\n\tdisplay: grid-lanes;\n grid-template-columns: repeat(3, 1fr);\n gap: 10px;\n}"
+ },
+ {
+ "timestamp": "4:36",
+ "title": "Implement a Brick Variation",
+ "language": "swift",
+ "code": ".container {\n\tdisplay: grid-lanes;\n grid-template-rows: repeat(3, 1fr);\n gap: 10px;\n}"
+ },
+ {
+ "timestamp": "4:58",
+ "title": "Experiment with different layouts",
+ "language": "swift",
+ "code": ".container {\n\tdisplay: grid-lanes;\n grid-template-columns: 1fr 1fr 1fr;\n gap: 10px;\n}"
+ },
+ {
+ "timestamp": "5:02",
+ "title": "Experiment with different layouts",
+ "language": "swift",
+ "code": ".container {\n\tdisplay: grid-lanes;\n grid-template-columns: 1fr 2fr 1fr;\n gap: 10px;\n}"
+ },
+ {
+ "timestamp": "5:10",
+ "title": "Experiment with different layouts",
+ "language": "swift",
+ "code": ".container {\n\tdisplay: grid-lanes;\n grid-template-columns:\n repeat(auto-fill,\n minmax(200px, 1fr));\n gap: 10px;\n}"
+ },
+ {
+ "timestamp": "5:24",
+ "title": "Experiment with different layouts",
+ "language": "swift",
+ "code": ".container {\n\tdisplay: grid-lanes;\n grid-template-columns:\n repeat(auto-fill,\n minmax(8rem, 1fr)\n minmax(14rem, 2fr);\n gap: 10px;\n}"
+ },
+ {
+ "timestamp": "5:59",
+ "title": "Control Individual Items",
+ "language": "swift",
+ "code": ".container {\n\tdisplay: grid-lanes;\n grid-template-columns: 1fr 1fr 1fr;\n gap: 10px;\n}\n\n.item {\n grid-column: span 2;\n}"
+ },
+ {
+ "timestamp": "6:07",
+ "title": "Control Individual Items",
+ "language": "swift",
+ "code": ".container {\n\tdisplay: grid-lanes;\n grid-template-columns: 1fr 1fr 1fr;\n gap: 10px;\n}\n\n.item {\n grid-column: 2 / span 2;\n}"
+ },
+ {
+ "timestamp": "6:34",
+ "title": "Integrate Subgrid",
+ "language": "swift",
+ "code": ".container {\n\tdisplay: grid-lanes;\n grid-template-columns: 1fr 1fr 1fr;\n gap: 10px;\n}\n\n.item {\n display: grid-lanes;\n grid-template-columns: subgrid;\n grid-column: span 2;\n}"
+ },
+ {
+ "timestamp": "6:48",
+ "title": "Integrate Subgrid",
+ "language": "swift",
+ "code": ".container {\n\tdisplay: grid-lanes;\n grid-template-columns: 1fr 1fr 1fr;\n gap: 10px;\n}\n\n.item {\n display: grid;\n grid-template-columns: subgrid;\n grid-column: span 2;\n}"
+ },
+ {
+ "timestamp": "8:37",
+ "title": "Improve item positioning",
+ "language": "swift",
+ "code": ".container {\n\tdisplay: grid-lanes;\n grid-template-columns: 1fr 1fr;\n gap: 10px;\n flow-tolerance: normal;\n}"
+ },
+ {
+ "timestamp": "8:41",
+ "title": "Improve item positioning",
+ "language": "swift",
+ "code": ".container {\n\tdisplay: grid-lanes;\n grid-template-columns: 1fr 1fr;\n gap: 10px;\n flow-tolerance: 2.1em;\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "WebKit.org - CSS Grid Lanes Field Guide",
+ "url": "https://gridlanes.webkit.org"
+ },
+ {
+ "title": "WebKit.org – Report issues to the WebKit open-source project",
+ "url": "https://bugs.webkit.org"
+ },
+ {
+ "title": "Submit feedback",
+ "url": "http://feedbackassistant.apple.com"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/314/4/72928edd-5728-4010-b8f0-27f1a7bdec8c/downloads/wwdc2026-314_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/314/4/72928edd-5728-4010-b8f0-27f1a7bdec8c/downloads/wwdc2026-314_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "315",
+ "year": "2026",
+ "title": "Rediscover the HTML select element",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/315"
+ },
+ {
+ "id": "204",
+ "year": "2026",
+ "title": "What’s new in WebKit for Safari 27",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/204"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:20.507Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-315.json b/data/wwdc/videos/2026-315.json
new file mode 100644
index 0000000..02ebbd0
--- /dev/null
+++ b/data/wwdc/videos/2026-315.json
@@ -0,0 +1,165 @@
+{
+ "id": "315",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/315/",
+ "title": "Rediscover the HTML select element",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Design",
+ "Safari & Web"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi! I'm Tim, a Safari Engineer. Today, I'm going to take you through a new way to use the select element that makes it completely customizable, while still reusing the power of semantic HTML.\n\nAs a web developer, you've probably wrestled with drop-downs. Going beyond the default select element meant using heavy JavaScript libraries or lots of div elements. Accessibility can also get tricky to handle. Now, there's an easier way, all using just HTML and CSS: Customizable select. Starting in Safari 27 and Chrome 135, you can use the pre-existing select element to seamlessly integrate your drop-downs into your website.\n\nTo see how it works, I'm going to implement customizable select on a website I'm building for a client.\n\nI've been working with a photographer to build their portfolio so they can showcase and sell their photos. My client wants some sorting and filtering options, for which I'll use the select element.\n\nAnd yes, my client is also named Tim.\n\nAs a reminder, this is the HTML markup for the select element with its label.\n\nThe select element is a powerful tool, giving us basic accessibility right out of the box.\n\nI'm able to use the keyboard to navigate option elements and it works well with screen readers, all without needing any external libraries. I'll use that to build the Sort by button on my page. This is the native select, which by the way, are called pull down buttons on Apple platforms.\n\nWhat's great about the native select is it matches every other control on the platform, giving users a familiar way to navigate them.\n\nBut when I place my form control on my photo site, it feels a little out of place. It doesn't quite blend into the style of my site the way I want.\n\nThis is where customizable select comes in. I'll take you through the different steps of customization so that I get a select that better matches my design. I'll start by styling the select buttons on my site. The button is the part of the select element that I can click to show the select menu. Then, I'll use customizable select to style that menu, which displays my options.\n\nFinally, I'll show you how you can break away from the classic select element layouts with content that goes beyond just text.\n\nLet's go back to our first item, I'm going to style the most basic part of the control, the button.\n\nStarting from scratch, I get the native control. But now, I can use the new customizable select appearance to get a smaller set of styles to change to match my site's design. The first step is to apply — appearance: base-select.\n\nSince I previously set up font-family: Gill Sans on the body element, the body font is now inherited by the select button, matching the label beside it.\n\nThat's already bringing me one step closer to matching the design of my site. As a next step, I'll adjust the background, border, and padding. I like how well it matches my site. One last detail I want to change is the arrow. With customizable select, I can use a new selector called ::picker-icon to change it. I'll use the ::picker-icon selector to set the content property to a new glyph and to size it correctly with a width. Using the:open pseudo-class, I can also set different colors on the button when the drop-down menu is open.\n\nI've also updated the arrow to match the text color for the open state.\n\nHere is my select, matching the rest of my site. And that took me only a few lines of CSS to write. Now, I want to style the drop-down itself, and customizable select lets me do this! I'll show you how. Like ::picker-icon, the drop-down menu also comes with styleable parts: the menu itself can be styled with ::picker(select) on the select element and the check with ::checkmark on the option element.\n\nNow let's add some CSS. To start with a clean slate, I need to first opt-out of the native menu. I can do that by using the new ::picker(select) selector, and by setting appearance: base-select. Now, I'm ready to go. First, I'm going to arrange my spacing with some padding and margin. Let's handle the borders and the box-shadow on my drop-down.\n\nPerfect! Now I want to put some emphasis on the selected option so it's clear to my customer what they've picked. I can set a bold font on the checked option and gray out the other ones.\n\nAs a final step, I'll change the default checkmark by setting the content CSS property and the width on the ::checkmark selector, similarly to what I did with ::picker-icon.\n\nIt's amazing how far I was able to go styling my Sort by menu with so little code.\n\nHow much further can I go? With customizable select, I can now go beyond the simple list of options displayed as text. I'll go through an example with my next feature. My client is best known for photography in a handful of categories and I want to spotlight those photos. So, I'll add a way to browse photographs by their most popular categories. I decide to create another select element using the previous styling. However, I want the select to have symbols to make it more visually interesting.\n\nWith customizable select, I can put any kind of content: images, videos, emojis, whatever I like.\n\nIn this case, I've chosen to use an SVG and a label inside each option element so the customer can explore the categories more easily.\n\nI left the image alt text empty because I don't want the \"Flowers\" label to be called out twice on screen readers. Since I removed the checkmark, I want to highlight the selected option more prominently. I'll use the checked selector to change the colors.\n\nThis works, but my layout doesn't really fit my window.\n\nThe symbols make the drop-down very long. I need to try something else. With customizable select, it's simpler than ever to bring different layouts to the select drop-down, while reusing the power of other CSS features in my drop-down. Here, I've gone with a grid layout.\n\nGrid-template defines the number of rows and columns, while gap defines the spacing between the grid cells. That puts my drop-down in a nice grid. I think this looks much more organized. I've finished my drop-down, but I realize I now want to have the SVG of the selected option inside the button itself. My symbols were already in the HTML markup, so why aren't they in the button? I need to do one more thing for this to happen. Select comes with a button. That's what people click to open the drop-down. But that button only displays text.\n\nMy image is rich content.\n\nI'm going to use another tool that I get from customizable select to solve that problem: the element.\n\nCustomizable select now lets me replace the built-in button by placing a button element as the first child of the select element. Since my button is currently empty, I only see the arrow.\n\nPutting a button in a select element was previously not allowed in HTML, now it lets me put custom content inside the button, like labels or like the new element. What's special about is that it shows the rich content that's part of my selected option, like my SVG that's next to the \"Everything\" label.\n\nI think Tim is really going to like how this menu looks and works, but I'm not quite done yet. I need to check how this looks in browsers that don't support customizable select.\n\nHere's where progressive enhancement kicks in - it's still usable in browsers that don't support the feature.\n\nCustomers get the native pop-up. This is one of the things that are great about re-using the select element. And because it is a semantic element, I still get those built-in accessibility features.\n\nThis was exciting! My select elements blend in nicely with the look and feel of the site. I even added a fun radial color picker, also built entirely with customizable select. Now, my client has a beautiful, sortable home for their photographs. I also got to take advantage of Safari's support for Grid Lanes to lay out my images.\n\nTo learn more, check out \"Learn CSS Grid Lanes\" where Brandon shows you how this new layout method works.\n\nThese features are coming to Safari 27. If you want to try them now, you can download Safari Technology Preview or Safari Beta.\n\nBe sure to check out the demo on webkit.org, and try styling something simple with customizable select on your own website.\n\nMake sure to test your select with browsers that don't support the technology and with assistive tools. Webkit.org has a blog post to learn more about best practices to help make your interface work for everyone.\n\nFinally, get creative and try experimenting with different ways to implement it. Most importantly, I hope you have fun! I can't wait to see how you use this feature on your website. Thanks for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "1:11",
+ "title": "Basic markup",
+ "language": "swift",
+ "code": "\n"
+ },
+ {
+ "timestamp": "2:37",
+ "title": "Native form control",
+ "language": "swift",
+ "code": "select {\n \n}"
+ },
+ {
+ "timestamp": "2:50",
+ "title": "appearance: base-select",
+ "language": "swift",
+ "code": "body {\n font-family: Gill Sans, sans-serif;\n}\n\nselect {\n appearance: base-select;\n}"
+ },
+ {
+ "timestamp": "3:07",
+ "title": "Style the select button",
+ "language": "swift",
+ "code": "select {\n appearance: base-select;\n background-color: var(--green-10);\n border: none;\n padding: 0.6em 1em;\n}"
+ },
+ {
+ "timestamp": "3:08",
+ "title": "Picker icon",
+ "language": "swift",
+ "code": "select:open {\n background-color: var(--green-100);\n color: white;\n}"
+ },
+ {
+ "timestamp": "3:29",
+ "title": "Picker icon open state",
+ "language": "swift",
+ "code": "select:open {\n background-color: var(--green-100);\n color: white;\n}\n\nselect:open::picker-icon {\n content: url(icons/arrow-white.svg);\n}"
+ },
+ {
+ "timestamp": "4:08",
+ "title": "Picker select",
+ "language": "swift",
+ "code": "::picker(select) {\n\n}"
+ },
+ {
+ "timestamp": "4:21",
+ "title": "Picker select spacing",
+ "language": "swift",
+ "code": "::picker(select) {\n appearance: base-select;\n padding: 4px;\n margin-top: 0.5em;\n}"
+ },
+ {
+ "timestamp": "4:28",
+ "title": "Picker select border and shadow",
+ "language": "swift",
+ "code": "::picker(select) {\n appearance: base-select;\n padding: 4px;\n margin-top: 0.5em;\n border: 1px solid rgba(0,0,0,0.2);\n border-radius: 9px;\n box-shadow: 0 4px 20px rgba(0,0,0,0.2);\n}"
+ },
+ {
+ "timestamp": "4:36",
+ "title": "Custom option styles",
+ "language": "swift",
+ "code": "option:checked {\n font-weight: 600;\n}\n\noption:not(:checked) {\n color: #777;\n}"
+ },
+ {
+ "timestamp": "4:42",
+ "title": "Picker option checkmark",
+ "language": "swift",
+ "code": "option::checkmark {\n content: url(checkmark.svg);\n width: 0.65em;\n}"
+ },
+ {
+ "timestamp": "5:31",
+ "title": "Images in option",
+ "language": "swift",
+ "code": ""
+ },
+ {
+ "timestamp": "5:52",
+ "title": "Custom option highlight",
+ "language": "swift",
+ "code": "option::checkmark {\n display: none;\n}\n\noption:checked {\n background: #00857e;\n color: white;\n}"
+ },
+ {
+ "timestamp": "6:20",
+ "title": "Grid layout in drop downs",
+ "language": "swift",
+ "code": "::picker(select) {\n display: grid;\n grid-template: \n 1fr 1fr / 1fr 1fr 1fr;\n gap: 1rem;\n}"
+ },
+ {
+ "timestamp": "6:43",
+ "title": "Select with image options",
+ "language": "swift",
+ "code": ""
+ },
+ {
+ "timestamp": "7:11",
+ "title": "Select menu",
+ "language": "swift",
+ "code": ""
+ },
+ {
+ "timestamp": "7:13",
+ "title": "Select menu button",
+ "language": "swift",
+ "code": ""
+ },
+ {
+ "timestamp": "7:29",
+ "title": "SelectedContent Element",
+ "language": "swift",
+ "code": ""
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "WebKit.org - Example website demonstrating Customizable Select",
+ "url": "https://webkit.org/demos/customizable-select/"
+ },
+ {
+ "title": "WebKit.org - CSS Grid Lanes Field Guide",
+ "url": "https://gridlanes.webkit.org"
+ },
+ {
+ "title": "WebKit.org – Report issues to the WebKit open-source project",
+ "url": "https://bugs.webkit.org"
+ },
+ {
+ "title": "Submit feedback",
+ "url": "http://feedbackassistant.apple.com"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/315/4/f3bd9835-9ced-4f6a-a0f1-655000972674/downloads/wwdc2026-315_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/315/4/f3bd9835-9ced-4f6a-a0f1-655000972674/downloads/wwdc2026-315_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "314",
+ "year": "2026",
+ "title": "Learn CSS Grid Lanes",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/314"
+ },
+ {
+ "id": "204",
+ "year": "2026",
+ "title": "What’s new in WebKit for Safari 27",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/204"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:20.571Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-319.json b/data/wwdc/videos/2026-319.json
new file mode 100644
index 0000000..71e1814
--- /dev/null
+++ b/data/wwdc/videos/2026-319.json
@@ -0,0 +1,73 @@
+{
+ "id": "319",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/319/",
+ "title": "Build with the new Apple Foundation Model on Private Cloud Compute",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Machine Learning & AI",
+ "System Services"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Louis. In this video, I'll show you how you can access a powerful new server LLM in your apps, using Private Cloud Compute. Last year, we gave you access to a powerful on-device LLM with the new Foundation Models framework. And this year, we've made the on-device LLM even better.\n\nIt now has support for image input, it's better at instruction following and calling your custom tools. But we know there are more complex use cases that require an even more powerful model.\n\nSo this year we're also giving you access to a new server model running on Private Cloud Compute. With this model, you can build complex AI features in your apps. Like assistants that reason over large user input or features that rely on making lots of tool calls, with large outputs, And you can even call Private Cloud Compute from watchOS.\n\nIn this video, we'll go over what Private Cloud Compute is. I'll show you how you can access it from your apps with the Foundation Models framework, and how to handle usage limits.\n\nPrivate Cloud Compute powers our system features, to send complex tasks to Apple's servers. And you now get access to this in your apps as well. That means you can access a powerful server LLM, without compromising on privacy.\n\nPrivate Cloud Compute is designed with end-to-end privacy in mind, ensuring that user data is never stored. The data is only used for requests. And all of this has been independently verified by researchers. But it gets even better. Private Cloud Compute is integrated in the OS, together with iCloud. So you don't have to worry about authentication or API keys, like you typically do with server models. Your users just need a device that supports Apple Intelligence. With no account setup, no authentication and no API keys, this is really the easiest server LLM you'll ever use. And even better, there are no token costs to you, the developer.\n\nEach user gets a daily limit. And users can upgrade to iCloud+ to get higher limits.\n\nThis model is available for apps with less than 2M downloads. And you can apply on the developer website today. So let's take a look at how you can integrate this in your apps, with the Foundation Models framework.\n\nIf you already have an app using Foundation Models, you know that it takes just 3 lines of code to prompt the on-device LLM. You create a session and then ask it to respond to your prompt.\n\nAnd now by changing just 1 line of code, you can switch to the new server model on PCC.\n\nWith just that line, you're now talking to a much larger model, with larger context and more complex reasoning capabilities. The Foundation Models framework offers a unified Swift API, regardless of which model you're talking to.\n\nGetting structured output with Generable, or calling Tools, works just the same with the PCC model, as it does with the on-device model.\n\nThis easily lets you switch between models, without having to rewrite your code.\n\nKeep in mind, just like with the on-device model, PCC is only available on Apple Intelligence devices.\n\nIt's important to check the availability API, and gracefully handle when Apple Intelligence is not available on a user's device.\n\nWhen writing a feature using Foundation Models, deciding which model to use is an important decision. So let's take a look at the differences between the on-device System model and the PCC model.\n\nThey both offer privacy. But the on-device model works offline, while PCC requires an internet connection.\n\nThe on-device model has no request limits, while PCC offers a daily limit per user. Context size is another important factor for some features.\n\nThe on-device model offers 4k, and with PCC you get 32K.\n\nAnd the PCC model supports reasoning.\n\nBut what is reasoning? When an LLM responds to your prompt, it typically just reads the prompt and generates a response.\n\nWith reasoning, the model thinks before it generates the response. This literally happens by letting the model generate extra text, in a separate segment of the transcript.\n\nThe PCC model offers 3 levels of reasoning. Light lets the model gather some extra context. Moderate lets the model reason a little deeper. And with Deep, the text for the reasoning segment may be even longer than the actual response.\n\nYou can set the reasoning level when calling respond on your session.\n\nThe transcript of your session includes the reasoning segment.\n\nYou can observe the transcript to show progress, which is especially useful with the Deep reasoning level, which may take some time.\n\nBut keep in mind, reasoning is extra text that the model generates. So it uses tokens. This counts towards your context size limit.\n\nSpeaking of context size, we also added a convenient API to let you programmatically get the context size for a model. Just access the contextSize property on either SystemLanguageModel or PrivateCloudComputeLanguageModel.\n\nWhen deciding between the on-device and PCC model, or deciding the reasoning level to use, it's good to make that decision based on data, not just vibes. Evaluating let's you understand the quality of your specific feature. You may be surprised how well the on-device model performs at certain tasks, especially with the updated model this year. But the only way to know is by evaluating.\n\nThat's why we created the brand new Evaluations framework. It's a new Swift framework that helps you evaluate your Foundation Models features. It's integrated right in Xcode, and it's easy to get started. You can check out \"Meet the Evaluations framework\" to learn more.\n\nAnd you can even use the on-device and server model together! Check out \"Build agentic app experiences with Foundation Models\" to learn more about that.\n\nWhen using the PCC model in your app, it's important to handle usage limits well. Requests are counted with your user's iCloud account. And you can optimize your app for the case where a user hits a limit. So, let's take a look at how to do that.\n\nHere I have an app that summarizes an article using the PCC model. I can select a markdown file, and we take the text and images, feed that into a LanguageModelSession, and generate a summary.\n\nThis works great with the large context size that PCC offers.\n\nBut when a user hits a limit, the request throws an error. If that error is just shown in the UI, that's not a great user experience, because it's not very actionable. To handle this better, you can check for isLimitReached on the quotaUsage of the model. And handle that with custom UI in your app. Here I'm using a label to go under my button.\n\nAnd when the user's limit is exceeded, you can show a button to let the user manage their limit. For example, a user could upgrade their account to get a higher limit, which would let them make more requests.\n\nYou should integrate this with your existing UI. Avoid showing an alert for the usage limit. Because this UI should persist, and not be dismissed. Instead, you can update the state of your UI, like disabling the button that makes a request. And under that button I'm showing a subtle label, with the button for letting the user get a higher limit, if they want. You can also detect the case where a user is approaching their limit. This can be good to indicate to your users that they are close to their daily limit, so they can make an informed decision for which requests they want to make.\n\nIn Xcode, we have a convenient debug option to simulate the usage limit status. In your scheme, select Debug and then Options.\n\nHere we have the Simulate Apple Foundation Models Availability option.\n\nWe can select Quota Usage Limit Reached, to simulate the case we just handled in our UI.\n\nAnd we can also select Nearing Usage Limit, to simulate the case where the user is close to reaching their daily limit.\n\nWe already handled the isLimitReached case in the code before.\n\nWe can now also test the belowLimit case. Just like with isLimitReached, we can show a simple label.\n\nIn the app, this now shows a label under the button to make a request.\n\nAgain, this contains the actionable button. Now the user can control their limits, even when they're not yet at the maximum. And all this took just a few lines of code. So that was a quick overview of integrating Private Cloud Compute in your apps.\n\nIf you would like to use this new server model in your app, you can apply on the Developer website today.\n\nWe have a ton of other content to tell you all about what's new with Foundation Models and related frameworks. You can start with \"What's new in the Foundation Models framework\", for a great overview. And to better understand what happens with the models at runtime, you can check out \"Debug and profile agentic app experiences with Instruments\". Thanks for watching! Where is that book? I need to bring it out to the library.\n\nNo, really, where is that book?",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "2:49",
+ "title": "Prompt the on-device model",
+ "language": "swift",
+ "code": "import FoundationModels\n\n let session = LanguageModelSession()\n let response = try await session.respond(to: \"Summarize this article: \\(article)\")"
+ },
+ {
+ "timestamp": "3:02",
+ "title": "Switch to the PCC server model (one-line change)",
+ "language": "swift",
+ "code": "import FoundationModels\n \n let session = LanguageModelSession(\n model: PrivateCloudComputeLanguageModel()\n )\n let response = try await session.respond(to: \"Summarize this article: \\(article)\")"
+ },
+ {
+ "timestamp": "3:25",
+ "title": "Structured output and tools work the same",
+ "language": "swift",
+ "code": "import FoundationModels\n\n @Generable\n struct ArticleSummary {\n let oneLineSummary: String\n let keyPoints: [String]\n }\n\n struct FindRelatedArticlesTool: Tool {\n\n }\n \n let session = LanguageModelSession(\n model: PrivateCloudComputeLanguageModel(),\n tools: [FindRelatedArticlesTool.self]\n )\n\n let response = try await session.respond(\n to: \"Summarize this article: \\(article)\",\n generating: ArticleSummary.self\n )"
+ },
+ {
+ "timestamp": "3:51",
+ "title": "Check availability",
+ "language": "swift",
+ "code": "import FoundationModels\n \n struct ArticleSummarizationView: View {\n private var model = PrivateCloudComputeLanguageModel()\n\n var body: some View {\n if model.isAvailable {\n // Show UI for making request\n } else {\n // Fall back\n }\n }\n }"
+ },
+ {
+ "timestamp": "5:26",
+ "title": "Set a reasoning level",
+ "language": "swift",
+ "code": "let response = try await session.respond(\n to: prompt,\n contextOptions: ContextOptions(reasoningLevel: .light)\n )\n // Reasoning levels: .light, .moderate, .deep"
+ },
+ {
+ "timestamp": "5:58",
+ "title": "Read the context size",
+ "language": "swift",
+ "code": "SystemLanguageModel().contextSize\n // 4096 on 26.0\n // 8192 on 27.0 (newer devices)\n\n PrivateCloudComputeLanguageModel().contextSize\n // 32768"
+ },
+ {
+ "timestamp": "9:41",
+ "title": "Handle usage limits",
+ "language": "swift",
+ "code": "struct ArticleSummarizationView: View {\n private var model = PrivateCloudComputeLanguageModel()\n\n var body: some View {\n if case .belowLimit(let info) = model.quotaUsage.status {\n if info.isApproachingLimit {\n Text(\"Nearing usage limit.\")\n .foregroundStyle(Color.orange)\n }\n }\n if model.quotaUsage.isLimitReached {\n Text(\"Usage limit exceeded.\")\n .foregroundStyle(Color.red)\n }\n if let suggestion = model.quotaUsage.limitIncreaseSuggestion {\n Button(\"Show options\") {\n suggestion.show()\n }\n }\n }\n }"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Adding server-side intelligence with Private Cloud Compute",
+ "url": "https://developer.apple.com/documentation/FoundationModels/adding-server-side-intelligence-with-private-cloud-compute"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/319/4/1a3ac4f6-73d2-4a24-9e5d-0cfd56564f42/downloads/wwdc2026-319_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/319/4/1a3ac4f6-73d2-4a24-9e5d-0cfd56564f42/downloads/wwdc2026-319_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:20.353Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-320.json b/data/wwdc/videos/2026-320.json
new file mode 100644
index 0000000..2f715e4
--- /dev/null
+++ b/data/wwdc/videos/2026-320.json
@@ -0,0 +1,179 @@
+{
+ "id": "320",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/320/",
+ "title": "Explore immersive website environments in visionOS",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Safari & Web",
+ "Spatial Computing"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Bonjour, I'm Jean, I'm an Engineer on the visionOS Safari team.\n\nIn this session, I'm going to show you how you can elevate your websites with immersive environments. A whole new way to tell stories to your audience and engage your customers like never before.\n\nTake this ticket sales website.\n\nAs the visitors choose their seats, they get an inline preview of their selection. But more than that, they can enter the theater as an immersive environment, and literally stand at their chosen spot. And all of this from a webpage, in Safari! Well, this is one experience that I'll show you how to build today! But not the only one… Check this out. This is a marketing website for a visionOS escape game app. Transporting visitors into one of the game's escape rooms - a dimly lit chamber, with a TV screen showing a mysterious video. As it plays, it will invite the visitors to discover what's behind this door, encouraging them to download the app and find out for themselves.\n\nIn the next few minutes, you'll learn everything you need to know to start building your first website environment. I'll start by giving you a high-level overview of the immersive API for the web. Then, I'll cover how to preview environments inline in the page. After which, I'll show you how to go immersive and enter the theater from the inline preview, as well as the escape room.\n\nAnd finally, I will guide you through how you can optimize your experience with video, animations, and shadow casting, while maintaining excellent runtime performance.\n\nAlright, let me introduce you to this immersive API.\n\nIt all starts with the HTML model element, which allows you to display 3D models on your website. To use it, you need to specify your 3D asset resource. Here, a USDZ file representing a teapot. And maybe an environment map. Which is a 360-degree image that captures the lighting around your scene. With an environment map, you can add dramatic reflections and lighting on shiny objects like this teapot.\n\nThe 3D model and the environment map assets can be created through a large variety of tools. I'll be using Blender later in the session, but you can use any tool that exports a USDZ file.\n\nIf you later want to know more about this HTML element, you can check out the session \"Get started with the HTML Model Element\" which dives deeper in the element itself and its behaviors on all platforms. Now, let's get to the immersive part! If you're familiar with the JavaScript Fullscreen API, you'll feel right at home. And if you're not, well, you will be soon, because the immersive API follows the already widely used Fullscreen API pattern.\n\nThe same way you'd request a video to go fullscreen through the requestFullscreen JavaScript API, you request a model element to go immersive through the requestImmersive JavaScript API. And very similarly, you can exit the immersive presentation, detect if the feature is available, check if there currently is an immersive element, listen to changes, and error events, and even customize your layout directly in your stylesheet with a CSS pseudo-class.\n\nBut unlike the Fullscreen API, which replaces the web page content with the provided element, the immersive API transports the model element beyond the browser bounds while keeping the page visible. And both APIs can be active simultaneously, so you can have a fullscreen video player while being immersed in a virtual environment. What I find beautiful with this API, is that I can either use it very simply with just one request call on a model element, or use its full depth and combine it with other APIs to create incredible experiences. Like those immersive websites that I've showed you earlier, and in which I'll dive in right now. Starting with the venue ticket sales website, which provides an inline preview of the environment, inline previews are a great way to introduce an environment to visitors as a first step.\n\nSo I'll show you how to create a compelling one now.\n\nHere is my website, running locally on the simulator. When the visitor selects a seat, this model appears, but I left it empty so far. And I'd like to add my Inline preview in there! So, in this empty HTML div, I'll add the model element. I provide the theater model USDZ file, and its lighting map.\n\nThe inline preview now shows the whole theater model from the outside. This is because, by default, the model is scaled to fit the element's bounds. In this case, this not a very interesting view to show, we'd rather show the theater from the inside! So, to get rid of this default fitting behavior, I'll need to customize the entity transform - which controls the model's position, rotation, and scale.\n\nIn my code, after retrieving the model element, and waiting for it to be loaded and ready, I create an identity matrix. And set it to my model's entity transform.\n\nThis effectively removes all transformations from the model.\n\nNow the floor of the model currently sits at the center of the layer. That's because the origin of the theater model is on its floor. But the goal here is to show the environment at a human eye level. So, I'll translate the model down by 1 meter, which is the measured eye level height of my roommate seating on a regular chair.\n\nThis looks better! But right now, this is showing the view from the stage.\n\nWhich, honestly, looks quite nice, but I'd like the preview's point of view to match the actual seat selection. To help with that, I've created a JSON file that maps each seat to its location in the theater.\n\nEach entry contains the translation from the model's origin to the bottom of the seat, as well as an angle representing the seat's orientation towards the stage.\n\nThe values are expressed in the right handed Y-up coordinate system, which is the convention used on the web.\n\nSo back to my website's code, I've created a new function just to build the correct transform.\n\nIt currently contains the translation for previewing the model at eye level. And I can now add the current selected seat as a parameter. And apply the corresponding seat rotation, and translation to match the seat's point of view.\n\nAnd here we go — as a seat is selected, the model shows the point of view from that exact seat's location. And this inline preview also works on other platforms like macOS and iOS! I think it is a really nice experience already. But the real magic truly begins now, when the ticket buyer can step inside the theater! This is where we unlock the full potential of spatial platforms like visionOS, so let's go immersive! First, I'm checking whether the immersive API is available. This property tells me whether the current browser supports immersive presentations. With this checked, I can confidently show my Immersive Preview button, and it will only appear where the feature is actually supported. Then, I'll want to request an immersive transition on the model. This must happen in response to a user interaction. In my case, the tap of the immersive button.\n\nNow, one important thing to note is that an inline model and an immersive model have different reference frames. They have different origins and different scales. When inline, as I just showed you, the origin of the model element is at the center of the inline layer, and the scale is following CSS conventions.\n\nBut when immersive, the origin is at the person's feet, on the floor, and the scale is true to the real world. Additionally, keep in mind that the immersive environment will open from behind Safari's window. So try to keep the main focus of your model visible, without needing to reposition the window.\n\nSo, for my use case, back in the build transform function, I add a second parameter, letting me know whether the model is displayed immersively as opposed to inline in the page.\n\nWhen immersive, I add a slight rotation, so that the stage, which is my main focus here, is not hidden behind Safari's window.\n\nAdditionally, I make sure that the eye level translation is only performed when presenting the model inline in the page.\n\nNow because the entity transform depends on the immersive state of the model, it needs to be updated every time the model goes in and out of immersive.\n\nListening to the immersive change event on the model element is the right way to do this. Here, I check the current document immersive state and re-compute the entity transform with the right flag. And, while I'm here, I'll also update the page layout to reflect the current state. In my case, when going immersive, I'm adjusting the model interface, and displaying an exit button.\n\nIt's always good to present a clear exit affordance for your immersive experiences.\n\nBut also keep in mind that a visitor using an Apple Vision Pro can use the Digital Crown at any time to dismiss the immersive environment.\n\nThat's why, if your UI depends on the immersive state, it's critical to listen to immersive state changes and update your layout accordingly.\n\nAnd just like that, I've built an experience that transports the ticket buyer inside the theater, sitting right in the seat they picked. They can look around, check the view of the stage, lean over the balcony… Oh wow! This is quite high, you don't want to fall down there. Make sure to hold the safety railing……. What?! What happened with the safety railing here? Ugh.. sorry, I'm getting off topic. This experience is way too immersive.\n\nAlright, let's switch gears and dive into something a little more mysterious.\n\nSay you've built a visionOS escape game app. You've poured hours into crafting incredible environments for this game. Well, with the immersive API, you can use those same environment models to create an unforgettable marketing experience, right from your website, in Safari. Let me show you how I built this. I was surprised how little code it takes.\n\nSo here, even though the model element would make it easy to create an inline preview, I prefer to keep the surprise.\n\nWhen adding the model element in my HTML code, I simply set display to none. Doing this, hides the inline layer from the page, but doesn't prevent me from requesting the model as immersive.\n\nAnd there's a practical benefit to hiding the inline preview this way. Indeed, the asset won't be downloaded or decoded until the immersive request actually happens. For heavy environment models, that can save significant bandwidth and memory if the visitor doesn't actually enter the environment.\n\nThe next thing to do, is simply to request the escape room model as immersive on the button's click event.\n\nSince the model isn't pre-loaded inline, the immersive request may take a moment. Especially for heavier and more complex assets.\n\nI'll show you later how you can reduce this loading time by optimizing your asset. But for now, I'll at least give visitors feedback that something is happening, with an intriguing loading animation.\n\nIn the code, I'm showing it before the immersive request, and hiding it once it completed.\n\nAnd that's about it for getting into the environment! Pretty straightforward! Alright, now, I'll talk about the features that make this escape room come alive on visionOS.\n\nAll the little things, that make one simple experience go a long way.\n\nI'll start with videos.\n\nInstead of having your video playing inline in your website. The video docking feature elevates your video, and places it directly inside the environment, on a TV screen, a projector, or a billboard, whatever surface fits your story best.\n\nAdditionally, you can add materials that diffuse, or reflect the light coming from the video. Which makes it feel part of the scene.\n\nTo create this experience, you'll need to add some custom RealityKit annotations to your USDZ file. These are not yet standards, but I'm a big fan of Blender, so I made a little plugin to add those components directly while creating my environment.\n\nHere, I am using it to tag the TV screen, to be the video docking region of my scene. And I'm also using this same Blender extension to facilitate the baking of the video light spill onto my materials.\n\nNow, once I exported my assets with these new properties, the next thing to do, is to request a fullscreen transition on the video. Here, I'm requesting this on the click of the demoButton.\n\nThis automatically transports the video to the right location in my environment, and hides Safari's window! The video is fully integrated into the environment. The light from the TV spills onto the floor and walls, dramatically increasing the realism of the space.\n\nNow, for the twist, let me show you how I can make this mysterious door slide open.\n\nI've created this door opening animation in Blender, right before exporting the model. So really the only thing left to do is play it at the right time.\n\nWith these few lines, I listen for the video's ended event, then exit the fullscreen video, which will undock the video and bring back the website. And finally, I play the model animation.\n\nHere you go, the room transforms, the door slides open, and the mystery keeps on growing.\n\nModel animations can be quite powerful. You could create an entire timeline of animations. And you'd be able use the model's currentTime property to navigate through the timeline, and transform your environment through multiple stages. If you want to know more about that, I'd encourage you to check out the session \"What's new for the spatial web\", which dives a bit deeper into these possibilities.\n\nOk, let's have some fun with just one more thing.\n\nI want to show you how Safari's window is casting its shadow on the environment. I personally find this detail so important, because this shadow helps people understand the positioning of the window inside the space, and makes it feel like part of the environment.\n\nEnabling this also requires a RealityKit annotation. Here, with my same Blender extension, I'm tagging the meshes that should receive these shadows with the Scene Understanding component. I made sure to create a dedicated low poly mesh, optimized just for this, as computing shadows on a complex mesh can consume a lot of resources.\n\nAlright, now I'll end this session with a quick note on performance.\n\nEnvironment models tend to be heavier and more complex to render than simpler object models. Let me show you what you can do to optimize your asset, making it smoother to render and faster to download.\n\nFirst off, reduce your asset vertex count.\n\nIn my escape room, I made sure that I'm not exporting any mesh that would be invisible to the viewer standing at the origin.\n\nDoing this, I've drastically reduced the number of vertices, without anyone noticing.\n\nThen reduce the entity count. In my case, I've merged the desk with all its decorations to avoid having too many separate entities.\n\nUse low poly meshes when appropriate. Like the one I used for the scene understanding component to get shadow casting at a low cost.\n\nKeep your shaders as simple as possible. For the escape room, I've baked all the lighting in the materials. Which means that I painted the light and shadows onto the textures, allowing me to make them unlit materials and skip heavy shading computations at runtime.\n\nFinally, use the usdcrush tool to compress your USDZ's textures. It's available as a command line tool on any Mac, and can help you reduce the size of your model quite a lot, which directly translates to faster loading time for people on a slower connection. I'd recommend to check out the WWDC session, \"Optimize your custom environments for visionOS\" that goes way deeper into 3D asset optimization.\n\nAlright, here we are. You're now ready to create your own experiences. To transport your customers into your environments. With just a model element and a handful of API calls, you can redefine the way your visitors interact with your website. And I've only scratched the surface here. There are so many other APIs you can combine to bring an immersive dimension to your website.\n\nOne of them is the image controls API. By simply adding the controls attribute on my image element here, the browser will offer native controls. Providing a relevant User Interface for platform specific features. Like on visionOS, where this allows people to make this panorama fullscreen, which will wrap it around them in their space. And this also works for spatial photos. Which can be captured directly from your Apple Vision Pro, or your iPhone. Just like with the model element and the immersive API, with image controls, one little thing in your code goes a long way. Creativity is the only limit.\n\nHere's what I encourage you to do next.\n\nTry the online demos on webkit.org with your Apple Vision Pro, there's no better way to understand the impact of the feature than experiencing it yourself.\n\nCreate your first website environment. I have attached resources to this session to further help you, like the API specifications, check them out! And finally, while you'll be building your own experiences, file feature requests or bug reports at bugs.webkit.org. Oh and also, check out this session called \"Design immersive environments for visionOS apps and the spatial web\". It explores the high level principles of creating great photorealistic environments.\n\nI'll personally be on the lookout for what immersive experiences you'll create. Thank you so much for joining this session, hope you enjoyed it! Have a great WWDC!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "1:51",
+ "title": "Basic model element",
+ "language": "swift",
+ "code": "\n"
+ },
+ {
+ "timestamp": "2:06",
+ "title": "Model element with environment map",
+ "language": "swift",
+ "code": "\n"
+ },
+ {
+ "timestamp": "4:40",
+ "title": "Adding the environment model on the page for inline preview",
+ "language": "swift",
+ "code": "
\n\t\n\t\n
"
+ },
+ {
+ "timestamp": "5:14",
+ "title": "Reset the model entity transform",
+ "language": "swift",
+ "code": "const theater = document.getElementById(\"theater\");\n\nasync function updateModelTransform() {\n\t// Make sure the model is loaded\n\tawait theater.ready;\n\t// Create a transform matrix\n\tconst identity = new DOMMatrix();\n\t// Apply the transform matrix to the model\n\ttheater.entityTransform = identity;\n}\n\nupdateModelTransform();"
+ },
+ {
+ "timestamp": "5:42",
+ "title": "Translate the model down",
+ "language": "swift",
+ "code": "const theater = document.getElementById(\"theater\");\n\nasync function updateModelTransform() {\n\t// Make sure the model is loaded\n\tawait theater.ready;\n\t// Create a transform matrix\n\tconst transform = new DOMMatrix();\n\t// Translate model down, for eye level preview\n\ttransform.translateSelf(\n\t\t0, \t\t\t// x\n\t\t-1.0, \t// y\n\t\t0 \t\t\t// z\n\t);\n\t// Apply the transform matrix to the model\n\ttheater.entityTransform = transform;\n}\n\nupdateModelTransform();"
+ },
+ {
+ "timestamp": "6:40",
+ "title": "Build the seat transform",
+ "language": "swift",
+ "code": "function buildTransform(seat) {\n\tconst transform = new DOMMatrix();\n\tconst { x, y, z, ry } = seat;\n\t// Rotate and translate the model to match \n // the seat's origin and orientation\n\ttransform.rotateSelf(0, -ry, 0);\n\ttransform.translateSelf(-x, -y, -z);\n\t// Translate the model down, for eye level preview\n\ttransform.translateSelf(0, -1.0, 0);\n\treturn transform;\n}"
+ },
+ {
+ "timestamp": "7:16",
+ "title": "Detect feature availability",
+ "language": "swift",
+ "code": "if (document.immersiveEnabled) {\n\timmersiveButton.hidden = false;\n}"
+ },
+ {
+ "timestamp": "7:34",
+ "title": "Request the immersive transition on the model",
+ "language": "swift",
+ "code": "immersiveButton.addEventListener(\"click\", async () => {\n\tawait model.requestImmersive();\n});"
+ },
+ {
+ "timestamp": "8:24",
+ "title": "Build immersive transform",
+ "language": "swift",
+ "code": "function buildTransform(seat, immersive) {\n\tconst transform = new DOMMatrix();\n\t// [...] Seat transform logic\n\tif (immersive) {\n\t\t// Rotate to the left\n\t\ttransform.rotateSelf(\n\t\t\t0,\t\t// x\n\t\t\t45,\t\t// y\n\t\t\t0\t\t\t// z\n\t\t);\n\t} else {\n\t\t// [...] Eye level translation\n\t}\n\treturn transform;\n}"
+ },
+ {
+ "timestamp": "9:01",
+ "title": "Update the entity transform and the layout on immersive state updates",
+ "language": "swift",
+ "code": "theater.addEventListener(\"immersivechange\", () => {\n\tconst isImmersive = !!document.immersiveElement;\n\tconst transform = buildTransform(isImmersive, currentSeat);\n\ttheater.entityTransform = transform;\n document.body.classList.toggle(\"immersive\", isImmersive);\n});"
+ },
+ {
+ "timestamp": "10:53",
+ "title": "Hide the inline preview",
+ "language": "swift",
+ "code": "\n"
+ },
+ {
+ "timestamp": "11:25",
+ "title": "Request an immersive transition on the escape room model",
+ "language": "swift",
+ "code": "const enterButton = document.getElementById(\"enterButton\");\nconst escapeRoom = document.getElementById(\"escapeRoom\");\n\nenterButton.addEventListener(\"click\", () => {\n await escapeRoom.requestImmersive();\n});"
+ },
+ {
+ "timestamp": "11:52",
+ "title": "Handle the request result and show a loading animation",
+ "language": "swift",
+ "code": "enterButton.addEventListener(\"click\", async () => {\n\tshowLoadingAnimation(); \n\ttry {\n\t\tawait escapeRoom.requestImmersive();\n\t} catch (error) {\n\t\tconsole.log(error);\n\t} finally {\n\t\thideLoadingAnimation();\n\t}\n});"
+ },
+ {
+ "timestamp": "13:16",
+ "title": "Dock the video in the environment with the fullscreen API",
+ "language": "swift",
+ "code": "const trailerVideo = document.getElementById(\"trailerVideo\");\nconst demoButton = document.getElementById(\"demoButton\");\n\ndemoButton.addEventListener(\"click\", async () => {\n\tawait trailerVideo.requestFullscreen();\n});"
+ },
+ {
+ "timestamp": "14:01",
+ "title": "Play the model animation",
+ "language": "swift",
+ "code": "const trailerVideo = document.getElementById(\"trailerVideo\");\nconst escapeRoom = document.getElementById(\"escapeRoom\");\n\ntrailerVideo.addEventListener(\"ended\", async () => {\n\tawait document.exitFullscreen();\n\tescapeRoom.play();\n});"
+ },
+ {
+ "timestamp": "16:38",
+ "title": "Compress your USDZ with usdcrush",
+ "language": "swift",
+ "code": "usdcrush model.usdz -o optimized.usdz"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Download - Immersive model add-on for Blender",
+ "url": "https://developer.apple.com/download/files/web-env-blender-plugin.zip"
+ },
+ {
+ "title": "WebKit.org - Theater Ticket Sales immersive website environment demo for Apple Vision Pro",
+ "url": "https://webkit.org/demos/model-demos/ticket-sales.html"
+ },
+ {
+ "title": "WebKit.org - Escape Game immersive website demo for Apple Vision Pro",
+ "url": "https://webkit.org/demos/model-demos/escape-room.html"
+ },
+ {
+ "title": "GitHub: Spatial Backdrop explainer",
+ "url": "https://github.com/WebKit/explainers/tree/main/spatial-backdrop"
+ },
+ {
+ "title": "WebKit.org – Report issues to the WebKit open-source project",
+ "url": "https://bugs.webkit.org"
+ },
+ {
+ "title": "Submit feedback",
+ "url": "http://feedbackassistant.apple.com"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/320/4/e1844891-477b-4612-ad8d-10e55bf395ba/downloads/wwdc2026-320_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/320/4/e1844891-477b-4612-ad8d-10e55bf395ba/downloads/wwdc2026-320_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "234",
+ "year": "2026",
+ "title": "Design immersive environments for visionOS apps and the spatial web",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/234"
+ },
+ {
+ "id": "215",
+ "year": "2026",
+ "title": "Get started with the HTML Model Element",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/215"
+ },
+ {
+ "id": "204",
+ "year": "2026",
+ "title": "What’s new in WebKit for Safari 27",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/204"
+ },
+ {
+ "id": "305",
+ "year": "2025",
+ "title": "Optimize your custom environments for visionOS",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/305"
+ },
+ {
+ "id": "237",
+ "year": "2025",
+ "title": "What’s new for the spatial web",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/237"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:20.862Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-321.json b/data/wwdc/videos/2026-321.json
new file mode 100644
index 0000000..ecb0633
--- /dev/null
+++ b/data/wwdc/videos/2026-321.json
@@ -0,0 +1,201 @@
+{
+ "id": "321",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/321/",
+ "title": "Dive into lazy stacks and scrolling with SwiftUI",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Design",
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, my name is Rens, and I'm a UI Frameworks Engineer.\n\nLazy stacks are an essential component for any SwiftUI app showing long and custom scrolling content. And they have long been a part of SwiftUI. Like many other SwiftUI components, the power of lazy stacks comes from their simplicity. Different SwiftUI components can be mixed with many other SwiftUI components to build complex apps. For example, from the 2027 releases you can use reorderable to drag and reorder views. You can learn more about that in \"Code-along: Build powerful drag and drop in SwiftUI\".\n\nAnd SwiftUI allows swipe actions to be added on views outside list. Of course, both work great when used with lazy stacks. I think it's a good time for a refresh and dive into lazy stacks and scrolling. I'll explain how they work, what you can do with them, and what you may want to avoid. Afterwards, you will have a better understanding of the internals of lazy stacks stacks, that you'll be able to apply to lazy stacks in your own apps.\n\nThis video will assume basic familiarity with SwiftUI layout using stacks. If you're new to SwiftUI, I recommend \"Stacks, Grids, and Outlines in SwiftUI\".\n\nI've been working on an Origami app that shows the instructions to make some popular origami pieces.\n\nIn this early version, it's only showing the steps to make a swan. Here is the set-up for the main view. I have a ScrollView, with a LazyVStack inside, that in turn contains a StepView for each step.\n\nThe lazy stack allows scrolling through a potentially large number of steps, without loading all views at once immediately. I'll now focus on the LazyVStack.\n\nThree of the steps to create an origami swan are fully visible. A small part of the StepView for step 4 is also visible.\n\nNow, the full LazyVStack is a lot larger than the visible views. But, unlike a VStack, a LazyVStack does not evaluate or render views that aren't visible. A LazyVStack simply lays out its views top to bottom, and stops once the visible rect is filled. If you scroll down the LazyVStack adds views as appropriate to make sure the visible rect remains filled, and as views are scrolled out of screen, they are removed from the lazy stack.\n\nBy not loading all views at once, a LazyVStack can be more efficient than a VStack. But there is a correctness cost. Since a LazyVStack doesn't load all of its views, the height of the subviews that are off-screen are estimated. This estimated height is based on the average size of views that have been placed before, and the estimated number of remaining subviews. The lazy stack is also unaware of changes in off-screen views, since they aren't loaded.\n\nSimilarly, since not all views are loaded, it wouldn't be able to find the maximum width of all views. So the ideal width of a LazyVStack is that of its first subview. In the case of my origami app, the first view is infinitely flexible, so the width of the LazyVStack equals the screen width.\n\nSince the height of the LazyVStack is estimated and not precise, it can change during scrolling as the lazy stack learns more about the layout of new views scrolled onto screen. For example, if you scroll down all the way and the last views are a bit smaller than the other views the lazy stack has to adjust its originally estimated size to account for this.\n\nThe space above the visible rect isn't precise either. The scroll position, or content offset of the scroll view, therefore depends on an estimated position of the visible items. One example where the space above the visible region is not precise, is after an orientation change on an iPhone.\n\nStepViews are less tall in landscape than they are in portrait. The subtitle text generally fits on fewer lines in landscape. During the orientation change, the lazy stack will keep the StepView for step 4, the topmost visible view, anchored. The LazyVStack isn't yet aware of the exact layout changes in the first few StepViews, since they aren't loaded. But when scrolling all the way back up, the lazy stack must align to the top of the scroll view. This means it must correct the estimated space above the visible region along the way. It will update the content offset of the ScrollView with the same amount, such that the content offset at the top is zero as well.\n\nThe lazy stack and the embedding scroll view coordinate the position and content offset. That way, when the estimations are updated, the relative position of the visible subviews in the scroll view doesn't change.\n\nIt's common to compose different types of views or content in the same lazy stack. For my origami app, I think it could be cool if people could share a photo of their creation with others when they are done. I'd like to display these photos at the bottom, in a horizontal scroll view. I've added a Showcase view for these photos.\n\nThe Showcase view has a horizontally scrolling ScrollView, with a LazyHStack within.\n\nThis means that my app now has a LazyHStack nested inside the outer LazyVStack.\n\nNesting a LazyHStack in a LazyVStack like this can also be good for performance, since not everyone will scroll this nested scroll view to see the extra views.\n\nFor a LazyHStack, the ideal height, and therefore, its height in a vertical ScrollView, is that of the first subview. For my origami app, all photos use the same height. But if every photo had a user description label with a variable number of lines, longer subtitles would be cut off. The LazyHStack cannot know in advance what the largest subtitle of all views is. It hasn't loaded all of them. The best solution is to fix the view heights. For example, for text, you can set a line limit, and reserve space for shorter text.\n\nBut I'm actually thinking the photos in my origami app, should be a bit larger. Maybe I should just add them vertically below the steps. If I add them in a section, I could even pin the section header.\n\nTo pin section headers, use the pinnedViews parameter on the LazyVStack.\n\nI've added the new Section inside the Showcase view, with a header view. If I scroll down, the Showcase section header sticks to the top.\n\nI'll now discuss some patterns to avoid so the lazy stacks perform at their best. I'll do that using the photos showcase I've just added. It could be nice to add a scroll transition to the photos as they scroll into and out of screen.\n\nHere, I'm using the .scrollTransition modifier to give my steps an effect as they scroll on or off-screen. However, lazy stacks only load views that are on screen, based on their original position. And the transform here is pushing them out of their original frame.\n\nThat causes them to disappear when they should be visible, since the lazy stack believes they are off-screen. Here, as you scroll down, the pink swan disappears too soon.\n\nIf you apply a scroll transition to views in a lazy stack, make sure that views that wouldn't be normally visible aren't pushed into the visible rect. Here, I'm using a different scale effect.\n\nThis works fine. In general, make sure that views that wouldn't normally be visible aren't pushed into the visible rect in a transform. The lazy stack won't be aware of that.\n\nSince you need to scroll down a little to get to the Showcase, I'll also add a button to quickly scroll there.\n\nBut the button shouldn't always be visible. I'd like it to only be visible when near the top of the scroll view.\n\nWhen someone scrolls down, it should disappear.\n\nHere, I'm using an .onScrollGeometryChange on my scroll view to get the absolute content offset.\n\nWhen I'm scrolled down more than a 100 points, the button disappears.\n\nThis works, but since the content offset of a lazy stack is estimated, the exact position where the buttons disappears, can change when the estimations change. Instead, it's better to use the relative positions of subviews in the visible region of the scroll view.\n\nOne way to do that, is to use the .onScrollTargetVisibilityChange modifier.\n\nThe closure in that modifier is called when the visibility of the subviews in the visible region of the scroll view changes. Here, the visibility of the \"Scroll to Showcase\" button depends only on which subviews are visible, with a threshold of 80%.\n\nI've now covered the layout of lazy stacks in detail. I've said that lazy stacks add subviews, when they are about to enter the visible part of the scroll view. However, the subviews a lazy stack loads individually, do not always correspond directly to the view structs you define in code. Let's go back to my original code and take another look at the ContentView. In this simple case, there is a 1-to-1 correspondence of StepView instances and the subviews that the LazyVStack sees.\n\nThere's a ScrollView, and the ScrollView has a LazyVStack as a subview, and the LazyVStack has the ForEach as a subview. But of course the ForEach isn't just a single view.\n\nIt's resolved to one StepView for every step. And in most cases, these are the subviews that the LazyVStack loads.\n\nBut here, the StepView is slightly more complicated. And that will be important. The body contains two views, StepDiagram and StepInstructions, at the top level of the body. They're also not embedded in another layout, like a VStack.\n\nIn this case, LazyVStack still has the ForEach which is resolved to a StepView for every step. But just like the ForEach resolves to multiple StepViews, each StepView now resolves to two views as well. The LazyVStack evaluates and loads StepDiagram and StepInstructions seperately.\n\nOf course, StepView still needs to be evaluated for the lazy stack to create either of those. Views can also resolve to a dynamic number of views. But that is something you have to watch out for. In this case, StepView is using a detailLevel environment value, to check whether it should be visible.\n\nThe ForEach again resolves to a StepView for each step. But each StepView now resolves to either one subview or zero subviews. In this case, step 2 isn't visible given the current detail level, but the first and third steps are.\n\nThat works, and the contents of the StepView are loaded lazily, but the StepView itself can be kept alive longer than you may expect.\n\nThat is because a LazyVStack addresses the visible subviews using their index.\n\nIt now has to keep earlier StepViews around, just in case the detailLevel environment value changes, because that would affect the indices.\n\nIn leaf subviews that are created many times in a ForEach, like StepView, avoid creating a dynamic number of subviews.\n\nThe example where a detailLevel environment value is used to filter out steps, is therefore not a good idea.\n\nSay that unrelated environment value, like writingStyle, is used in the contents of the StepView body. A change in this environment value can now cause body evaluations for views that are scrolled out of screen, causing unnecessary view updates. The lazy stack also won't release state allocated for the StepView. Instead, filter at the data level. If you're using SwiftData, use a Predicate to filter your Query.\n\nHere, I'm using the detailLevel in the Predicate. This makes the number of subviews immediately clear to the LazyVStack. It doesn't have to construct views to compute view counts or indices.\n\nNote that unwrapping an optional in a view body has the same effect. Here, I'm optionally unwrapping an apiToken Environment variable. The body only returns something if that token isn't nil.\n\nThe token is something that could be handled by a NetworkClient model object.\n\nIf someone is not authenticated, a view higher up in the hierarchy could show a ContentUnavailableView, instead of showing the lazy stack in the first place.\n\nSince lazy stacks only keep a small part of their data in memory, they do not need to perform a full diff of their contents. They only perform a minimal check for changes in the visible views.\n\nLazy stacks don't always load a subview all at once. I'll now discuss prefetching, an internal mechanism with which lazy stacks improve the scrolling performance of your apps.\n\nWhen you scroll in a specific direction, and the visible part of the lazy stack reaches the end of the placed content, the lazy stack already prefetches views before adding them on screen. Prefetching means that lazy stacks will perform part of the work of displaying a view before it's visible.\n\nWhile scrolling, a ScrollView needs to draw frames at constant rates. That means there is only a limited time available to perform computations, up to a frame deadline. This work includes the ScrollView updating the content offset, your views rendering at a new position, and work your app may do in response to the content offset change. And when the ScrollView contains a lazy stack, it includes the work for evaluating views scrolled on screen, performing the layout, and rendering them. But, the work for placing new views on screen can be expensive.\n\nIf the work would take too long, passing the deadline, that would result in a dropped frame. That is visible as a hitch while scrolling so that should be avoided.\n\nPrefetching is used to prevent such dropped frames. While scrolling, the lazy stack already checks if there is enough time available to perform part of the work of rendering a new subview, before it is scrolled on screen.\n\nFor example, a lazy stack may be able to evaluate the body and layout of a view about to appear on screen, before it appears.\n\nWhen the view finally does appear, most of the work has already been performed, broken up across multiple frames.\n\nThe work to show a nested LazyHStack in a LazyVStack can be broken up across multiple frames as well. When the view appears, onAppear is called. So generally your view's body is called at one point, and onAppear only a little later, when the view is placed on the screen. If the scroll direction is reversed, it's even possible that the view's body is called as part of prefetching, and onAppear is never called.\n\nUsing onAppear in a lazy stack is useful for a number of things, even for data loading. One such use case is infinite scrolling. Here, the origami app fetches more photos from the web, when you scroll to the end. The last view, a ProgressView, has an .onAppear modifier. When that view appears, a new page is fetched.\n\nBut, loading everything in onAppear for each view is not a good idea.\n\nIn this example, onAppear is used to set-up every view. The size and large parts of the view's contents completely change after it's placed.\n\nThe work that prefetching has done earlier will be thrown away, and has to be re-done when the view appears. The lazy stack may also load more views than needed, and scrolling can be affected, as I'll show later.\n\nInstead, set-up the view in the initializer such that it is in a reasonable state before it appears on screen.\n\nEven when it's not essential, it can be useful to load content before views appear.\n\nHere, I'm using the task modifier to remotely load a diagram from the internet when the view appears. But I can actually make use of prefetching to load it slightly earlier, so the chance is higher it's loaded by the time it appears.\n\nFor example, I could use a DiagramLoader observable object, connected to a cache. When the cache doesn't contain the data for a specific ID, it could load the data immediately when it's initialized. Since it starts loading the diagram in the initializer, the diagram will be fetched slightly earlier. Views that are scrolled out of screen aren't rendered or updated anymore. But they aren't removed from memory immediately. Lazy stacks keep these around for a number of updates, in case they are scrolled back on screen. When views finally are deleted from memory, state variables are deleted alongside.\n\nSince the data associated with views scrolled out of screen will be deleted, don't depend on view state for data that needs to be kept alive after scrolling. Here, StepView uses an isHighlighted state variable. But if the view is scrolled away, that highlight state will be lost.\n\nInstead, move important state to model objects, or outer views using a binding, as here.\n\nYou typically use lazy stacks inside a scroll view. I'd now like to give you some tips to make scrolling work well in lazy stacks. Earlier, I added a button to my origami app, to scroll to the showcase with user photos.\n\nThe code to programmatically scroll to the section would look like this. I'm using a ScrollPosition binding to scroll to the showcase's section header.\n\nProgrammatic scrolling works in lazy stacks, even if the target view isn't on screen.\n\nScrolling to an off-screen view requires the lazy stack to estimate its position. In an animated scroll, the lazy stack updates this estimated position on every frame. Still, there are some things that can prevent scrolling from being smooth and fast. For example, also here, having a dynamic number of views in StepView has a performance impact. Programmatic scrolling to a view with an ID is most performant if each view in your ForEach always resolves to one single subview. In that case, the lazy stack can query the ForEach to find the ID to scroll to, without constructing any of the views. Scrolling to a subview near the end is also more performant, if the lazy stack can quickly count its subviews.\n\nAs before, instead of filtering out views with a conditional in the view body, you should filter on the data level, for example, with a predicate on a Query.\n\nProgrammatic scrolling also becomes less smooth, if too many views change their layout after they appear on screen. A common pattern that does this, is using onGeometryChange, to set a state value that is then used in another layout pass.\n\nHere, StepView has a state variable subtitleHeight, updated in an onGeometryChange on the subtitle.\n\nThe view is then evaluated again, and subtitleHeight is used to compute the frame of the diagram. This makes scrolling less reliable. The lazy stack measures the view's original height, but the height changes after the view appears, pushing down other content.\n\nIn cases like this, if you cannot use SwiftUI's layout primitives, use a custom layout instead. Here, that custom layout is StepLayout. To learn more about using custom layouts check out \"Compose custom layouts with SwiftUI\".\n\nAlright, I've shown you many aspects of lazy stacks. I've talked about their layout, how views structs don't always resolve to a single subview and how that affects lazy stacks, how lazy stacks prefetch views for better scrolling performance, and how they allow programmatic scrolling to views off-screen. Along the way, I've given some tips and best practices that you can use for lazy stacks in your apps. For example, avoid using the absolute content size or content offset with lazy stacks, since these are estimated and unstable.\n\nAvoid using conditional view content in leaf views to filter out data, as that can cause SwiftUI views to stay alive longer than expected.\n\nSet up your lazy stack's subviews before onAppear is called where possible to ensure prefetching works best. And don't change the layout of subviews of lazy stacks after they appear, as that can push the lazy stack out of the targeted scroll position.\n\nUnderstanding some of the mechanisms with which SwiftUI components work helps you excel at using them. And I think, after preparing this video, I now excel at creating swans.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "1:23",
+ "title": "Origami app",
+ "language": "swift",
+ "code": "// Origami app\n\nstruct ContentView: View {\n var body: some View {\n ScrollView {\n LazyVStack {\n ForEach(steps) { step in\n StepView(step: step)\n }\n }\n }\n }\n}\n\nstruct StepView: View { /* ... */ }"
+ },
+ {
+ "timestamp": "5:11",
+ "title": "Horizontally scrolling showcase",
+ "language": "swift",
+ "code": "// Horizontally scrolling showcase\n\nstruct ContentView: View {\n var body: some View {\n ScrollView {\n LazyVStack {\n ForEach(steps) { step in\n StepView(step: step)\n }\n Showcase()\n }\n }\n }\n}\n\nstruct StepView: View { /* ... */ }\n\nstruct Showcase: View {\n var body: some View {\n ScrollView(.horizontal) {\n LazyHStack {\n ForEach(photos) { photo in\n PhotoView(photo: photo)\n }\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "6:30",
+ "title": "Showcase section",
+ "language": "swift",
+ "code": "// Showcase section\n\nstruct ContentView: View {\n var body: some View {\n ScrollView {\n LazyVStack(pinnedViews: [.sectionHeaders]) {\n ForEach(steps) { step in\n StepView(step: step)\n }\n Showcase()\n }\n }\n }\n}\n\nstruct StepView: View { /* ... */ }\n \nstruct Showcase: View {\n var body: some View {\n Section {\n ForEach(photos) { photo in\n PhotoView(photo: photo)\n }\n } header: { /* ... */ }\n }\n}"
+ },
+ {
+ "timestamp": "7:04",
+ "title": "Scroll effect",
+ "language": "swift",
+ "code": "// Scroll effect\n\nstruct ContentView: View { /* ... */ }\n\nstruct StepView: View { /* ... */ }\n\nstruct Showcase: View {\n var body: some View {\n Section {\n ForEach(photos) { photo in\n PhotoView(photo: photo)\n .scrollTransition { effect, phase in\n effect\n .rotationEffect(.degrees(phase.value * 20))\n .scaleEffect(1 + phase.value * 0.2)\n }\n }\n } header: { /* ... */ }\n }\n}"
+ },
+ {
+ "timestamp": "7:36",
+ "title": "Scroll effect",
+ "language": "swift",
+ "code": "// Scroll effect\n\nstruct ContentView: View { /* ... */ }\n\nstruct StepView: View { /* ... */ }\n\nstruct Showcase: View {\n var body: some View {\n Section {\n ForEach(photos) { photo in\n PhotoView(photo: photo)\n .scrollTransition { effect, phase in\n effect\n .scaleEffect(1 - abs(phase.value) * 0.1)\n }\n }\n } header: { /* ... */ }\n }\n}"
+ },
+ {
+ "timestamp": "8:20",
+ "title": "Scroll to Showcase button",
+ "language": "swift",
+ "code": "// Absolute offset\n\nstruct ContentView: View {\n @State var isScrollToShowcaseVisible = false\n\n var body: some View {\n ScrollView { /* ... */ }\n .overlay(alignment: .bottom) { /* ... */ }\n .onScrollGeometryChange(for: Bool.self) { geo in\n geo.contentOffset.y <= 100\n } action: { _, newValue in\n self.isScrollToShowcaseVisible = newValue\n }\n }\n}"
+ },
+ {
+ "timestamp": "8:51",
+ "title": "Scroll to Showcase button",
+ "language": "swift",
+ "code": "// Absolute offset\n\nstruct ContentView: View {\n @State var isScrollToShowcaseVisible = false\n\n var body: some View {\n ScrollView { /* ... */ }\n .overlay(alignment: .bottom) { /* ... */ }\n .onScrollTargetVisibilityChange(\n idType: Step.ID.self,\n threshold: 0.8\n ) { visibleIDs in\n isScrollToShowcaseVisible = shouldShowScrollButton(visibleIDs: visibleIDs)\n }\n }\n}"
+ },
+ {
+ "timestamp": "9:29",
+ "title": "One resolved subview",
+ "language": "swift",
+ "code": "// Origami\n\nstruct ContentView: View {\n var body: some View {\n ScrollView {\n LazyVStack {\n ForEach(steps) { step in\n StepView(step: step)\n }\n }\n }\n }\n}\n\nstruct StepView: View { /* ... */ }"
+ },
+ {
+ "timestamp": "10:03",
+ "title": "Multiple resolved subviews",
+ "language": "swift",
+ "code": "// Multiple subviews\n\nstruct ContentView: View { /* ... */ }\n\nstruct StepView: View {\n let step: Step\n\n var body: some View {\n StepDiagram(/* ... */)\n StepInstructions(/* ... */)\n }\n}"
+ },
+ {
+ "timestamp": "10:52",
+ "title": "Dynamic number of subviews",
+ "language": "swift",
+ "code": "// Dynamic number of views\n\nstruct ContentView: View { /* ... */ }\n\nstruct StepView: View {\n let step: Step\n\n @Environment(\\.detailLevel) var detailLevel\n\n var body: some View {\n if step.isVisible(in: detailLevel) {\n VStack { /* ... */ }\n }\n }\n}"
+ },
+ {
+ "timestamp": "11:46",
+ "title": "Filtering on the view level",
+ "language": "swift",
+ "code": "// Dynamic number of views\n\nstruct ContentView: View { /* ... */ }\n\nstruct StepView: View {\n let step: Step\n\n @Environment(\\.detailLevel) var detailLevel\n @Environment(\\.writingStyle) var writingStyle\n\n var body: some View {\n if step.isVisible(in: detailLevel) { /* ... */ }\n }\n}"
+ },
+ {
+ "timestamp": "12:15",
+ "title": "Filtering on the data level",
+ "language": "swift",
+ "code": "// Filter at the data level\n\nstruct ContentView: View {\n @Query var steps: [Step]\n\n init(detailLevel: DetailLevel) {\n _steps = Query(filter: #Predicate { step in\n step.detailLevel >= detailLevel\n })\n }\n\n var body: some View { /* ... */ }\n}\n\nstruct StepView: View { /* ... */ }"
+ },
+ {
+ "timestamp": "12:35",
+ "title": "Optional unwrapping",
+ "language": "swift",
+ "code": "// Optional unwrapping\n\nstruct ContentView: View { /* ... */ }\n\nstruct StepView: View {\n let step: Step\n\n @Environment(\\.apiToken) var token\n\n var body: some View {\n if let token { /* ... */ }\n }\n}"
+ },
+ {
+ "timestamp": "12:48",
+ "title": "Optional unwrapping",
+ "language": "swift",
+ "code": "// Optional unwrapping\n\nstruct ContentView: View { /* ... */ }\n\nstruct StepView: View {\n let step: Step\n\n @Environment(NetworkClient.self) var networkClient\n\n var body: some View { /* ... */ }\n}"
+ },
+ {
+ "timestamp": "15:28",
+ "title": "Loading more content",
+ "language": "swift",
+ "code": "// Loading more content\n\nstruct Showcase: View {\n @State var pager = ShowcasePager()\n\n var body: some View {\n ForEach(pager.pages) { page in\n PageView(page: page)\n }\n if !pager.atEnd {\n ProgressView()\n .progressViewStyle(.circular)\n .onAppear {\n pager.fetchPage()\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "15:53",
+ "title": "Setting up lazy stack subview in onAppear",
+ "language": "swift",
+ "code": "// onAppear\n\nstruct StepView: View {\n let id: Step.ID\n @State var viewModel = StepViewModel()\n\n var body: some View {\n VStack {\n if let content = viewModel.content { /* ... */ }\n }\n .onAppear {\n viewModel.configure(with: id)\n }\n }\n}"
+ },
+ {
+ "timestamp": "16:14",
+ "title": "Lazy stack subview ready before onAppear",
+ "language": "swift",
+ "code": "// onAppear\n\nstruct StepView: View {\n @State var viewModel: StepViewModel\n\n init(id: Step.ID) {\n _viewModel = State(initialValue: StepViewModel(id: id))\n }\n\n var body: some View { /* ... */ }\n}"
+ },
+ {
+ "timestamp": "16:23",
+ "title": "Loading diagram with task modifier",
+ "language": "swift",
+ "code": "// Diagram loading\n\nstruct StepView: View {\n let step: Step\n @State var diagramLoader = DiagramLoader()\n\n @State var diagram: Diagram?\n\n var body: some View {\n VStack { /* ... */ }\n .task {\n diagram = await diagramLoader.loadDiagram(id: step.id)\n }\n }\n}"
+ },
+ {
+ "timestamp": "16:40",
+ "title": "Loading diagram in initializer",
+ "language": "swift",
+ "code": "// Diagram loading\n\nstruct StepView: View {\n let step: Step\n @State var diagramLoader: DiagramLoader\n\n init(step: Step) {\n self.step = step\n _diagramLoader = State(initialValue: DiagramLoader(id: step.id))\n }\n\n var body: some View { /* ... */ }\n}\n\n@Observable\nclass DiagramLoader { /* ... */ }"
+ },
+ {
+ "timestamp": "17:16",
+ "title": "Highlight @State variable",
+ "language": "swift",
+ "code": "// Highlighting\n\nstruct ContentView: View { /* ... */ }\n\nstruct StepView: View {\n let step: Step\n @State var isHighlighted = false\n\n var body: some View { /* ... */ }\n}"
+ },
+ {
+ "timestamp": "17:33",
+ "title": "Highlight @Binding",
+ "language": "swift",
+ "code": "// Highlighting\n\nstruct ContentView: View {\n @State var highlighted: Set = []\n\n var body: some View { /* ... */ }\n}\n\nstruct StepView: View {\n let step: Step\n @Binding var highlighted: Set\n\n var body: some View { /* ... */ }\n}"
+ },
+ {
+ "timestamp": "17:58",
+ "title": "Programmatically scroll to showcase",
+ "language": "swift",
+ "code": "// Programmatically scroll to showcase\n\nstruct ContentView: View {\n @State var scrollPosition = ScrollPosition()\n\n var body: some View {\n ScrollView { /* ... */ }\n .scrollPosition($scrollPosition)\n .overlay(alignment: .bottom) {\n Button {\n scrollToShowcase()\n } label: { /* ... */ }\n }\n }\n\n func scrollToShowcase() {\n withAnimation {\n scrollPosition.scrollTo(id: \"showcase-header\")\n }\n }\n}"
+ },
+ {
+ "timestamp": "18:24",
+ "title": "Dynamic number of views",
+ "language": "swift",
+ "code": "// Dynamic number of views\n\nstruct ContentView: View { /* ... */ }\n\nstruct StepView: View {\n let step: Step\n\n @Environment(\\.detailLevel) var detailLevel\n\n var body: some View {\n if step.isVisible(in: detailLevel) { /* ... */ }\n }\n}"
+ },
+ {
+ "timestamp": "19:16",
+ "title": "Using onGeometryChange in lazy stack subview",
+ "language": "swift",
+ "code": "// Don't change layout after views appear\n\nstruct ContentView: View { /* ... */ }\n\nstruct StepView: View {\n let step: Step\n @State var subtitleHeight: CGFloat?\n\n var body: some View {\n VStack {\n StepDiagram(diagram: step.diagram)\n .frame(height: diagramHeight(subtitleHeight: subtitleHeight))\n Title(step.title)\n Subtitle(step.subtitle)\n .onGeometryChange(for: CGFloat.self, of: \\.size.height) { _, value in\n subtitleHeight = value\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "19:17",
+ "title": "Using custom layout in lazy stack subview",
+ "language": "swift",
+ "code": "// Don't change layout after views appear\n\nstruct ContentView: View { /* ... */ }\n\nstruct StepView: View {\n let step: Step\n\n var body: some View {\n StepLayout {\n StepDiagram(diagram: step.diagram)\n Title(step.title)\n Subtitle(step.subtitle)\n }\n }\n}\n\nstruct StepLayout: Layout { /* ... */ }"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Grouping data with lazy stack views",
+ "url": "https://developer.apple.com/documentation/SwiftUI/Grouping-Data-with-Lazy-Stack-Views"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/321/5/78830752-d07d-4d89-aeab-94405c084de9/downloads/wwdc2026-321_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/321/5/78830752-d07d-4d89-aeab-94405c084de9/downloads/wwdc2026-321_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "271",
+ "year": "2026",
+ "title": "Code-along: Build powerful drag and drop in SwiftUI",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/271"
+ },
+ {
+ "id": "10056",
+ "year": "2022",
+ "title": "Compose custom layouts with SwiftUI",
+ "url": "https://developer.apple.com/videos/play/wwdc2022/10056"
+ },
+ {
+ "id": "10031",
+ "year": "2020",
+ "title": "Stacks, Grids, and Outlines in SwiftUI",
+ "url": "https://developer.apple.com/videos/play/wwdc2020/10031"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:21.462Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-322.json b/data/wwdc/videos/2026-322.json
new file mode 100644
index 0000000..1ffb8d7
--- /dev/null
+++ b/data/wwdc/videos/2026-322.json
@@ -0,0 +1,143 @@
+{
+ "id": "322",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/322/",
+ "title": "Compose advanced graphics effects with SwiftUI",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Design",
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi! I am Haotian, an engineer on the UI Frameworks team. Since its inception, SwiftUI has been steadily growing with its capabilities in graphics and layout, making it the choice for people who want to ship rich and custom experiences on Apple devices. Apple uses SwiftUI to build advanced effects across its own apps, too. Well, the word advanced can sound intimidating. But here's the thing, even with advanced effects, SwiftUI apps share the same basic elements. It is like a pipeline.\n\nData flows through a series of standard pipes. It takes something in, transforms it, and passes it along. SwiftUI's progressive disclosure means each pipe already works on its own. But you can connect them, create branches, or merge the flows. That's when you get creative. The 'advanced' lies in the construction, not the complexity. Here's the roadmap. First, I will take a design and break it apart. Then I will build advanced effects. And finally, I will share how you can incorporate these techniques in your apps, using a creative pipeline. Here is a design I am building.\n\nSo, I have been building my own podcast app. This is what it currently looks like, a bare-bones transcript view. And I am going to make it fancy, like the live lyrics view in Apple Music. With animated cover art and transcripts that scroll in sync with time. How do I even start? I start with what I already have. My existing user interface already contains all the data I need, including the cover art, the playback info, and the transcript text. The question isn't what data I need, it's how I transform it using the pipeline. Here are a couple of examples.\n\nStarting with the cover art, I need a pipe that converts the image into a visualizer, and the shader pipe fits here.\n\nThe visualizer needs to be in motion to reflect our playback state. For dynamic visuals, I connect the time pipe to the pipeline, that is two pipes merged into one.\n\nWell, the time pipe can do more than that. The transcript pipe was transformed to have timestamp overlays, but it does not know about the current time and therefore cannot scroll correctly.\n\nI can connect the same time pipe to form a pipeline of time-synced scrolling text. And now I have dynamic visuals for the background, and a scrolling transcript for the foreground. Time to connect those two parallel pipes together. And if you zoom out, you realize that every modifier, every API, is another stage in the pipeline. It just flows.\n\nJust like what was shown, my podcast app contains advanced layout and graphics. I have a full-screen cover art, applied with shader effect, and time-driven animation. On the other front, I have a time-synced scrollable transcript view, refined with floating-view attachment! I will go through each of those and explain how to achieve them, starting with the cover art.\n\nHere's our raw material. A cover art image.\n\nThe cover art is beautiful but it's going to sit behind the transcript. I soften it with a .blur modifier so it doesn't compete.\n\nNow that my cover art is blurred, next, I will apply some shader magic. You might ask, what is a shader? And how different is it from writing SwiftUI code? Let me explain.\n\nThis icon here starts as vector, then it was rasterized by GPU to pixels.\n\nAt this point, I can run a program on GPU called shader to decide which color to fill in those pixels.\n\nThe shader function runs in parallel. Each pixel executes independently, with no awareness of its neighbors. Knowing that, it will make perfect sense how Metal shader can be called from SwiftUI's shader effect APIs. There are three types of shader effects. Each has different method signature, certain parameters are required, although you can also append additional ones for the information you want to forward from SwiftUI to shaders.\n\ncolorEffect works by transforming each pixel's color to a new color, where each pixel is provided with the pixel position and the original view's pixel color at that position. You then return a new color based on that information. This is useful for simple effects like turning a colored image into a black and white one.\n\ndistortionEffect works differently. Instead of expecting a color on a certain position, distortionEffectFunction takes the existing position for a new position that SwiftUI will sample from the original image. There is no pixel color involved, you tell SwiftUI 'I want this position's color to follow that position's color'. This is useful for geometric effects like the sheer effect shown here.\n\nlayerEffect is the most flexible. The layerEffectFunction still works per pixel, but it provides the layer of the entire view, which allows you to sample adjacent pixels or the entire region. This is useful for effects like blur where the output pixel color depends on multiple input pixels.\n\nFor my use case, distortionEffect works, but layer effect provides the most flexibility. I'll add a layerEffect modifier, then I will add a shader function called backgroundWarp.\n\nFor now it just samples from the original layer at the given position, which gives me back the same image. But now I have a shader function I can build on.\n\nWith layerEffect, I can sample anywhere from the original view. For example, I can pass a float2 vector to the shader function, and use it to offset the sample position in the shader.\n\nTo match with the function parameters, I now put a float2 vector from the SwiftUI side.\n\nNow as I increase the offset, each pixel runs the shader with this offset value and so they all uniformly and increasingly sample from a distance.\n\nAnd as I decrease offset to zero, the image goes back.\n\nStill, because of the uniform offset, I only get the shifted pixels in a fixed pattern. I need something more organic, something that varies per pixel.\n\nFor organic variation, I use a NoiseTexture, a pre-computed image of smooth, random values.\n\nThis time, on the SwiftUI side, I pass the view size alongside the NoiseTexture as an image parameter.\n\nAnd on the Metal side, the image arrives as texture2d.\n\nNow I am about to show some really metal Metal code.\n\nI first use the current pixel position and the size to get the uv value, which stands for where I am relative to this image, it allows me to sample textures without an absolute position.\n\nNow I'll unpack the NoiseTexture.\n\nIt has RGB channels, the red and green channels are interesting because each one contains a different noise pattern.\n\nIf I move my uv, I get different red and green values. This pair of constantly changing values happen to be a good fit for our organic offset in X and Y since it is different per pixel.\n\nNow come back to the Metal shader.\n\nI create a sampler with repeat mode so it tiles, then I sample the noise at each pixel's UV position. The red and green channels give me a two-dimensional offset, which I scaled and added to the position to sample from the original view. Now, the shader twists the image slightly.\n\nThat was per-pixel variation, but I want something richer.\n\nSo I experiment, what if, instead of one noise sample, I do it twice. The first gives me an initial offset. Then I sample the noise again, but this time at a position shifted by the initial offset, and just like that, I get these organic, flowing blobs.\n\nThis layered noise approach is a well known technique called domain warping. To explore how I did it, download the sample app, it even has a preview so you can play with the parameters as you want.\n\nNow I have a cool shader effect, but it's still frozen. I need to make it move. That's where time comes in.\n\nDifferent from SwiftUI's transaction-based animation, shaders are stateless. They have no memory of the previous frame, the output relies only on the parameters. So, if I want animation, I need to pass in a value that changes over time.\n\nTimelineView is exactly the pipe that I need to connect. With the animation schedule, it fires every frame with a timestamp. I pass that timestamp into the shader, add it to the position to sample from noise, and the pattern starts flowing.\n\nThat was shader animation, driven by time. For my transcript view, I also need to add time to the mix, so that the current running transcript line will be highlighted and centered in the scroll view.\n\nHere's my transcript. Text views in a LazyVStack inside a ScrollView. Each line is its own view, familiar SwiftUI. Now I need to make it follow the playback state.\n\nI use the playback timestamp to determine which line is current. The current line is bold and clear, the rest fades back. And with the onChange modifier to monitor the current line change, I scroll to keep the current line centered.\n\nI have got my time-synced scroll view working. Now, I want to focus on the small timestamp on the current line. Every line has a timestamp in its overlay, but only the one for the current line is visible. This way, it doesn't interfere with the layout. It's always there, just waiting to be shown.\n\nLet's focus on this one row, a sub view, attached on the edge of its container. How do I get it there? The offset modifier cannot do it without knowing the size of both views.\n\nFirst, let's talk about alignment. Every view has alignments. Think of it as the point the layout system uses to position the view, and it is defined by both axes.\n\nWhen I place the sub view in the overlay container, the layout system aligns them using the default center alignment.\n\nThink of it like a pin punching through both views, so it holds them together at each view's alignment point.\n\nI change the overlay's alignment to .bottomLeading.\n\nNow the pin goes through the bottom leading point of each view and they lock together there.\n\nRight now, the layout system asks for the bottom leading alignment, and so the subview returns its bottom leading point to punch through.\n\nIf I were to explicitly express this in code, I would write an alignment guide here to mean bottom is bottom.\n\nNow, remember the goal is that the subview's top edge should touch the bottom edge of the container.\n\nWhat if, I tell the subview that, when the layout system asks about the bottom alignment, don't use the default one. Instead, I have a custom override that moves the bottom alignment to the top edge.\n\nAnd now, when the pin comes to punch through, it follows that point instead.\n\nI get the result by just writing a purely semantic override without manually offsetting the view. There's more to this API. I can define my own custom alignments, and the closure gives me ViewDimensions so I can compute point from the view's actual size. Check out the documentation on \"SwiftUI Alignment\" for the full picture.\n\nAnd here it is. The bare-bones transcript view I started with, now with an animated background driven by a shader and time, a transcript that scrolls in sync with playback, and a floating timestamp positioned with alignment guides.\n\nAll from the simple pipes, composed together, and it works across Apple devices.\n\nLet's step back. I took a design, broke it down into layers, and for each layer I found the right API to turn raw data into views. Each stage's output fed the next stage's input.\n\nConnecting stages like this is what I called a creative pipeline. But those were the choices I made for this podcast app. For your own app, the pipeline can get even more creative. The inputs could have been gyroscope data instead of audio. The shader could have been a ripple instead of a twist. The foreground could have been a freeform canvas instead of a scroll view. Every combination gives you something different. That's the creative part, the APIs are the same. What you feed in and how you connect them, that's yours.\n\nSo go make it your thing. Download the sample project and experiment with the shader, change the noise, tweak the speed, try a different image. Look for opportunities in your own app where a small visual effect could make a big difference. And when you start connecting those pipes together, you'll be surprised how quickly something simple becomes something advanced.\n\nThank you for watching, and goodbye!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "4:18",
+ "title": "Cover art image",
+ "language": "swift",
+ "code": "Image(\"CoverArt\")"
+ },
+ {
+ "timestamp": "4:24",
+ "title": "Blurred cover art image",
+ "language": "swift",
+ "code": "Image(\"CoverArt\")\n .blur(radius: 30)"
+ },
+ {
+ "timestamp": "7:09",
+ "title": "Applying layer effect in SwiftUI",
+ "language": "swift",
+ "code": "GeometryReader { proxy in\n CoverArtView()\n .layerEffect(\n ShaderLibrary.backgroundWarp(),\n maxSampleOffset: .zero\n )\n}\n.ignoresSafeArea()"
+ },
+ {
+ "timestamp": "7:21",
+ "title": "Writing layer effect shader in Metal",
+ "language": "swift",
+ "code": "[[stitchable]] half4 backgroundWarp(\n float2 position, SwiftUI::Layer layer\n) {\n return layer.sample(position);\n}"
+ },
+ {
+ "timestamp": "7:39",
+ "title": "Metal shader with offset parameter",
+ "language": "swift",
+ "code": "[[stitchable]] half4 backgroundWarp(\n float2 position, SwiftUI::Layer layer,\n float2 offset\n) {\n return layer.sample(position + offset);\n}"
+ },
+ {
+ "timestamp": "7:55",
+ "title": "SwiftUI layer effect with offset parameter",
+ "language": "swift",
+ "code": "GeometryReader { proxy in\n CoverArtView()\n .layerEffect(\n ShaderLibrary.backgroundWarp(\n .float2(.init(x: 0, y: 0))\n ),\n maxSampleOffset: .zero\n )\n}\n.ignoresSafeArea()"
+ },
+ {
+ "timestamp": "8:04",
+ "title": "SwiftUI layer effect with full-width offset",
+ "language": "swift",
+ "code": "GeometryReader { proxy in\n CoverArtView()\n .layerEffect(\n ShaderLibrary.backgroundWarp(\n .float2(.init(x: proxy.size.width, y: 0))\n ),\n maxSampleOffset: .zero\n )\n}\n.ignoresSafeArea()"
+ },
+ {
+ "timestamp": "8:37",
+ "title": "SwiftUI layer effect with noise sampling",
+ "language": "swift",
+ "code": "GeometryReader { proxy in\n CoverArtView()\n .layerEffect(\n ShaderLibrary.backgroundWarp(\n .float2(proxy.size),\n .image(Image(\"NoiseTexture\"))\n ),\n maxSampleOffset: .zero\n )\n}\n.ignoresSafeArea()"
+ },
+ {
+ "timestamp": "8:55",
+ "title": "Metal shader with noise sampling",
+ "language": "swift",
+ "code": "[[stitchable]] half4 backgroundWarp(\n float2 position, SwiftUI::Layer layer,\n float2 size, texture2d noiseTex\n) {\n constexpr sampler s(address::repeat, filter::linear);\n float2 uv = position / size;\n\n half4 n = noiseTex.sample(s, uv);\n float2 offset = (float2(n.r, n.g) - 0.5) * 200.0;\n\n return layer.sample(position + offset);\n}"
+ },
+ {
+ "timestamp": "10:22",
+ "title": "Metal shader with domain warping",
+ "language": "swift",
+ "code": "[[stitchable]] half4 backgroundWarp(\n float2 position, SwiftUI::Layer layer,\n float2 size, texture2d noiseTex\n) {\n constexpr sampler s(address::repeat, filter::linear);\n float2 uv = position / size;\n\n half4 n = noiseTex.sample(s, uv);\n\n float2 q = float2(n.r, n.g);\n n = noiseTex.sample(s, uv + q);\n\n float2 offset = (float2(n.r, n.g) - 0.5) * 200.0;\n\n return layer.sample(position + offset);\n}"
+ },
+ {
+ "timestamp": "11:37",
+ "title": "SwiftUI layer effect with animated visual",
+ "language": "swift",
+ "code": "@State private var startDate = Date.now\n\nTimelineView(.animation) { timeline in\n let elapsed = timeline.date.timeIntervalSince(\n startDate\n )\n CoverArtView()\n .layerEffect(\n ShaderLibrary.backgroundWarp(\n .float2(proxy.size),\n .image(Image(\"NoiseTexture\")),\n .float(elapsed)\n ),\n maxSampleOffset: .zero\n )\n}"
+ },
+ {
+ "timestamp": "12:15",
+ "title": "Basic transcript view",
+ "language": "swift",
+ "code": "ScrollView {\n LazyVStack(alignment: .leading, spacing: 12) {\n ForEach(sampleTranscript) { line in\n .font(.title)\n .fontWeight(.bold)\n }\n }\n}"
+ },
+ {
+ "timestamp": "12:33",
+ "title": "Time-synced transcript view",
+ "language": "swift",
+ "code": "@State private var playback = PlaybackState()\n\nScrollViewReader { scrollProxy in\n ScrollView {\n LazyVStack(alignment: .leading, spacing: 12) {\n ForEach(sampleTranscript) { line in\n Text(line.text)\n .transcriptLineStyle(isCurrent: \n line.id == playback.currentLineIndex\n )\n }\n }\n }\n .onChange(of: playback.currentLineIndex, { _, i in\n scrollProxy.scrollTo(i, anchor: .center)\n })\n}"
+ },
+ {
+ "timestamp": "13:53",
+ "title": "Overlay with center alignment",
+ "language": "swift",
+ "code": "Text(line.text)\n .overlay {\n Text(line.formattedTimestamp)\n }"
+ },
+ {
+ "timestamp": "14:06",
+ "title": "Overlay with bottom leading alignment",
+ "language": "swift",
+ "code": "Text(line.text)\n .overlay(alignment: .bottomLeading) {\n Text(line.formattedTimestamp)\n }"
+ },
+ {
+ "timestamp": "14:32",
+ "title": "Overlay with alignment guide override",
+ "language": "swift",
+ "code": "Text(line.text)\n .overlay(alignment: .bottomLeading) {\n Text(line.formattedTimestamp)\n .alignmentGuide(.bottom) { $0[.top] }\n }"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Alignment",
+ "url": "https://developer.apple.com/documentation/SwiftUI/Alignment"
+ },
+ {
+ "title": "Composing advanced graphics effects with SwiftUI",
+ "url": "https://developer.apple.com/documentation/SwiftUI/Composing-advanced-graphics-effects-with-SwiftUI"
+ },
+ {
+ "title": "Shader",
+ "url": "https://developer.apple.com/documentation/SwiftUI/Shader"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/322/4/db4c622a-2091-45ef-a024-df317a5b55a5/downloads/wwdc2026-322_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/322/4/db4c622a-2091-45ef-a024-df317a5b55a5/downloads/wwdc2026-322_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "10151",
+ "year": "2024",
+ "title": "Create custom visual effects with SwiftUI",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10151"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:21.266Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-324.json b/data/wwdc/videos/2026-324.json
new file mode 100644
index 0000000..6dbc4f7
--- /dev/null
+++ b/data/wwdc/videos/2026-324.json
@@ -0,0 +1,148 @@
+{
+ "id": "324",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/324/",
+ "title": "Meet Core AI",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi everyone, my name is Ben and I'm an engineer on the Core AI team.\n\nToday I'll be giving an introduction to Core AI, and showing how you can use it to add intelligent features into your apps.\n\nAI is advancing faster than ever. New models and capabilities that previously seemed out of reach are emerging constantly.\n\nCore AI is built to help you harness that momentum and build on top of it. Core AI marks the next evolution of on-device AI execution across Apple platforms. It's built from the ground up for modern workloads, and delivers the high-performance inference you need to build advanced AI features. Core AI is the inference framework powering on-device Apple Intelligence. And now, it's available for you to use, bringing that same power to your app's own intelligence. Core AI is more than just a framework. It's a complete set of technologies, covering the model deployment lifecycle, from model optimization and conversion to debugging and integration into your app. All designed to support the fast, iterative cycle that building great AI features requires.\n\nCore AI allows you to leverage all of Apple Silicon. It provides blazing fast inference across the CPU, GPU, and Neural Engine.\n\nThe framework comes with a modern Swift API. It's an expressive API that delivers the performance your app demands without compromising on memory safety.\n\nThe broader set of technologies fit naturally into common ML engineering workflows, reusing familiar Python and PyTorch foundations for model authoring, optimization and conversion.\n\nCore AI also supports extensive customization from fine-grained inference management and model specialization to custom GPU kernels.\n\nAnd all of this is tightly integrated into a new developer toolchain, with ahead-of-time compilation, dedicated Core AI Instruments, and a powerful visual Debugger to trace tensor values directly back to your original Python source code. Core AI is designed to scale to your needs and available compute.\n\nWhether you want your app to identify who's talking in a live meeting with a small speaker diarization model, your users to point their camera at anything, ask a question, and instantly get an answer with a larger vision language model, or let them hand off complex, multi-step tasks to a powerful agentic assistant powered by a 70 billion parameter LLM. Core AI has you covered. With all of it running locally on Apple devices, with no server and no cost per token. In this talk, I'll start by showing how to get your model into the Core AI format.\n\nThen I'll go over how to integrate the converted model into your app.\n\nI'll then dive a little deeper into optimizing the performance of your model and app. And lastly I'll highlight some additional features of Core AI and its associated tools that you may find useful.\n\nLet's get started. Every great app experience starts with an idea. Maybe you want to build something that feels a little magical, something that responds intelligently, or makes a decision that would otherwise require a human or hard coded rules.\n\nMachine learning and AI are what make those kinds of experiences possible. Once you have that idea, the next step is finding or building a model that can power it. Just like your idea itself will evolve over time, finding the right model is an iterative process. You'll try things, evaluate them against your requirements and refine. Core AI is designed to support that iteration and make it as fast and frictionless as possible.\n\nSo to make this concrete, I'll implement a fun game idea I had. It's an app that lets you play a two player snake game where one snake is powered by an AI model run through Core AI. The app will follow traditional snake rules where snakes can grow by eating food and must avoid hitting walls, themselves and the other snake. The last snake standing wins. At each time step, the AI model will see a set of features describing the current board state, and those features will be accumulated into the full game history that gets fed to the model. It will then predict the best direction to move. While snake is a simple game, the tools and APIs used to create this experience are the same foundation that scale all the way up to the larger, more complex use cases.\n\nI was curious to see what I could put together with PyTorch for this project. With a little help from an AI coding assistant I was able to sketch out a simple snake action prediction model pretty quickly. To train it, I used a naive simulation to generate training data, just running the game and recording states and actions. The idea was to start simple and get the model working in my app.\n\nSo the next step is taking this PyTorch model and converting it to Core AI.\n\nI'll use the new Core AI Torch Python package to easily perform the conversion.\n\nFirst I'll load the trained checkpoint of the SnakeTransformer module, and prepare a sample input.\n\nThen I'll export the torch program using torch.export and also make sure to use the dynamic_shapes argument to specify that the sequence length of the features is dynamic, that way it doesn't get traced with the static sample length of 5. Also I'll run decompositions on the converted program using Core AI's decomposition table.\n\nNext I'll run Core AI's TorchConverter, specify the names of the inputs and outputs, and finally save the converted Core AI model to disk. Before leaving the Python environment, one more thing I'll do is run a test to verify that the converted Core AI model matches the numerics of my original PyTorch model. This can be done easily with the Core AI framework Python bindings. First I'll load the PyTorch and Core AI models. Then prepare a sample snake game input.\n\nThen run that same input through both the PyTorch module and the Core AI inference function. And finally assert a sufficiently small delta for my use case between the PyTorch and Core AI outputs.\n\nNow that I have the converted AI model, the next step is to hop into Xcode and integrate the model into my app. First I'll open the AI model file with Xcode, which shows information about the model.\n\nIt includes the model size, the distribution of operations and other helpful metadata. Also in the Functions tab it shows you the exact function signature of each unique function in the model.\n\nIn this case the model just has one function, which takes the features of the game board as an input and produces logits as an output which indicate which direction the model thinks would be best to move. Also note that the question mark in the NDArray values denotes that the dimension has a dynamic shape, which matches how I converted the model with a dynamic sequence length. Now that I've included the AI model file in my Xcode project and have examined its structure, the next step is to use the Core AI framework to run the model.\n\nThe Core AI framework is a new Swift API surface for loading and running Core AI models.\n\nIt offers a progressively disclosing set of APIs, which makes it simple to get things up and running, while also having deeper layers of flexibility for supporting performance critical applications.\n\nAlso, it uses modern Swift language features like non-escapable types, to offer memory-safe APIs while not sacrificing performance.\n\nLet's begin by discussing the core types within the framework.\n\nAn AIModel is initialized from a URL to a .aimodel file and is used primarily to inspect and load one or more inference functions.\n\nAn InferenceFunction is the runnable object which represents a single loaded compute graph. In the common case, your AIModel will only have a single main InferenceFunction, though you can convert a single model with multiple functions. The AIModel and InferenceFunction are typically objects you'll construct when preparing your app's AI feature. For example this could be on app initialization.\n\nNDArray is the type which holds your multi-dimensional input and output data and you use the run method on an InferenceFunction to run inference with that data.\n\nFinally you can read and process the outputs of the inference.\n\nSo for implementing the snake game, I'll start by making the ModelPlayer type. At app initialization time, it'll be initialized with the URL to the AI model file that it should use. Then it will initialize the AIModel, and load the main inference function from it.\n\nNext is the logic for the model player to make decisions. It'll conform to the SnakePlayer protocol that I've defined in my app.\n\nThe main protocol requirement is the chooseAction function which is passed in the game's history, and returns the next action that the snake should take.\n\nThe first thing to do is create an NDArray to populate with the input features.\n\nFor this inference function, the expected structure of the NDArray is 2 dimensional with float32 data, where the first dimension of the shape is the current sequence length, and the second is the fixed hidden dimension size.\n\nThen it'll write the features into that NDArray using this writeFeatures helper function which takes the game and a mutable view of the NDArray. The NDArray.MutableView type is a non-escapable type which provides safe and efficient access to the backing storage of the NDArray.\n\nAfter preparing the inputs, it'll run inference with them, and extract the expected output logits ndarray.\n\nThe last step is to sample the output logits to pick the next direction that the snake will move, by passing an ndarray view into the helper function which will read the values and choose the direction with the largest corresponding logit. The writeFeatures function is what's populating the input features. Let's briefly go over what these features include.\n\nThey have the normalized distance of the AI snake's head to all the walls. The normalized relative X and Y distance to the nearest food.\n\nFour elements encoding it's current direction.\n\nThe normalized distance to the other snake.\n\nAnd finally the opponent's direction.\n\nNow with this put together I'm going to try a test run with both snakes powered by the AI model to see how it does.\n\nRunning it shows that the model is working. However, I see that the game is getting slower as it goes on.\n\nAlongside the Core AI framework, there's a new instrument in Xcode to help you profile the Core AI models running in your app. In this case I've ran the app with Instruments and I can see the inference intervals getting notably larger over time, which means the inference calls are increasing in latency. This makes sense because transformer models have quadratic time complexity with respect to the sequence length. And in our game the sequence length is increasing with every move the model makes. The next step in this case is to optimize the performance of the model usage.\n\nEach time the input sequence is increased, the transformer model recomputes a set of internal key and value embeddings for every element in the sequence. A common strategy used to improve the performance of decoding loops like this when using transformers is to cache keys and values that are computed for each element in the sequence, as opposed to re-computing them all from scratch with each inference. This can be achieved through Core AI by using states.\n\nStates are inputs to the model which are both read, and updated in-place during inference. By introducing the key and value caches as states on the model, we both avoid recomputing them on each inference, and also remove the need to provide the full history of the game as an input since the data needed from older steps are stored in the states.\n\nSo after the first input, each subsequent step uses the cache for history and only takes the new features of the latest board state.\n\nTo implement the key/value caching, I'll go back to the original authoring code and make a few changes to add in the key and value caches. First I'll update the torch module by adding key and value cache tensors as buffers within the transformer module, by using the torch register_buffer API. This will later result in these tensors being mutable buffers in the exported torch program which Core AI will convert to states. Then in the forward function of the module, I'll add the logic to actually use the caches. This involves reading previous features keys and values out of the cache. Then writing the computed keys and values for the new features back into the cache. Lastly, I'll rerun the same code from before to re-convert the model, but now adding in the state_names argument to the convert call to specify the names of the new state arguments. Now that I've re-converted the model with the new function signature, I'll update the app code to handle it. To start, I'll update the ModelPlayer to store the key and value cache NDArrays which will be the state arguments passed to each inference. I'll initialize them with the expected shape for the transformer. In this case I converted the model such that it expects the key and value caches to always be a fixed size for a maximum possible context length.\n\nThen when it's time to run inference, I'll construct a collection of MutableViews containing both views of the key and value caches. Then provide those as the states argument of the InferenceFunction.run method. Now the caches will be both read and updated in-place during each inference. Now with the updated model, I'll re-run the app. This time I can see it maintains a steady speed, no longer slowing down overtime.\n\nWhen tracing the updated app in Instruments, I can confirm that the inference latency is growing at a much slower rate.\n\nBefore wrapping up, I'll show some features that I didn't use while making the snake game, but that you may find useful when developing your own apps. When converting the snake game models, I used the coreai-torch package to directly convert the PyTorch module. This flow is simple and works great for many use cases, but sometimes you may need more control over how your model is authored, and potentially even how the operations within the model are run.\n\nWe've only touched the surface of what the Core AI Python package has to offer. It also has support for directly authoring your model with Core AI APIs, optimizing the model for Apple Silicon, and defining custom kernel implementations with Metal 4. To learn more about these advanced model authoring flows, see the talk \"Dive into Core AI model authoring and optimization\". In addition to debugging performance, it's also crucial to be able to debug the numerics of your converted model. For this you can use the Core AI Debugger which allows you to visualize your converted model, easily inspect intermediate tensor values, and trace back operations in the converted model to the Python source code which introduced them.\n\nThere is also a convenient Core AI debug gauge which shows you streaming Core AI activity while your app is running in Xcode. This is a great place to spot performance issues before jumping into instruments.\n\nOne thing that was glossed over in the snake game implementation is the process of model specialization.\n\nWhen you ship an AI model with your app, that is a source representation of the model, which can be run on any Apple device. However, to actually load and run the model within your app, it must be specialized for the device that the app is running on. When your model is loaded it is checked to see if it has already been specialized and cached. The specialization process can take a significant amount of time for very large models.\n\nWhile future loads are from the cache and fast, that first time is something you may need to plan for. It is recommended you avoid having model specialization occur within user interactive flows.\n\nCore AI can help you with that. First, Core AI gives you programmatic access to the default model cache for your app. You can request to load models directly from it. If nil is returned, it is not present and requires specialization. You can use this to gate features or inform the users that they may need to wait a bit while your app prepares the model.\n\nSecond, you can request model specialization explicitly in your app independent of it being loaded.\n\nYou can do this after downloading assets or when the user opts in to a feature so the model is ready to go ahead of time. And there is a lot more control available. SpecializationOptions help configure how you want your model to be optimized for inference.\n\nWith the AIModelCache you can also delete entries you no longer need, and control the policy on how long entries persist.\n\nYou can even share a cache between multiple apps in the same app group.\n\nCheck out the \"Managing model specialization and caching\" article on developer.apple.com to learn more.\n\nIndependent of when specialization occurs, it still takes time. Lets take a quick peak inside. During specialization, the model goes through two main transformations. First, it goes through a core set of compilation steps which segment, plan and optimize compute. Second, executable artifacts are generated for the compute units used. These artifacts are tied to the device and OS version they were generated on. Of these two steps, compilation is the one which incurs most of the latency.\n\nThe Core AI toolchain can help you reduce that time by allowing some compilation to occur ahead of time on your development machine, producing a compiled version of the model.\n\nWhile that compiled model still needs to be specialized for the specific users device, there is now much less work to do and finishes significantly faster. To learn more about this option, check out the \"Compiling Core AI models ahead of time\" article on developer.apple.com. Controlling when, where, and how specialization happens is one way to help you optimize your users experience. Another area you may want to optimize is removing any overheads in tight inference loops using your model. The Core AI Framework has several APIs to help you here.\n\nYou can dynamically check the optimal memory layout of NDArray arguments and allocate them with that structure to avoid layout conversions at inference time.\n\nYou can also pre-allocate output values for the framework to write into, to avoid allocating new output values during inference.\n\nAnd you can also use asynchronous values to efficiently pipeline execution of multiple inference functions together. For most use cases, the higher-level inference APIs will get you exactly where you need to be.\n\nBut when you're optimizing a tight inference loop or integrating a model into a complex compute pipeline, these lower-level APIs are there when you need them.\n\nWhether you're just getting started or diving deep, the Core AI Models repository is a great place to find what you need.\n\nIt has a collection of popular models, each just a single command away from being converted and optimized for your app.\n\nAI skills that are experts in Core AI model authoring, optimization, and conversion.\n\nAnd a Swift package with libraries for specific families of models that give you higher-level APIs that already have many of those low-level inference optimizations built in.\n\nIt also provides an API for creating a Core AI Language model, which plugs right in to the Foundation Models framework, letting you bring your own custom models and token sampling strategies.\n\nTo wrap things up: Core AI is available on all Apple Silicon to help you build cutting edge AI experiences on all Apple platforms.\n\nIt has tight integration with the existing Python tools that you're already familiar with, a modern Swift framework for running your models efficiently within your app, and state of the art debugging tools to help you understand how your models are running on Apple devices.\n\nWe can't wait to see what sorts of experiences you build.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "5:08",
+ "title": "Convert a PyTorch model to Core AI",
+ "language": "swift",
+ "code": "import torch\nimport coreai_torch\n# Load trained snake model and sample input for tracing\npt_model = SnakeTransformer().load_checkpoint(\"snake.pt\")\nexample = torch.randn(1, 5, 16)\n\n# Export the torch program including dynamic shape for input sequence\nseq_len = torch.export.Dim(\"seq_len\", min=1, max=256)\nexported = torch.export.export(\n pt_model, args=(example,), \n dynamic_shapes={\"features\": {1: seq_len}},\n)\nexported = exported.run_decompositions(coreai_torch.get_decomp_table())\n\n# Convert torch graph → Core AI graph\nai_program = coreai_torch.TorchConverter().add_exported_program(\n exported, input_names=[\"features\"], output_names=[\"logits\"],\n).to_coreai()\n\n# Save as a .aimodel asset the runtime can load\nai_program.save_asset(\"SnakeTransformer.aimodel\")"
+ },
+ {
+ "timestamp": "5:44",
+ "title": "Verify converted model numerics",
+ "language": "swift",
+ "code": "import torch\nimport numpy as np\nfrom coreai. runtime import AIModel, NDArray\n# Load models\npt_model = SnakeTransformer().load_checkpoint(\"snake.pt\")\nai_model = await AIModel.load(\"SnakeTransformer.aimodel\")\nfunction = ai_model.load_function(\"main\")\n# Assemble input sample - 10 frames of 16-dim game features, shape (1, 10, 16)\nfeatures = np.array(lextract_features(game) for - in range (10)],\ndtype=np.float32)[np.newaxis]\n# PyTorch reference\nwith torch.no_grad():\n\tpytorch_logits = pt_model(torch.from_numpy(features)) . numpy )[0, -1]\n# Core AI inference\nresult = await function({ \"features\": NDArray(data=features)} )\ncoreai_logits = result[\"logits\"]. numpy()[0, -1]\n# Validate\nmax_diff = np.max(np.abs(pytorch_logits - coreai_logits))\n\tassert max_diff < 0.01"
+ },
+ {
+ "timestamp": "7:41",
+ "title": "Core AI framework core types",
+ "language": "swift",
+ "code": "// Core types within Core AI\nimport CoreAI\n\n// Load the '.aimodel' file\nlet model = try await AIModel(contentsOf: modelURL)\n\n// Load the main inference function\nlet mainFunction: InferenceFunction = try model.loadFunction(named: \"main\")!\n\n// Construct the n-dimensional input data\nlet inputNDArray: NDArray = nextInput()\n\n// Run inference\nvar outputs = try await mainFunction.run(inputs: [\"input\": inputNDArray])\n\nguard let outputNDArray = outputs.remove(\"output\")?.ndArray else {\n // Handle unexpected missing output\n}"
+ },
+ {
+ "timestamp": "8:33",
+ "title": "Initialize ModelPlayer with AIModel",
+ "language": "swift",
+ "code": "// Initialize the player by loading the AIModel and InferenceFunction\nstruct ModelPlayer {\n let nextActionFunction: InferenceFunction\n\n init(modelURL: URL) async throws {\n let model = try await AIModel(contentsOf: modelURL)\n self.nextActionFunction = try model.loadFunction(named: \"main\")!\n }\n}"
+ },
+ {
+ "timestamp": "8:49",
+ "title": "Run inference with NDArray inputs",
+ "language": "swift",
+ "code": "extension ModelPlayer: SnakePlayer {\n\n mutating func chooseAction(game: SnakeGame) async throws -> Direction {\n\n // Create an NDArray for the next input and write board features into it\n var inputFeatures = NDArray(shape: [game.stepCount, hiddenDim], scalarType: .float32)\n writeFeatures(of: game, into: inputFeatures.mutableView())\n\n // Run inference and extract the expected logits output NDArray\n var outputs = try await nextActionFunction.run(inputs: [\"features\": inputFeatures])\n guard let logits = outputs.remove(\"logits\")?.ndArray else {\n throw ModelError.missingOutput\n }\n\n return predictedDirection(from: logits.view())\n }\n\n func writeFeatures(of game: SnakeGame, into view: consuming NDArray.MutableView) { … }\n func predictedDirection(from logits: NDArray.View) -> Direction { … }\n}"
+ },
+ {
+ "timestamp": "10:10",
+ "title": "Input features for the snake model",
+ "language": "swift",
+ "code": "// Features at each time step\nvar features = [Float]()\n\n// Distance to wall in all directions, normalized between [0, 1]\nfeatures += [dWallUp, dWallDown, dWallLeft, dWallRight]\n\n// Distance to nearest food, normalized between [-1, 1]\nfeatures += [dFoodX, dFoodY]\n\n// Direction encoded as one-hot: [1,0,0,0]=up, [0,1,0,0]=down, etc.\nfeatures += dir.oneHotEncoding\n\n// Distance to the other snake, normalized to [-1, 1]\nfeatures += [dUserX, dUserY]\n\n// Direction of the opponent snake\nfeatures += dirU.oneHotEncoding"
+ },
+ {
+ "timestamp": "12:18",
+ "title": "Add KV cache buffers to PyTorch module",
+ "language": "swift",
+ "code": "# Update torch module to include key and value caches\n# Use register_buffer to later make the exported torch program treat them as mutable\n\nclass SnakeTransformerStateful(nn.Module):\n def __init__(self, ...):\n super().__init__()\n self.register_buffer(\n \"k_cache\", torch.zeros(N_LAYERS, 1, MAX_SEQ_LEN, D_MODEL))\n self.register_buffer(\n \"v_cache\", torch.zeros(N_LAYERS, 1, MAX_SEQ_LEN, D_MODEL))\n # …"
+ },
+ {
+ "timestamp": "12:50",
+ "title": "Update forward pass to read/write KV caches",
+ "language": "swift",
+ "code": "# During forward pass, read/write KV caches\n\nclass SnakeTransformerStateful(nn.Module):\n\n def forward(self, features, position_ids):\n new_k, new_v = [], []\n for i, block in enumerate(self.blocks):\n # read previous keys/values from caches\n k_prev = self.k_cache[i]\n v_prev = self.v_cache[i]\n # ... compute q/k/v for the new token, attend over valid prefix ...\n new_k.append(k_updated)\n new_v.append(v_updated)\n\n # Update key/value caches\n self.k_cache.copy_(torch.stack(new_k))\n self.v_cache.copy_(torch.stack(new_v))\n\n return self.action_head(self.ln_final(x))"
+ },
+ {
+ "timestamp": "12:59",
+ "title": "Re-convert model with state names",
+ "language": "swift",
+ "code": "# Updated coreai-torch conversion code using key/value cache states\nimport torch\nimport coreai_torch\n\nexported = torch.export.export(\n stateful_model,\n args=(example_features, example_position_ids),\n dynamic_shapes={\"position_ids\": {1: seq_len}},\n)\nexported = exported.run_decompositions(coreai_torch.get_decomp_table())\n\nai_program = coreai_torch.TorchConverter().add_exported_program(\n exported,\n input_names=[\"features\", \"position_ids\"],\n state_names=[\"keyCache\", \"valueCache\"],\n output_names=[\"logits\"],\n).to_coreai()\n\nai_program.save_asset(\"SnakeTransformer.aimodel\")"
+ },
+ {
+ "timestamp": "13:17",
+ "title": "Store KV cache NDArrays in ModelPlayer",
+ "language": "swift",
+ "code": "// Add stored properties for the key and value caches\nstruct ModelPlayer {\n let nextActionFunction: InferenceFunction\n\n var keyCache: NDArray\n var valueCache: NDArray\n\n init(modelURL: URL) async throws {\n let model = try await AIModel(contentsOf: modelURL)\n self.nextActionFunction = try model.loadFunction(named: \"main\")!\n\n self.keyCache = NDArray(shape: [layers, maxContext, hiddenDim], scalarType: .float32)\n self.valueCache = NDArray(shape: [layers, maxContext, hiddenDim], scalarType: .float32)\n }\n}"
+ },
+ {
+ "timestamp": "13:45",
+ "title": "Pass state views to inference function",
+ "language": "swift",
+ "code": "extension ModelPlayer: SnakePlayer {\n mutating func chooseAction(game: SnakeGame, snakeID: Int) async throws -> Direction {\n // …\n\n var stateViews = InferenceFunction.MutableViews()\n stateViews.insert(&keyCache, for: \"keyCache\")\n stateViews.insert(&valueCache, for: \"valueCache\")\n\n // Run inference and extract the expected logits output NDArray\n var outputs = try await nextActionFunction.run(\n inputs: [\"features\": inputFeatures],\n states: stateViews)\n // …\n }\n}"
+ },
+ {
+ "timestamp": "16:22",
+ "title": "Check model cache before loading",
+ "language": "swift",
+ "code": "// Check if your model can be loaded from the cache\nlet cache = AIModelCache.default\n\nguard let model = try cache.model(for: modelURL, options: .default) else {\n Task { @MainActor in\n informUser(\"Preparing AI features. This may take a while…\")\n }\n}"
+ },
+ {
+ "timestamp": "16:42",
+ "title": "Request model specialization",
+ "language": "swift",
+ "code": "// Explicitly request specialization\ntry await AIModel.specialize(contentsOf: modelURL)"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Core AI PyTorch Extensions",
+ "url": "https://apple.github.io/coreai-torch"
+ },
+ {
+ "title": "Core AI Python",
+ "url": "https://apple.github.io/coreai-torch/main/coreai-core"
+ },
+ {
+ "title": "Core AI Optimization",
+ "url": "https://apple.github.io/coreai-optimization"
+ },
+ {
+ "title": "Core AI",
+ "url": "https://developer.apple.com/documentation/CoreAI"
+ },
+ {
+ "title": "Compiling Core AI models ahead of time",
+ "url": "https://developer.apple.com/documentation/CoreAI/compiling-core-ai-models-ahead-of-time"
+ },
+ {
+ "title": "Managing model specialization and caching",
+ "url": "https://developer.apple.com/documentation/CoreAI/managing-model-specialization-and-caching"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/324/4/3b67b624-4060-495f-9ba7-659805ee6b88/downloads/wwdc2026-324_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/324/4/3b67b624-4060-495f-9ba7-659805ee6b88/downloads/wwdc2026-324_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "325",
+ "year": "2026",
+ "title": "Dive into Core AI model authoring and optimization",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/325"
+ },
+ {
+ "id": "326",
+ "year": "2026",
+ "title": "Integrate on-device AI models into your app using Core AI",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/326"
+ },
+ {
+ "id": "330",
+ "year": "2026",
+ "title": "Optimize custom machine learning operations with Metal tensors",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/330"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:21.070Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-325.json b/data/wwdc/videos/2026-325.json
new file mode 100644
index 0000000..78670b8
--- /dev/null
+++ b/data/wwdc/videos/2026-325.json
@@ -0,0 +1,94 @@
+{
+ "id": "325",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/325/",
+ "title": "Dive into Core AI model authoring and optimization",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi. I'm Sachin, an engineer on the Core AI team, and later I will be joined by my colleague Nicole. Today, we look forward to showing you how Core AI makes it easy, to get your model running efficiently on Apple Silicon from the basics to more advanced approaches. As you saw in the \"Meet Core AI\" talk, Core AI is a complete suite of technologies, covering the model deployment lifecycle from model optimization and conversion to debugging and integration into your app.\n\nIn this talk, we will be zooming in on Core AI's Python ecosystem. You will meet the different libraries and tools that Core AI provides to get your models deployed on Apple Silicon.\n\nYou will also meet the Core AI Debugger, which can be an integral part of your workflow for understanding and debugging key issues. Lets dive in.\n\nHere's our agenda. I will first introduce you to the Core AI models repository, and Core AI skills, powerful tools that will jump-start your journey. Then, I will show you the basic conversion and verification process. Core AI is built around the Python and PyTorch workflows you already know and if you've used Core ML before, a lot of this will feel pretty familiar.\n\nI will follow that with model optimization, and show how you can make the right choices for your use-case and target platform.\n\nTo help you get a deeper insight into your model, Nicole will introduce you to Core AI Debugger. And finally, I will touch upon key ways that Core AI allows deep customization during model authoring and conversion. Okay. Lets start with models and skills.\n\nAt the forefront of Core AI's ecosystem, is the coreai-models repository. It includes a Swift package for running LLMs in your app. But at its core, its an open-source repository of models that are ready to go, including generative architectures like cutting-edge large language models.\n\nWe have examples engineered for various use-cases and constraints, along with components that you can use to bring your own models to Core AI. And last but not the least, Core AI models ships with a set of agent skills. You can install these skills into your favorite coding assistant, to get started with Core AI, just like an expert, from day one. Core AI skills work with you and translate your high-level ideas into a clear deployment plan for downstream tasks. They may get clarifications from you around the model you are interested in, the hardware families you are targeting, and the constraints your application has. These requirements inform the Core AI features you need, all the way from any changes in the PyTorch model code to conversion, optimization and running the models. AI skills give your coding agent access to the best practices and domain knowledge from our engineers. This empowers you to leverage Core AI like a pro, and even understand it better with your coding assistant! In fact, most of the code you will see throughout this talk was co-developed with an agent actively leveraging these skills.\n\nNow, lets dig into converting and running models in Core AI with Python.\n\nThe Core AI Python libraries, primarily Core AI PyTorch extensions, are your entry point into the ecosystem. Installation is simple with pip install coreai-torch, this installs both the coreai package and the coreai-torch library building on top of it.\n\nYou hand coreai-torch a PyTorch exported program, and it converts directly to a Core AI model. It supports advanced features that let you tailor the Core AI program to your exact use-case. For example, you can assemble multiple models into a single artifact, register custom lowerings for specific operations, and inline Metal 4 kernels right into your converted model. And finally, you can specialize models into optimized assets, and run them natively on Apple Silicon entirely from Python.\n\nHere's the pipeline I just described. Now, lets see this in practice.\n\nI will walk you through a quick example. Here I have a neural network — two linear layers with a relu activation. Standard PyTorch. Then, I run torch.export, I pass the model and an example_input, which gives me an exported_program. This exported_program is the starting point for Core AI conversion. It captures the full computational graph: weights, operations and shapes in a format that coreai-torch can work with.\n\nAnd now, the Core AI side.\n\nCore AI's TorchConverter takes my exported program, along with the input and output names, and converts it to a core_ai_ program. If you've used CoreML tools before, this will feel familiar.\n\nThe converted model is then optimized and saved as an aimodel asset — an on-device format ready to run on Apple Silicon.\n\nOnce I have the specialized asset, I can load a function from the program and perform inference right from Python. You can also pass specialization options at this point to customize the process. To actually run inference, all you need to do is provide a dictionary mapping input names to corresponding numpy tensors! That's it… the whole workflow. Conversion, optimization and execution — all from Python.\n\nNow let's talk about making a model smaller, using Core AI's optimization library. To showcase Core AI's optimization features, I'll be taking Segment Anything Model — SAM3 as the driving use-case.\n\nSAM3 is an 850-million parameter model that performs prompt-based image segmentation. Before I can optimize the model, it's key to understand its internals at a high level. SAM3 has three main pieces. An Image encoder that processes the image. A Text encoder that handles the user's prompt. These two components combined make up 96% of the model's parameters so getting these right is key. And to complete the picture, a Detector module wrapping a DEtection TRansformer, combined with a mask decoder, produces the final output — the segmentation mask.\n\nAs you can see, SAM3 performs a complex, end-to-end task. And this is exactly the kind of use-case developers increasingly want to execute on-device.\n\nTo optimize this, I'll leverage Core AI's optimization library — called coreai-opt. Coreai-opt enables config-driven model compression, you describe what to compress and what to leave alone. It supports various optimization schemes, from which you can choose one to optimize differently for macOS versus iOS, as an example.\n\nIt also supports int4, int8, FP4 and FP8 weight compression with flexible granularity.\n\nAnd finally, coreai-opt includes quantization APIs that you can either use with small amount of calibration data, or perform quantization aware training on larger data sets.\n\nThis is the simple pipeline I had previously. Now I am adding a step. Before conversion, I run the model through coreai-opt with a compression config or I can use one of their convenient presets. This gives me a smaller model that still goes through the same export pipeline. Let's try this on SAM3 and see what happens.\n\nI start by wrapping SAM3 for export. This wrapper defines the interface for torch export to capture the full computational graph of the model.\n\nAnd here's the conversion pipeline from the slides, wrapped into a reusable helper. A couple of interesting points though.\n\nFirst, it runs decompositions in the PyTorch exported_program with Core AI's custom table. This ensures that high-level semantics that Core AI supports, like attention, are preserved in the graph.\n\nSecond, it also supports casting the program to 16-bit floating point using coreai-opt's helper, if needed.\n\nThe full conversion takes a few minutes, so I have pre-computed the baseline asset.\n\nWhat I do here is load the baseline 32-bit converted model and run it.\n\nAs you can see, it's over 3 gigs in size. When I run, the default specialization kicks in to specialize and run the model.\n\nThis is my baseline. In this image, I ask for a segmentation mask over all the flowers. All are successfully detected based on the default threshold, running on-device. This is what I need to preserve after compression.\n\nNow let's look at compression. coreai-opt ships with preset configurations. presets.w4 gives me 4-bit per-channel, symmetric quantization in one line.\n\nI set ExecutionMode to EAGER, which works great for weight compression. For activations, I would use the GRAPH mode.\n\nThen I initialize Coreai-opt's Quantizer with the config, pass example inputs and finalize — the model is then compressed.\n\nAs before, I load the model and run it on-device.\n\nThe model is now around 430 megabytes.\n\nLook at the result. One of the occluded flowers is no longer detected.\n\nI applied the same aggressive compression to every single layer, and its likely that not every layer handles this equally well. The question is — which layers are causing this? This is the kind of problem that's hard to diagnose from the output alone. I need to see inside the model. Let me hand it over to Nicole to show you how.\n\nThanks, Sachin! I'm excited to talk to you about Core AI Debugger. Now we've seen how to create and optimize your Core AI model. But, if you need a deeper understanding of your model and its behaviour you can use Core AI Debugger. Core AI Debugger is a new standalone application that can help you inspect your models on Apple platforms. With the debugger you can visualize your model's structure in an easy-to-understand graph format, execute your model on specific hardware for true runtime results, and validate inference correctness against a reference run — all in one place. I'm excited to show you Core AI Debugger in action and figure out what happened when the SAM3 model was quantized. I'll start by opening the original model and click Inspect to get started.\n\nNow that the model is open, I can see the debugger workspace. On the left is the navigator which contains a structured list of operations in the model.\n\nThese operations are grouped by their PyTorch module, which is especially powerful for larger models like SAM3 and allows you to navigate your model in a way that feels familiar.\n\nSelecting a PyTorch module in the navigator, like the detector decoder, will highlight all of the corresponding nodes in the structure viewer at the top of the workspace. This view shows you a graphical representation of your model and provides a clear picture of operation connectivity, execution order, and data dependencies. And, with the source viewer at the bottom, I'm always grounded in my model's original Python code down to the specific line.\n\nFinally, I can learn even more about an operation by selecting it and opening the inspector on the right. Here, I can find a description, and additional details on the operation's inputs and outputs.\n\nTogether, these views allow you to move fluidly between graph structure, source code, and execution details, which dramatically reduces the cognitive overhead of debugging complex models like SAM3. Beyond static analysis, the debugger enables runtime analysis of how your model actually executes on-device. This will be especially helpful for tracking down where quantization has caused a problem. To run the model, I'll click device at the top of the workspace. In the scheme settings, I'll pick my Mac from the list of targets, then specify the inputs I want to provide to the model. Starting with the pixel values, then the input_IDs, and the attention_mask.\n\nFinally, I'll click Run.\n\nSAM3 is now being specialized to run on my device. Now that it's ready, the structure viewer has updated to show me the model, exactly as it would run on my Mac. And I can now click on any operation to see its output tensor directly in the inspector. Without needing to modify anything. Back to the problem at hand, I first want to verify the final detection masks. So I'll scroll to the end of the model and select the final operation.\n\nIn the inspector, I'll click on the tensor preview to get a closer look at the mask. I can see the flowers, but just like in the notebook, one is missing.\n\nNow I want to understand how these results compare with the original PyTorch run. I'll return to my notebook and use the NEW save intermediates API. This API executes a PyTorch model and captures intermediate tensor values at each operation. I want to compare my quantized results with the baseline Sachin showed earlier, so I'll pass in the int4 model alongside the original SAM3.\n\nI'll let it run and now that the intermediates are saved, I'll return to the debugger to compare the results. I'll start by clicking the comparison icon at the top of the workspace to initialize a new comparison session. On the left is the existing configuration I specified earlier. On the right, I can choose another configuration to compare against like a different Target or Compute Unit. In this case, I'll click Target and load a reference run from an Intermediates File.\n\nI'll use the file I just exported and start the comparison.\n\nThe navigator is now populated with operation pairs which combine an operation from the specialized model and PyTorch model.\n\nThese pairs are called sync points, places where the specialized model's output is expected to match the original PyTorch result. The debugger automatically identifies these points throughout the model to make the comparison process easy.\n\nEach sync point is paired with a metric indicating how similar the two outputs are which makes it trivial to find where they diverge. The default metric is a peak signal-to-noise ratio or PSNR, but this can be changed to witchever similarity indicator suits your model best. For SAM3, I'll stick to PSNR.\n\nThe value of the similarity metric can also be quickly gleaned from the status indicator on the right or from the graph itself: green nodes indicate similar tensors, red nodes would indicate significant differences.\n\nAs I scroll through the operations, I'm seeing several yellow sync points, which indicates that parts of my model have moderately diverged from the expected result. I'll sort by similarity, and investigate the most dissimilar sync points.\n\nWhen I click on a sync point in the navigator, the source viewer updates to show me the operation's PyTorch module hierarchy. For example, this operation came from the detector decoder. I'll use the up arrow key to navigate through the low-PSNR sync points one-by-one to see if a pattern emerges.\n\nI'm noticing that the vast majority of low-PSNR sync points are actually coming from the detector decoder. This tells me that the quantization scheme applied earlier has mildly corrupted the detector results. Since we previously identified that the detector block only accounts for 4% of model parameters, we're not getting much benefit from compressing it anyway. So, I'll return to the Jupyter notebook, and try changing the quantization scheme to ignore the detector.\n\nNow that the new scheme has been applied, I'll re-export the model and verify if the change worked.\n\nGreat! I can see that we have once again reached baseline quality where all flowers are detected and the model is only a fraction of the size! Core AI Debugger turned hours of manual tensor comparison into a visual diagnosis. I started with missing detections and reached a revised quantization scheme in minutes. Beyond what I showcased today, Core AI Debugger is capable of solving increasingly complex issues. It gives you deep visibility into how your models behave, enabling greater confidence when bringing your model to Apple platforms. Now, back to Sachin.\n\nThanks Nicole! Now let's take things a step further. So far, I have been converting the model as a single, end-to-end unit. And for a lot of models, that works just fine. But it may not always be enough, depending on your use-case and especially your constraints. And this is where Core AI really empowers you to dig deeper. Concretely, I will now be zooming in to the PyTorch source itself which defines a graph of computations from inputs to outputs. What advanced model authoring implies, is that you look inside this computational graph and really tune how it runs on the hardware. As a simple example, let's consider this series of operations. You can take a group of those ops and fuse them into a single operation This replaces several steps with a single kernel dispatch within the graph. Core AI already ships with pre-packaged fast kernels and primitives for heavy operations like Scaled Dot Product Attention, commonly found in Transformers. You can find examples of how to leverage these operations in the coreai-models repository. But if you live on the cutting edge and want even more customization, we also have support for custom Metal 4 kernels.\n\nComing back to my pipeline. Here's what changes with custom Metal kernels. I am adding a second input to coreai-torch. My kernel's source code written in the Metal Shading Language, or MSL. The converter takes both my PyTorch model and my custom kernel, and bundles them together into a single asset. The MSL is embedded right inside. It ships with the model. Let me show you what that looks like in code. First, I define a PyTorch reference for our example. A standard Sigmoid Linear Unit, or SiLU. It's a common activation function used in generative transformer models. This is what torch.export sees during tracing. Below that, I implement the actual Metal kernel in MSL. This is a simple element-wise kernel, one thread per element, that computes the fused activation directly on the GPU. With just these two pieces, I can now register a Core AI TorchMetalKernel, give it the Metal source, the PyTorch reference and the input and output names. In this case, the input and output names are \"x\" and \"y\" respectively, and you can see those names being used in the MSL kernel above. So you write the Metal. You write the PyTorch reference. And Core AI binds them together. Using it in a model, I will just call it like any other Python function. Pass the input, specify the thread grid and I am done. One thing to note, is that I pass in the result shapes to every instantiation of the custom kernel in the PyTorch source. This allows Core AI to bake in the computation of the output shapes of the kernel from the input shapes, if your model has dynamic shaped inputs.\n\nWhen I convert with TorchConverter, I register my custom kernels with the converter, then add the exported program as before. The Metal source gets embedded directly in the asset a single artifact. The kernel travels with the model.\n\nFor more details on how you can write efficient Metal kernels for Core AI, and to see an optimized kernel live in action with the SAM3 model please see the \"Optimize custom machine learning operations with Metal tensors\" talk.\n\nSo far, I showed how you can take multiple operations in the graph and fuse them into one. But for more advanced optimizations, especially for iOS, you need to go further and rewrite the entire model with a specific target in mind. We refer to this process, as model reauthoring. Back to our simple series of operations. Re-authoring typically involves replacing many aspects of this computational graph. This may imply using different operations, novel tensor layouts, and even modifying the interfaces of the model. Essentially, this is a completely different implementation of the source code.\n\nDigging deeper, what does this kind of authoring involve? One example, is using predefined patterns in the PyTorch code that tell Core AI about a specific concept. This allows the framework to map these semantics to an optimized implementation at runtime. An example of this, is in-place updates of the Key-Value cache commonly used in Large Language models. Another mechanism used, especially when targeting iOS, is the usage of static tensor shapes, channels-first tensor layouts and convolutional op patterns. These enable Core AI to leverage powerful underlying primitives and meet your on-device constraints. When you engineer a novel PyTorch implementation in this way, its crucial that you employ rigorous testing both at the module level and at the model level. This ensures that individual building blocks, as well as the entire model work as intended. This testing can take the shape of unit tests or integration tests. To get you started, the Core AI models repository includes multiple examples of such reusable components and best practices across different models. Core AI skills also enable your coding assistant to write PyTorch code optimized for Apple Silicon from day one. Let's continue with SAM3. Instead of converting the model as-is, I can author a new PyTorch implementation that's hand-crafted for my goals. The biggest change I make is to have three separate functions in the Core AI Model instead of one. Coreai-torch has APIs that lets you do this. Image Encode handles the image, Text Encode processes the prompt, and Detect wraps the final post-processing to generate the output. Splitting the work this way allows me to run each bit at a different cadence. For example, I may want to process a single prompt once and use it across a variety of images in your application. It also gives each function a clean interface, and lets me compress and author each one independently. Lets see this in practice. Here's the attention block from the Image Encoder transformer, rewritten for power-efficient execution on iOS.\n\nInstead of standard Linear layers, I use convolutional projections. This is one of the patterns that lets Core AI leverage native hardware primitives on the right compute unit. The text encoder gets a similar treatment. The smaller decoder stays mostly unchanged. It's a small fraction of the compute, so the payoff from re-authoring it is minimal.\n\nI structure the re-authored model as three independent modules. ImageEncoder, TextEncoder and the Detector. As mentioned earlier, this separation lets me use different aspects of the model uniquely.\n\nFor compression, I apply 4-bit palettization with per-channel scales to the two encoders. There is a preset available for this, but I use the lower-level representation here to showcase the APIs. This lookup-table-based compression, is well-suited for power efficiency on iOS.\n\nAs before, I construct a KMeansPalettizer similar to the Quantizer, and pass it the model and config. Then, I prepare and finalize. Also note, that I changed the input image size from 1008 pixels to 336 to run on an iPhone.\n\nThe detector stays uncompressed. I know that its sensitive to compression from our previous exercise.\n\nI then run each model through torch export. All of them get cast to half-precision.\n\nAnd here's where it comes together. A single TorchConverter, three exported programs, each with its own entrypoint name.\n\nFirst, image_encode. Then, text_encode. And finally, detect.\n\nWhen saved, I get one model asset with three callable functions inside.\n\nNow, lets load and run the pre-computed asset.\n\nFirst, I see all the flowers segmented as expected.\n\nAnd here's the payoff of the three-function split. I swapped the prompt to butterfly and only re-ran the text encoder and the detector.\n\nAs a result, the second inference is 76% faster, even after warmup. This shows the benefit of re-authoring.\n\nSo, here's what you can do today. Convert your PyTorch models using Core AI's Python libraries. Optimize them with coreai-opt, and use the debugger when you need to understand what's happening inside. Build on top of the examples in coreai-models. And plug in Core AI Skills into your favorite AI agent to leverage the new framework like an expert. I look forward to seeing what models you bring to the platform! Thank you!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:27",
+ "title": "Define and export a PyTorch model",
+ "language": "swift",
+ "code": "import torch\nimport torch.nn as nn\n\n# Define a simple model\nclass MLP(nn.Module):\n def __init__(self):\n super().__init__()\n self.fc1 = nn.Linear(256, 512)\n self.fc2 = nn.Linear(512, 10)\n\n def forward(self, x):\n return self.fc2(torch.relu(self.fc1(x)))\n\n# Export with torch.export\nmodel = MLP().eval()\nexample_input = (torch.randn(1, 256),)\nexported_program = torch.export.export(model, example_input)"
+ },
+ {
+ "timestamp": "4:02",
+ "title": "Convert, optimize and run inference with Core AI",
+ "language": "swift",
+ "code": "import coreai\nimport coreai_torch\nfrom coreai.runtime import NDArray\n\n# Convert to Core AI\nconverter = coreai_torch.TorchConverter()\nconverter.add_exported_program(\n exported_program,\n input_names=[\"features\"], output_names=[\"logits\"])\ncore_ai_program = converter.to_coreai()\n\n# Optimize and save to .aimodel\ncore_ai_program.optimize()\nasset = core_ai_program.save_asset(\"mlp.aimodel\")\n\n# Run inference\nspecialized_model = await AIModel.load(\"mlp.aimodel\")\nspecialized_function = specialized_model.load_function(\"main\")\nresult = await specialized_function({\"features\": NDArray(example[0].numpy())})"
+ },
+ {
+ "timestamp": "21:12",
+ "title": "Define a SiLU Metal kernel with PyTorch reference",
+ "language": "swift",
+ "code": "import torch\nfrom coreai_torch.dsl import TorchMetalKernel, MetalParameter\n\ndef silu_torch(x):\n return x * torch.sigmoid(x)\n\nSILU_MSL = \"\"\"\nfloat val = float(x[gid]);\nfloat sig = 1.0f / (1.0f + exp(-val));\ny[gid] = TYPE(val * sig);\n\"\"\"\n\nsilu_kernel = TorchMetalKernel(\n name=\"fused_silu\",\n input_names=[\"x\"],\n result_names=[\"y\"],\n src=SILU_MSL,\n torch_defn=silu_torch,\n metal_params=[MetalParameter(\"gid\", \"uint\", \"thread_position_in_grid\")],\n template_dtypes={\"x\": \"TYPE\"},\n)"
+ },
+ {
+ "timestamp": "22:09",
+ "title": "Use a custom Metal kernel and convert with TorchConverter",
+ "language": "swift",
+ "code": "class MyModel(torch.nn.Module):\n def __init__(self):\n super().__init__()\n self.linear = torch.nn.Linear(256, 256)\n\n def forward(self, x):\n h = self.linear(x)\n n = h.numel()\n return silu_kernel(\n h,\n threads_per_grid_size=(n, 1, 1),\n threads_per_thread_group=(min(n, 256), 1, 1),\n result_shapes=[h.shape],\n )\n\nexported_program = torch.export.export(MyModel(), (torch.randn(1, 256),))\n\nconverter = coreai_torch.TorchConverter()\nconverter.register_custom_kernels([silu_kernel])\nconverter.add_exported_program(exported_program,\n input_names=[\"x\"], output_names=[\"y\"])\ndeployable = converter.to_coreai() # MSL integrated into asset"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Core AI PyTorch Extensions",
+ "url": "https://apple.github.io/coreai-torch"
+ },
+ {
+ "title": "Core AI Python",
+ "url": "https://apple.github.io/coreai-torch/main/coreai-core"
+ },
+ {
+ "title": "Core AI Optimization",
+ "url": "https://apple.github.io/coreai-optimization"
+ },
+ {
+ "title": "Inspecting, debugging, and profiling Core AI models",
+ "url": "https://developer.apple.com/documentation/CoreAI/inspecting-debugging-and-profiling-core-ai-models"
+ },
+ {
+ "title": "Inspecting Core AI models with Core AI Debugger",
+ "url": "https://developer.apple.com/documentation/CoreAI/inspecting-core-ai-models-with-core-ai-debugger"
+ },
+ {
+ "title": "Core AI",
+ "url": "https://developer.apple.com/documentation/CoreAI"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/325/5/8d08c9d4-3c64-49e1-8590-8b76bd9ad4cb/downloads/wwdc2026-325_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/325/5/8d08c9d4-3c64-49e1-8590-8b76bd9ad4cb/downloads/wwdc2026-325_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "233",
+ "year": "2026",
+ "title": "Explore distributed inference and training with MLX",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/233"
+ },
+ {
+ "id": "328",
+ "year": "2026",
+ "title": "Explore numerical computing in Swift with MLX",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/328"
+ },
+ {
+ "id": "232",
+ "year": "2026",
+ "title": "Run local agentic AI on the Mac using MLX",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/232"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:21.125Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-326.json b/data/wwdc/videos/2026-326.json
new file mode 100644
index 0000000..476b344
--- /dev/null
+++ b/data/wwdc/videos/2026-326.json
@@ -0,0 +1,90 @@
+{
+ "id": "326",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/326/",
+ "title": "Integrate on-device AI models into your app using Core AI",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi everyone, welcome! My name is Carina and I am from the Core AI team. Today, let's dive into the world of on-device intelligence.\n\nIn this talk, I will explore how to add some exciting new features to your app with Core AI. I'll show you how I built a language-learning app that uses a vision-transformer model and a large language model working together, running entirely on device.\n\nCore AI is a new set of technologies that lets you bring advanced on-device AI capabilities directly into your apps.\n\nWith Core AI, you can build app experiences where user's data never leaves their device. There's no server to manage, no cost per token, and no latency to the cloud. If you haven't already, check out \"Meet Core AI\". You will learn the high-level ideas behind our framework and design philosophy and the best ways to use our APIs.\n\nLet's start simple. I'm developing an iOS app for students to learn vocabulary in a new language, starting with Mandarin Chinese. I have a set of vocab cards that I've already curated by hand; it gives the word, translation, and example usage. But this is hard to scale. I would need to include all of these statically in my app.\n\nI'd like to bring AI to my app. How cool would it be if students could point their camera at something they see in their garden, or an object on the street, and just ask the app to pull it right out of the scene? From that, it generates a vocab card in the language they're learning. No curated deck can keep up with a curious student. But a camera and an on-device model can.\n\nEvery card features something from their own life. They learn wherever they are, whenever they want, and their collection grows with them. And this runs all locally on device. I will begin by identifying some models that can help power this experience. Then I'll write the code to use those models in the app. Next, I will explore some practical considerations of model deployment. And finally, I'll expand on my idea by building a macOS version of the app, re-using the same code and unlocking some new features with larger models. Let's start with model discovery.\n\nFirst, I need to define the core capabilities of my app. It starts with picture and a prompt from the user on what they want to learn about.\n\nGiven the input, the app needs to highlight and extract what the user requested from the image. This segmented image becomes the graphic on the card.\n\nAnd from the text input in their native language, the app will reason about the word and generate all the vocab information: the translation, the natural example usage in the language being learned, and the English meaning of that usage.\n\nWith these in mind, I have three requirements for my use case. First is content. This app is about real-world learning, so it needs to handle settings like kitchens, streets, and offices. Second is languages. The model architecture needs to support multiple languages from the start. For my initial release, I'm scoping to Mandarin Chinese.\n\nThird is device constraints. Everything runs on-device on iPhone, so I need to keep both storage and memory footprint small. That means being deliberate about model size and how many models I ship.\n\nI explored a few directions here, reading through model documentation, running some prototypes, and bouncing ideas off an AI assistant.\n\nThe conclusion was clear: decompose the problem into two small models.\n\nThe first is a dedicated vision model that handles image segmentation. The second is a multi-lingual large language model that takes that English label and generates vocab, translation, and example sentences.\n\nWhy two models on device? Task-specific models give me better quality, smaller individual sizes, and the ability to upgrade them independently. I'm targeting variants under one billion parameters each, which keeps the total on-device footprint manageable.\n\nFor image segmentation, I am interested in SAM 3, the Segment Anything Model 3. SAM 3 is a vision-transformer-based model for promptable image segmentation. It's a powerful model that does exactly what my app needs. A student points their camera at something and SAM 3 isolates the object according to their prompt precisely. It provides a clean cutout for the card graphic. The prompt can provide an English label for the language model.\n\nFor the language model, the flow would be simple: an English label like \"Hummingbird\" goes in, and the model generates vocab information in the target language. So I need four things. Multilingual, so it handles translations accurately. Reasoning, so I get contextual example sentences. Structured output, so it fills typed fields reliably. And compact, so it fits on device alongside the vision model.\n\nMany open source language models have strong reasoning capabilities in this size range. I did some quick tests and Qwen stood out — it supports one hundred nineteen languages and dialects, and it is a reasoning model, which means it can generate contextual examples, not just translations. A great starting point for vocab card generation.\n\nThere is even a 0.6 billion parameter version of the model, which should work great for my app. I found these models and documentation about them on HuggingFace and GitHub. So the next question is: how do I bring them into my app with Core AI? One path is to convert them directly from their PyTorch representation using the Core AI PyTorch extensions package.\n\nI could also incorporate model compression with the Core AI optimization package. To learn more about this process, check out the talk \"Dive into Core AI model authoring and optimization\". In that section we even show how to convert the SAM 3 model!\n\nCore AI has powerful tools for model optimization, conversion, and even direct authoring. However, for many popular models there is an another path.\n\nThe Core AI Models repo is a great resource to check out. It contains many popular models, each with conversion scripts that yield optimized versions of those models in the Core AI format, along with optional platform specific variants. Let's head to the Core AI models repository.\n\nmodels/ is the catalog. Browse what's available, find the model you want, and follow its export recipe. python/ gives you reusable primitives and utilities for exporting. Here I found the SAM 3 and Qwen family models, and I followed the export recipe to get our Core AI models.\n\nNow let's talk about integration.\n\nAfter our model export, we get these .aimodel files in Finder. Let's see what's inside of the SAM3 model.\n\nIn Xcode, I can inspect everything about it. I can see it's 623 MB — I am interested that it targets iOS 27.0 and macOS 27.0 for my use case. You can find useful information about the model, such as the size, metadata, and more.\n\nIf I click into the Functions tab, I can see this model's interface. It actually exposes three separate functions. For instance, let's look at the imageEncode function.\n\nThe input isn't just an image, it's a tensor with a specific shape and data type. And output is a dense feature embedding.\n\nAnother function is detect. It takes those image features plus a text prompt, and outputs raw masks, bounding boxes, and confidence scores. So to use this model directly, I'd need to write all the pre-processing to get my camera frame into the right format and all the post-processing to turn these raw tensors into something meaningful.\n\nThe Core AI Models repository can help me with these model-specific pre- and post-processing tasks. In addition to the models and Python conversion utilities, the repo also hosts a Swift package for a set of runtime libraries. The libraries abstract things such as text encoding on the way in, the mask extraction and labeling on the way out. So instead of wrangling tensor shapes, you just call a clean Swift API.\n\nI already cloned the repo so we can easily add coreai-models as a dependency to my project to try it out.\n\nOnce we add the coreai-models URL as a Swift Package, we can select the CoreAILM and CoreAISegmentation to our app target, as easy as that.\n\nNow let's see the code we write to integrate these two models into my app. CoreAIImageSegmenter imports the image segmentation library that provides the SAM 3 model functionality, which allows us to load the SAM 3 model from disk. Then we perform text-prompted segmentation on an input text prompt, such as \"flower\" and lastly we extract the best segmentation mask.\n\nNow for the language model. To load, it's just one line. I create a CoreAILanguageModel, point it at my model bundle and it's ready. One line — asset loading, engine creation, tokenizer setup — all abstracted away for you.\n\nNotice we're importing FoundationModels here. This is the same framework you may already be familiar with.\n\nHere's the beautiful part. To use it, I create a LanguageModelSession. This is the same API that gives you access to Apple's on-device large language model. The difference is that now you'll pass in your own model to use. Same session.respond to: call, same streaming support, same structured output capabilities. You get the ergonomics of the Foundation Models API with the flexibility of choosing exactly which model runs underneath.\n\nWe also support guided generation. This is important for our use case. Instead of letting the model generate free-form text, I can provide a @Generable macro that describes exactly what a vocabulary card looks like: a word field, a translation field, an example sentence field.\n\nNow let's see it in action. I'll take a photo... and we're waiting. The segmentation hasn't come back yet, so we can't get to card generation. Something is clearly slow here.\n\nI know from my code that I show this spinner when I'm first instantiating my SAM 3 model and sending it a prompt. Let's see what's going on.\n\nI took a trace with the new Core AI instruments, and sure enough there's a model load event right at that point, with a large sub-event for specialization.\n\nSpecialization is the process that prepares a Core AI model for execution on device. When your model is loaded it is checked to see if it has already been specialized and cached. This process can take a significant amount of time for very large models. That is what we were seeing in our instrument trace.\n\nWhile future loads are from the cache and are fast, that first time is something I need to plan for.\n\nHaving that happen right in the middle of the user experience is... probably not great. So when should I do it? I could kick it off at launch or run it in the background but that feels wasteful if the user isn't even interested in this feature yet.\n\nI think a better idea is to create a dedicated first-run experience, where I can move this work to happen while the user is learning about the feature for the first time. This keeps model loading and specialization out of the interactive flow Before I make that change though, I want to step back and think more broadly about my deployment strategy for this feature.\n\nThere are a few things I want to get right. I'm shipping this as an update to my existing app, so I want the feature to be discoverable but not required. Users who try it should have a great experience, and users who don't should feel just as great about the app as before.\n\nMy first-run experience gives me a natural place to explain the feature and prepare for a smooth first launch. But I'd been assuming the models would just be bundled with the app and when I checked, they're adding over 1 GB to my download size. That hits everyone who updates, even people who'll never touch this feature. So instead, I'll have my feature introduction screen include a button that only triggers the model download if the user actually wants to try it. I'll use Background Assets for this. If you want to dig into the details, check out \"Discover Apple-Hosted Background Assets\" from last year's WWDC.\n\nNow let's look at how that plays out. When a user says they want to give the feature a try, I request the model assets and show them the download progress. Once that's done, I kick off specialization.\n\nThe specialization is no longer interrupting the main experience but it's still taking a while. That's a bit of an awkward waiting time for the user experience.\n\nFortunately, Core AI has an awesome feature that can help here. During specialization the model goes through two main transformations. First it goes through a core set of compilation steps. Second, executable artifacts are generated. These artifacts are tied to the device and OS version they were generated on. Of these two steps, compilation is the most expensive and takes the most amount of time.\n\nThe Core AI toolchain lets me do some of that compilation ahead-of-time on my development machine, producing a compiled version of the model. While that compiled model still needs to be specialized for the specific user's device, there is now much less work to do and finishes significantly faster.\n\nThis is done with the coreai-build command. You give it a model as input, and depending on your options, it generates one or more compiled models targeting specific device architectures.\n\nI did this with my model and created a background asset for each compiled model. There is a small amount of code I add to my app to detect the architecture of the device it's running on and then request the appropriate asset based on that.\n\nYou can find all the details in the \"Compiling Core AI models ahead of time\" article on developer.apple.com.\n\nI've integrated this and now we have the ahead-of-time compilation already done. On my desk, I have some rocks I've collected from my travels. Let's see this in action.\n\nNow the model preparation step should be a fraction of what it was before, and the user can get started quickly.\n\nThe model gave me an example usage, and I can save it to my collection.\n\nLet's try a few more objects. Here I have a piece of wood gifted from my college roommate, and a sunflower from my little sister.\n\nThese are meaningful objects to me, and I want to capture them as I learn a new language.\n\nAnd on subsequent inferences, we are using the cached model asset so the user experience is seamless.\n\nSo I've been really enjoying this feature myself. I think it could seriously streamline building more curated card sets. Way easier than typing them out one-by-one. The thing is, I do most of my content creation on my Mac. So... What if I brought this there as well? Let's talk about multiplatform.\n\nHere's what we've built so far on iOS. SAM3 handles segmentation, and Qwen 0.6B model generates the vocab cards. With Core AI, I can reuse all the same code and just build from there on Mac.\n\nOn Mac, I'm not learning one word at a time. I'm curating. I might have a folder of photos from a recent trip, and I want to generate cards for all of them in one go. So I add a batch processing layer on top. What took an afternoon of typing can now be completely automated.\n\nAnd because I have more memory and processing power on the Mac, I can step up to a larger model variant of the same model. More parameters means better reasoning and higher-quality output. For curation, that matters. I can give the model richer prompts, ask for multiple example sentences instead of one, or even have it generate pinyin in Chinese. The same code, calling the same API, just a more capable model underneath.\n\nAnd with longer context, I can go beyond individual cards. I can hand the model an entire category of words and ask it to build a curriculum: sequence them from simple to complex, group them into lessons, and write example sentences that reuse earlier vocab to reinforce what the student already learned. One prompt, and I have a structured lesson plan.\n\nI went on a road trip recently and I'd like to bring in a few photos I took to include in my iOS app.\n\nI want to segment butterflies, rock, flower, lake, bird, etc. Right away, we are parallelizing the workload to segment the photos, to find all objects in all my photos, so I can reuse a photo to create multiple cards. Once that's done, we kick off the generation with our Qwen3 8 billion model. It is a more powerful reasoning model, so you can see that it is thinking before it gives me the outputs. In fact, it is checking whether the pinyin is correct for each word and example usage, since those are easy to mess up. Once that's done, we get cards with multiple images for me to now distribute to my apps, and even a curriculum to help me guide my teaching! There are many new features I'd like to develop, I should get back into developing, because my agents are calling me, so let's wrap up here.\n\nWith Core AI, you can build a multiplatform app experience where your user's data never leaves their device. There's no server to manage, no cost per token, and no latency to the cloud. The models are ready. The tools are ready. With Core AI you have everything you need to bring powerful, private intelligence to every Apple platform. Now, let's go build something powerful on device!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "11:01",
+ "title": "Load and run SAM3 image segmentation",
+ "language": "swift",
+ "code": "import CoreAIImageSegmenter\n\n// Load\nlet segmenter = try await ImageSegmenter(resourcesAt: sam3ModelURL)\n\n// Use\nlet response = try await segmenter.segment(image: inputImage, prompt: \"flower\")\nlet mask = response.segments.first?.mask"
+ },
+ {
+ "timestamp": "11:28",
+ "title": "Load a language model and create a session",
+ "language": "swift",
+ "code": "import FoundationModels\nimport CoreAILanguageModels\n\n// Create model instance\nlet model = try await CoreAILanguageModel(resourcesAt: qwen3ModelURL)\n\n// Create session using the model\nlet session = LanguageModelSession(model: model)\n\n// Generate response\nlet response = try await session.respond(to: \"...\")"
+ },
+ {
+ "timestamp": "12:29",
+ "title": "Generate structured output with @Generable",
+ "language": "swift",
+ "code": "import FoundationModels\nimport CoreAILanguageModels\n\n@Generable\nstruct VocabCard {\n let chineseWord: String\n let englishMeaning: String\n let exampleSentence: String\n}\n\nlet model = try await CoreAILanguageModel(resourcesAt: modelURL)\nlet session = LanguageModelSession(model: model)\nlet response = try await session.respond(\n to: \"Create a vocab card for flower\",\n generating: VocabCard.self\n)\nlet card: VocabCard = response.content"
+ },
+ {
+ "timestamp": "17:22",
+ "title": "Compile a Core AI model ahead of time",
+ "language": "swift",
+ "code": "$ xcrun coreai-build compile MyModel.aimodel --platform iOS"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Core AI PyTorch Extensions",
+ "url": "https://apple.github.io/coreai-torch"
+ },
+ {
+ "title": "Core AI Python",
+ "url": "https://apple.github.io/coreai-torch/main/coreai-core"
+ },
+ {
+ "title": "Core AI Optimization",
+ "url": "https://apple.github.io/coreai-optimization"
+ },
+ {
+ "title": "Core AI",
+ "url": "https://developer.apple.com/documentation/CoreAI"
+ },
+ {
+ "title": "Compiling Core AI models ahead of time",
+ "url": "https://developer.apple.com/documentation/CoreAI/compiling-core-ai-models-ahead-of-time"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/326/5/7ff038e2-12cb-4b92-9f49-1d051db7ce5d/downloads/wwdc2026-326_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/326/5/7ff038e2-12cb-4b92-9f49-1d051db7ce5d/downloads/wwdc2026-326_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "233",
+ "year": "2026",
+ "title": "Explore distributed inference and training with MLX",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/233"
+ },
+ {
+ "id": "328",
+ "year": "2026",
+ "title": "Explore numerical computing in Swift with MLX",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/328"
+ },
+ {
+ "id": "232",
+ "year": "2026",
+ "title": "Run local agentic AI on the Mac using MLX",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/232"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:21.331Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-328.json b/data/wwdc/videos/2026-328.json
new file mode 100644
index 0000000..f703665
--- /dev/null
+++ b/data/wwdc/videos/2026-328.json
@@ -0,0 +1,120 @@
+{
+ "id": "328",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/328/",
+ "title": "Explore numerical computing in Swift with MLX",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Swift"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi! I'm David Koski and I work on MLX Swift. Numerical computing, also called numerical analysis or scientific computing, is a set of techniques and algorithms used to solve mathematical problems. These are often problems that are impractical to solve symbolically or by hand. They need massive amounts of computation. Some applications include simulations in chemistry, biology, physics and financial systems. Other domains include audio and signal processing. Visual applications include rendering, ray tracing, and fractals. Large scale gradient descent can do arbitrary curve fitting. This is the basis of training machine learning models. Today we are going to discuss numerical computing using MLX Swift.\n\nApple platforms have a rich numerical-computing ecosystem. Each existing framework is great at what it's designed for. Accelerate gives you hand-tuned vector primitives on the CPU.\n\nBNNS is the building block layer for neural networks. Metal Performance Shaders give you direct access to GPU kernels.\n\nSwift Numerics adds a Complex type and generic numeric protocols.\n\nSo when do you use MLX Swift? If your primary goal is writing mathematical code with an eye for performance, MLX Swift is a great solution.\n\nThe code you write looks like the math you are implementing without the programming overhead of some of the lower level libraries or the detailed bookkeeping required when manipulating arrays in plain Swift.\n\nHow does it look like math? The core idea is simple. Mathematicians and numerical analysts work with vectors and matrices. Rather than performing operations on single values you are doing it for the entire matrix at once. MLX Swift uses n-dimensional arrays as the central abstraction, like NumPy and many others before it. In fact if you've used NumPy, the API will look very familiar.\n\nMost NumPy code can be translated to MLX Swift with minimal changes.\n\nThis is incredibly expressive without being complicated. You can understand the code when you write it and read it later. Array computing and lazy evaluation are what make two things possible: automatic GPU execution and automatic differentiation.\n\nBest of all, mlx-swift and the entire MLX ecosystem are all open source with an MIT license.\n\nWe welcome issues and PRs and have a very active community, ask questions, fix bugs, and make it better! Here's the plan. I will first introduce MLX Swift with some basic matrix-vector operations.\n\nThen I will dive into three examples showing how MLX Swift makes it easy to translate math into code: computation of the Mandelbrot set, finding the steady state for heat distribution, and finally curve fitting.\n\nFirst up, let's talk about MLX Swift.\n\nHere we show some MLX Swift operations, using the power iteration as an example. Let's walk through it.\n\nWe import MLX and set the matrix size and iteration count, then sample a random matrix and vector from a normal distribution.\n\nNext we build a symmetric matrix by adding B and its transpose.\n\nNotice how close this is to the math.\n\n.T gives the transpose, and plus does matrix addition.\n\nInside the loop, matmul does matrix-vector multiplication, and norm gives the L2 norm.\n\nAgain, the code reads like the math.\n\nHere we also see a key MLX feature: lazy evaluation.\n\nOperations on MLX array objects build a compute graph, and nothing runs until you call ee-val or read a value.\n\nIn a loop like this, we call ee-val each step so the graph stays small.\n\nLazy evaluation is also what powers MLX's function transformations, like grad for automatic differentiation.\n\nFinally, we recover the eigenvalue, and reading the value forces the computation.\n\nLike other array frameworks, MLX Swift code reads almost like the math.\n\nAnd if you actually need all the eigenvalues and eigenvectors of a matrix, the MLX Swift linear algebra package has functions for that too.\n\nLet's move on to the next example.\n\nNext, the Mandelbrot set. It's a classic fractal, and it's also a perfect showcase for array computing where we apply a function over a large grid of points.\n\nThe definition is surprisingly simple. For every point c in the complex plane, you iterate z = z² + c. If the magnitude never exceeds 2, the point is in the set and it's colored black.\n\nIf it diverges, it's colored by how quickly it escapes.\n\nThe beauty of a fractal is that it's self-similar and infinitely detailed, you can pan and zoom forever and the patterns never repeat. Let's start with a plain Swift implementation using scalars.\n\nYou loop over every pixel, and run the Mandelbrot iterations and check for divergence. It works. It's idiomatic Swift. But you're managing a lot of bookkeeping that has nothing to do with the problem. And it runs on the CPU, one point at a time.\n\nLet's look at MLX Swift.\n\nHere it is in MLX Swift. Set up a grid of complex numbers, that's c. Then the loop is just two lines. z = z * z + c applied to every point at once. Count how many iterations each point stays bounded. That's it. The code is a direct translation of the math. The computation is performed over an entire grid of points as easily as it is for a single point.\n\nBy default, the GPU is used, giving us fast performance.\n\nPlain Swift is expressive. You can write numerical computing code naturally. But you're working scalar-at-a-time, so you have to iterate over every point yourself. The bookkeeping can obscure the math.\n\nMLX Swift is built for numerical computing, you operate on arrays rather than scalars. It looks like the math you are trying to express. It runs faster on the GPU, processing all points in parallel. How much faster depends on the exact algorithm, but 10x faster is certainly possible. All this with smaller and simpler code.\n\nMandelbrot was embarrassingly parallel, every point independent. The next example is different: each cell talks to its neighbors. That pattern shows up all over physics, image processing, and neural networks. MLX handles it with a single operation: convolution.\n\nImagine a room with walls and heat sources. We want to know the steady-state temperature everywhere inside. The simplest method to solve this is known as the Jacobi iteration.\n\nModel the temperature as a 2D grid.\n\nEach new iteration averages the neighboring values with a stencil like this.\n\nYou repeat this over and over and the heat spreads out until it reaches a steady state. Notice the update only looks at a small neighborhood, and it's the same recipe at every point, that's exactly what a convolution is. Let's see it in code.\n\nHere's the core of the solver in MLX Swift. Let me walk through it. The kernel is the stencil from the previous slide, written out literally: the four quarters on the neighbors, zeros on the center and corners. The temperature grid starts as the heat sources, a reasonable initial value. Inside the loop, two lines. The first is the physics: conv2d applies the stencil across the entire grid in one call. The second line handles the boundary conditions: which is an elementwise ternary. Wherever the mask says this is a heat source or a wall, keep the fixed value; everywhere else, take the new value from the convolution. That's it.\n\nThe math said average the four neighbors and we implemented that as a single call to conv2d.\n\nJacobi iteration is fast to compute but slow to converge. Heat can move one cell at a time and typically steady state requires N^2 iterations where N is the side of the grid. Much like quicksort does less work than bubble sort, there are algorithms that reach steady state with less work. One of these is called Successive Over-Relaxation, or SOR.\n\nThe equation looks similar to Jacobi iterations. In fact it uses the same convolution kernel. It uses a parameter, omega, which pushes each update further in the direction of change, overshooting slightly to get there faster.\n\nThe overshoot will recover as it iterates. The omega parameter can be computed based on the size of the array and if the optimal value is used, this will converge in N iterations. The other key to the technique is in-place updates. MLX typically produces new arrays rather than updating in place, but a red/black checkerboard pattern where alternating cells are processed can be used to compute new values, giving the same effect.\n\nOn to the code! First, we compute the optimal omega based on the size of the grid.\n\nYou can see how we use omega here, it exactly matches the equation.\n\nWe use the checkerboard masks to update alternating cells in the array.\n\nNow the black cells run the same update, but this time their red neighbors already have fresh values, which is exactly the in-place effect we needed. Repeat this in a loop as before. Let's see the difference.\n\nThe Jacobi and SOR code are nearly identical and they closely match the math.\n\nLet's see how these run.\n\nYou can see Jacobi on the top slowly spread while SOR on the bottom quickly fills the area.\n\nSOR has a striking ripple pattern as it runs, that's the overshooting and correcting in real time.\n\nBy the end, both converge to the same configuration. One more thing. I had to slow SOR down by a factor of 100 just to make it visible. The power of choosing the right algorithm! The first two examples were forward computation: start with inputs, compute outputs. The last one flips that around. You have outputs, data points, and you want to find the parameters that produce them. This is where we will apply a key function transformation provided by MLX Swift: 'grad' for automatic differentiation.\n\nLet's say you have some points and you want to find a function that approximates them.\n\nYou decide the structure of the function you want. It could be a polynomial, a sum of sines, or whatever you like.\n\nFor this example we will use a polynomial, a quadratic that will give us a parabola. You want to minimize the loss.\n\nThe mean of the squared differences between the output of your function and the actual data.\n\nThis is the same core idea behind training every ML model, just on a smaller scale.\n\nTo minimize the loss, we need the gradient with respect to the parameters and an optimization loop to update parameters. We define the function f and the loss, which is mean squared error.\n\nNext we make theta, the coefficients we are going to fit, and transform the loss function into a function that produces the exact gradient with respect to the parameters. We didn't write any derivatives by hand. They were derived by MLX.\n\nIn the optimization loop we evaluate the gradient at the current parameters, take a small step, and call eval to flush the computation graph each iteration so it doesn't grow without bound. That's gradient descent.\n\nHere is what that looks like. The parabola quickly gets close and overshoots the data but settles in to closer and closer approximations.\n\nNow this example was a simple polynomial and we could have used QR from the linear algebra package to fit the curve directly.\n\nGradients work with arbitrarily complex functions. If you need more than just gradients, MLX has a suite of optimization algorithms like SGD, Adam, RMSprop, and more.\n\nI've shown array computing, convolution and grad today, but MLX includes the full numericalcomputing toolkit.\n\nHere is a sample. Linear algebra, FFTs, N-dimensional convolutions, Reductions, Scans, Indexing, Random number generation, and many more.\n\nThere's already a healthy ecosystem of packages built on MLX Swift. The core mlx-swift repo is the framework you've been seeing all session. mlx-swift-lm is where the Swift language-model implementations live. mlx-swift-examples has example programs that you can look at to get started. Examples based on this session will be posted there.\n\nAll of these are open source and are installable with a few lines with Swift Package Manager. MLX isn't only Swift. It's one framework with four front-ends: Swift, Python, C++, and C. Third parties have built even more front-ends if you have needs beyond that. They share the same concepts, the same operations, and the same lazy-evaluation model. The concepts and patterns transfer across them with minimal changes. So you can prototype in Python and ship in Swift. Python also has a broader research ecosystem around it. Projects like mlx-lm and mlx-vlm are worth a look if you want to see what's been built on the Python side. I encourage everybody to take a look at mlx-swift and mlx-swift-examples. mlx-swift has documentation and tests you can explore to see how it works. mlx-swift-examples has a variety of example applications that demonstrate LLM integration, stable diffusion, model training and fine-tuning, and of course the examples shown in this talk.\n\nDo you have numerical computing needs? Or just want to play around with interesting simulations and visualizations? Give it a try! And if something sparks an idea I encourage everybody to participate and contribute. There are open issues that you can try fixing or make a new example program.\n\nThanks for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:04",
+ "title": "Power iteration with MLX Swift arrays",
+ "language": "swift",
+ "code": "import MLX\nlet n = 100\nlet steps = 10\nlet B = MLXRandom.normal([n, n])\nvar v = MLXRandom.normal([n])\n\n// get symmetric matrix A = Bᵀ + B\nlet A = B.T + B\n\n// Power iteration → top eigenvector of A.\n// v ← A v / ‖A v‖\nfor _ in 0 ..< steps {\n let Av = matmul(A, v)\n v = Av / norm(Av)\n eval(v)\n}\n\n// recover the eigenvalue.\n// λ = vᵀ A v\nlet lambda = matmul(matmul(v.T, A), v)\n\nprint(lambda)"
+ },
+ {
+ "timestamp": "5:09",
+ "title": "Mandelbrot set in plain Swift (scalar)",
+ "language": "swift",
+ "code": "// Plain Swift, scalar-at-a-time\nvar counts = Array2D(width: w, height: h)\n\nfor y in 0 ..< h {\n for x in 0 ..< w {\n let c = Complex(xMin + Float(x) * xStep, yMin + Float(y) * yStep)\n var z = Complex.zero\n var limit = maxIterations\n for i in 0 ..< maxIterations {\n z = z * z + c\n if z.lengthSquared > radiusSquared {\n limit = i\n break\n }\n }\n counts[x, y] = limit\n }\n}"
+ },
+ {
+ "timestamp": "5:27",
+ "title": "Mandelbrot set in MLX Swift (array)",
+ "language": "swift",
+ "code": "// Compute the Mandelbrot set on a grid of complex numbers\nimport MLX\n\nlet x = linspace(Float(-2.0), 0.5, count: w)\nlet y = linspace(Float(-1.25), 1.25, count: h).reshaped(h, 1)\nlet c = x + y.asImaginary()\n\nvar z = MLXArray.zeros(like: c)\nvar counts = MLXArray.zeros(c.shape, dtype: .int16)\n\nfor _ in 0 ..< maxIterations {\n z = z * z + c // iterate z ← z² + c\n counts = counts + (abs(z) .< 2) // count bounded iterations\n}"
+ },
+ {
+ "timestamp": "7:27",
+ "title": "Jacobi iteration with conv2d",
+ "language": "swift",
+ "code": "// Jacobi iteration: average the four neighbors\n\n// Convolution weights\nlet kernel = MLXArray(converting: [\n 0, 0.25, 0,\n 0.25, 0, 0.25,\n 0, 0.25, 0,\n]).reshaped(1, 3, 3, 1)\n\n// Initial value\nvar temperature = heatSources\n\n// Run this in a loop until convergence\nlet next = conv2d(temperature, kernel, padding: 1)\ntemperature = which(heatMask, heatSources, next)"
+ },
+ {
+ "timestamp": "9:17",
+ "title": "Successive Over-Relaxation (SOR)",
+ "language": "swift",
+ "code": "// Successive Over-Relaxation: blend the previous and next state\nlet ω: Float = 2.0 / (1.0 + sin(Float.pi / Float(max(M, N))))\n\nlet redMask = checkerboard(rows: M, cols: N, phase: 0)\nlet blackMask = checkerboard(rows: M, cols: N, phase: 1)\n\n// Update red cells using black neighbors\nlet sorRed = ω * conv2d(temperature, kernel, padding: 1) + (1 - ω) * temperature\ntemperature = which(redMask, sorRed, temperature)\ntemperature = which(heatMask, heatSources, temperature)\n\n// Update black cells using (now-updated) red neighbors\nlet sorBlack = ω * conv2d(temperature, kernel, padding: 1) + (1 - ω) * temperature\ntemperature = which(blackMask, sorBlack, temperature)\ntemperature = which(heatMask, heatSources, temperature)"
+ },
+ {
+ "timestamp": "11:13",
+ "title": "Curve fitting with automatic differentiation",
+ "language": "swift",
+ "code": "// Define a loss, then optimize it with autodiff\n// x, y: data points as MLXArrays\nfunc f(_ θ: MLXArray) -> MLXArray {\n θ[0] + θ[1] * x + θ[2] * x ** 2\n}\n\nfunc loss(_ θ: MLXArray) -> MLXArray {\n mean((f(θ) - y) ** 2)\n}\n\nvar θ = zeros([numParams])\nlet gradLoss = grad(loss)\n\nfor _ in 0 ..< steps {\n let g = gradLoss(θ) // ∇L(θ)\n θ = θ - learningRate * g // parameter update\n eval(θ) // force evaluation\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "MLX Swift LM on GitHub",
+ "url": "https://github.com/ml-explore/mlx-swift-lm"
+ },
+ {
+ "title": "MLX Swift Examples",
+ "url": "https://github.com/ml-explore/mlx-swift-examples"
+ },
+ {
+ "title": "MLX Examples",
+ "url": "https://github.com/ml-explore/mlx-examples"
+ },
+ {
+ "title": "MLX Swift",
+ "url": "https://github.com/ml-explore/mlx-swift"
+ },
+ {
+ "title": "MLX LM - Python API",
+ "url": "https://github.com/ml-explore/mlx-lm"
+ },
+ {
+ "title": "MLX Explore - Python API",
+ "url": "https://github.com/ml-explore/mlx"
+ },
+ {
+ "title": "MLX Framework",
+ "url": "https://mlx-framework.org"
+ },
+ {
+ "title": "MLX",
+ "url": "https://ml-explore.github.io/mlx/"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/328/5/51d0ab0a-f401-4514-9f04-6b211897d3e8/downloads/wwdc2026-328_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/328/5/51d0ab0a-f401-4514-9f04-6b211897d3e8/downloads/wwdc2026-328_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "233",
+ "year": "2026",
+ "title": "Explore distributed inference and training with MLX",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/233"
+ },
+ {
+ "id": "232",
+ "year": "2026",
+ "title": "Run local agentic AI on the Mac using MLX",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/232"
+ },
+ {
+ "id": "298",
+ "year": "2025",
+ "title": "Explore large language models on Apple silicon with MLX",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/298"
+ },
+ {
+ "id": "315",
+ "year": "2025",
+ "title": "Get started with MLX for Apple silicon",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/315"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:21.548Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-330.json b/data/wwdc/videos/2026-330.json
new file mode 100644
index 0000000..9c6fc3c
--- /dev/null
+++ b/data/wwdc/videos/2026-330.json
@@ -0,0 +1,104 @@
+{
+ "id": "330",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/330/",
+ "title": "Optimize custom machine learning operations with Metal tensors",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Graphics & Games"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hello, my name is Shiyao. I'm a GPU Software Engineer. Today, I am pleased to guide you through an exploration of Metal tensors, and show you how to write optimized custom ML kernels with TensorOps. Apple platforms provide first-class support for running ML models at every layer of the software stack. High-level frameworks like Core AI and MLX make it easy to deploy your models with minimal code, while lower-level APIs like Metal Performance Shaders provide access to high-performance Metal kernels. These layers all build on the low-level acceleration provided by Metal Performance Primitives and the TensorOps library. There are a few reasons why you might want to work at the Metal level. ML research moves quickly, so you might want to implement custom operations which can plug into a higher level frameworks such as Core AI. You may also need to write Metal kernels if you're contributing to an ML framework such as MLX or llama.cpp or if you're working on a Metal-based application. The easiest way to get started is using the TensorOps library. TensorOps is a Metal Shading Language API which accelerates tensor operations on the GPU, including matrix multiplication and convolution. It automatically uses any available hardware acceleration across all Apple Silicon GPU generations, so you don't need to worry about the differences between hardware generations. In particular, it takes full advantage of the neural accelerator in the M5 chip family.\n\nThe neural accelerator is a new hardware block in M5, located directly in each shader core. It sits alongside the other GPU pipelines and is designed to accelerate dense compute-bound work such as the prefill stage of an LLM.\n\nYou can check out the related sessions to learn the basics of getting started with TensorOps. In this session, I'll build on those basics, starting with best practices for working with quantized data. Then, I'll show you how to build advanced custom operations such as FlashAttention.\n\nLet's dive into the first topic — working with quantized data.\n\nAs we know, state-of-the-art machine learning models are getting larger. The inference stage is typically memory bandwidth bound, so compressing the weights becomes necessary both to better fit models into memory and to save memory bandwidth.\n\nThe standard approach for compressing weights is quantization. The idea is simple — take higher-precision weights and reduce them into lower-precision data types. For example, 16-bit half-precision weights could be compressed down to just 4-bits. These quantized weights are paired with scale factors, which let us scale the quantized values back into the original range when it's time to compute.\n\nIn addition to 16- and 32-bit floating point types, TensorOps now natively supports quantized data types. We added a support for 4- and 8-bit integer types in an update to macOS and iOS 26, and we're extending support to even more data types in macOS and iOS 27. This includes 4- and 8-bit floating point types and 2-bit integer types.\n\nYou can simply create and pass your app's quantized tensors to TensorOps and it will automatically take advantage of any available hardware acceleration.\n\nCreating a tensor with a quantized data type is very similar to creating a regular tensor. You fill in your descriptor's properties like any other tensor, but simply specify a quantized dataType. Then create the tensor by calling newTensorWithDescriptor on your Metal device.\n\nSo that's how you can store your quantized element data. Next, let's talk about the scale factors. In macOS and iOS 27, a single MTLTensor object can now represent your scales alongside your tensor's quantized data as an additional scale plane. This plane supports the popular FP8 E8M0 block-wise scale factor format. Each element of the scale plane applies to a block of elements in the data plane. Declaring the scale plane is similar to declaring a tensor.\n\nFirst, create a descriptor object for the scale plane. Then fill in the dataType and blockFactors. Finally, create an auxiliary plane map to specify that this plane is for scales.\n\nThen simply attach the auxiliary planes map to your original tensorDescriptor. The quantized data, scales, and metadata will all be packed into a single tensor object.\n\nNow let's put this into practice by extending a basic matrix multiplication kernel to support quantization.\n\nMatrix multiplication is the core operation in machine learning workloads. For instance, LLMs perform millions of matrix multiplications during inference.\n\nWe covered the basics of how to write a high performance matrix multiplication kernel with TensorOps in the M5 machine learning talk. The basic approach is to slice the input matrices into smaller tiles, and then perform tile-wise matrix multiplications using TensorOps. This maximizes parallelism and keeps data in the cache.\n\nWe can use quantization to further reduce memory traffic and fit larger models in memory. In the kernel, it helps to define type aliases up front before binding the tensors. Here we declare a scales factor plane with fp8_e8m0_ data type, and a block size of 32 by 1. That means every 32 elements in the data plane share a single element in the scales_plane. Then we declare a full tensor type, specifying an FP8 data type along with the scales_plane. You can simply bind these tensors to buffer binding points. The kernel will then have access to the tensors you've allocated on the host side. Alternatively, if you don't want to create a full MTLTensor on the host, you can create a temporary tensor right on the shader's stack. The syntax is almost identical, just swap the tag tensor_handle with tensor_inline. Then pass your buffer pointers and other metadata to the tensor constructor to create a tensor on the stack.\n\nAs I mentioned earlier, we'll divide the problem over many threadgroups for better parallelism. First, we'll slice out the tile for each threadgroup and then perform the multiplication with TensorOps.\n\nTo do this, simply call slice on your input and output tensors using the threadgroup ID. The data and scales plane will both be sliced simultaneously according to the block size. Setting up the matrix multiplication with quantized tensors is identical to normal tensors. First, set up the matmul2d_descriptor, specifying the tile sizes and other parameters. Then create a matmul2d op, specifying the number of simdgroups in the threadgroup. Then simply pass in your quantized tensors and TensorOps will handle dequantization for you.\n\nIn most cases, you should feed your quantized data straight into TensorOps so that it can automatically utilize any available hardware acceleration. However, if you need to dequantize a custom format, TensorOps still have you covered. The simplest approach is to have each thread load a chunk of quantized data from device memory and dequantize it to f16 values in threadgroup memory. You can then pass it as an inline threadgroup tensor to TensorOps. However, this approach requires extra loads and stores through threadgroup memory. Ideally, we would keep all this data in thread registers instead. You can do this by dequantizing the data into a cooperative tensor, which can now be passed as an input to the matmul2d op. Cooperative tensors distribute their storage across the thread private memory of the threads participating in the matmul operation. So if you can't use quantized tensors directly, you can still skip the round trip through threadgroup memory.\n\nTo recap — Metal tensors natively support a wide range of quantized data types, including the new MX scaling formats and E8M0 scale factors coming in iOS and macOS 27. Note that these new data types have additional alignment requirements compared to the larger data types, so be sure to check the Metal documentation for details.\n\nNow let's take it up a notch — building a full, more complex custom operation with TensorOps.\n\nAttention is at the core of every transformer network, including LLMs. To compute attention, you first multiply two matrices together called Q and K. Next, you compute SoftMax using reductions on the rows of the intermediate matrix.\n\nFinally, you multiply by a third matrix called V. The popular FlashAttention algorithm fuses all of these operations together into a single kernel.\n\nTo implement this with TensorOps, you'll first need to set up a custom simd group mapping so that each simd group owns complete rows of the intermediate matrix. This allows you to compute the SoftMax without exchanging data between simd groups. You can do this using the execution_simdgroup operation scope. This means that each simd group will perform an independent matrix multiplication in parallel.\n\nYou can use the simd group ID to slice your input tiles. We'll use a cooperative tensor to store the intermediate matrix so that we can use it as an input to the next step without writing it to the memory. We'll compute SoftMax on the result.\n\nTo do this, we'll need to compute a couple of reductions on the cooperative tensor. TensorOps includes a reduce_rows function to help with this.\n\nThreads will exchange data amongst themselves to calculate the max for each row. The result is returned in another cooperative tensor.\n\nLet's set it up. First, create a cooperative tensor to store the reduction output. Then pass the source and destination to the reduce_rows function. Here we'll use the max reduction_operation with an initial value of negative INFINITY.\n\nThese two cooperative tensors have different shapes, so to help map between them, TensorOps also includes a map_iterator function. Given an iterator pointing to an element in the 2D tensor, it returns an iterator pointing to the corresponding element in the reduction destination.\n\nFirst, set up a loop over the 2D cooperative tensor using iterators. Then call map_iterator to map each element to its corresponding row max. Finally, dereference these iterators to compute SoftMax and store the result back into the cooperative tensor.\n\nNow we're ready to multiply this cooperative tensor by V. In macOS 26, you would have had to first store it to threadgroup memory. But it's now possible to use cooperative tensors directly as inputs to matmul operations.\n\nTo do this, call get_left_input_cooperative_tensor method, passing the source cooperative tensor as an argument. You can then pass the result as an input to the second matmul operation. One thing to watch out for: not every cooperative tensor can be reused as an input. The layouts may different depending on the data types and other factors. So before you do this, call the is_compatible_as_left or right _input method to check for compatibility.\n\nIf it returns true, you're good to go. If not, you'll need to store and reload the data through threadgroup memory to convert it to the correct layout. Either way, the call to op.run is the same. Those are the key TensorOps features you'll need to build an advanced operation like FlashAttention using TensorOps. Now that we've walked through how to build this operation, let's see how it runs in a real model using Core AI. Core AI provides tools for Python developers to convert Pytorch models to Core AI models, including support for custom Metal kernels. Check out the \"Deep Dive into Core AI Model authoring and Optimization\" session for the details of how to integrate a Metal kernel into a Core AI model.\n\nI've followed the steps outlined in that session to integrate our custom FlashAttention kernel into a Sam3 image segmentation model. We define the body of our custom attention kernel as a string in Python and register the TorchMetalKernel object, shown here.\n\nThen, we replace the default huggingface attention implementation with one that calls our kernel, shown here.\n\nFinally, we load the model from huggingface and export it from PyTorch as an optimized Core AI asset. The export will take a moment to finish.\n\nNow we're ready to do inference.\n\nSam3 performs promptable concept segmentation, so we provide the model with an image and text, and then it responds with a segmentation mask indicating where objects are located in the image. Here, I'm prompting the model to label all pixels containing a car in this image.\n\nOk, now, I'll run the segmentation.\n\nLooking at the final result, we can see the model correctly segmented the image. The car is highlighted in blue, so our attention kernel is fully integrated into the model as expected.\n\nToday, I've covered all the tools you can use to build optimized custom ML kernels on Apple Silicon. From quantized data types, to advanced TensorOps features like cooperative tensors and reductions, to integrating with Core AI. To go further, explore the Metal Performance Primitives documentation for the full API reference, and the programming guide for more performance optimization guidelines. You can also download the TensorOps sample code to see the details that I couldn't cover here. And be sure to check out the related sessions to learn more about Core AI and Metal. Thank you!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:53",
+ "title": "Create a quantized MTLTensor",
+ "language": "swift",
+ "code": "// Creating a tensor with a quantized data type from device\n\n#define RANK 2\n\nMTLTensorDescriptor *tensorDesc = [MTLTensorDescriptor new];\n\ntensorDesc.dataType = MTLTensorDataTypeMetalFloat8E4M3;\ntensorDesc.usage = MTLTensorUsageCompute;\n\nNSInteger dimensions[RANK] = {NumCols, NumRows};\ntensorDesc.dimensions = [[MTLTensorExtents alloc] initWithRank:RANK values:dimensions];\n\nNSError *err = nil;\nid tensor = [device newTensorWithDescriptor:tensorDesc error:&err];"
+ },
+ {
+ "timestamp": "4:48",
+ "title": "Declare a multi-plane tensor with scale factors",
+ "language": "swift",
+ "code": "// Creating a tensor with a scales auxiliary plane from device\n\n#define RANK 2\n\nMTLTensorAuxiliaryPlaneDescriptor *planeDesc = [MTLTensorAuxiliaryPlaneDescriptor new];\nplaneDesc.dataType = MTLTensorDataTypeMetalFloat8UE8M0;\n\nNSInteger blockFactors[RANK] = {32, 1};\nplaneDesc.blockFactors = [[MTLTensorExtents alloc] initWithRank:RANK values:blockFactors];\n\nMTLTensorAuxiliaryPlaneDescriptorMap *auxiliaryPlanes =\n [MTLTensorAuxiliaryPlaneDescriptorMap new];\n[auxiliaryPlanes setDescriptor:planeDesc forPlane:MTLTensorPlaneTypeScales];\n\nMTLTensorDescriptor *tensorDesc = [MTLTensorDescriptor new];\ntensorDesc.dataType = MTLTensorDataTypeMetalFloat8E4M3;\ntensorDesc.usage = MTLTensorUsageCompute;\n\nNSInteger dimensions[RANK] = {NumCols, NumRows};\ntensorDesc.dimensions = [[MTLTensorExtents alloc] initWithRank:RANK values:dimensions];\ntensorDesc.auxiliaryPlanes = auxiliaryPlanes;\n\nNSError *err = nil;\nid tensor = [device newTensorWithDescriptor:tensorDesc error:&err];"
+ },
+ {
+ "timestamp": "6:07",
+ "title": "MSL type aliases for an MXFP8 tensor handle",
+ "language": "swift",
+ "code": "// Type aliases for a MXFP8 multi-plane tensor handle\n\n#include \n\nusing namespace metal;\n\nusing scales_plane = tensor_blockwise;\n\nusing mxfp8_tensor = tensor,\n tensor_handle,\n scales_plane>;\n\nkernel void matmul(mxfp8_tensor matrixA [[buffer(0)]],\n mxfp8_tensor matrixB [[buffer(1)]],\n tensor> matrixC [[buffer(2)]])\n{\n // ...\n}"
+ },
+ {
+ "timestamp": "6:51",
+ "title": "Declare an inline MXFP8 tensor on the stack",
+ "language": "swift",
+ "code": "// Type aliases for a MXFP8 multi-plane tensor inline\n\n#include \n\nusing namespace metal;\n\nusing scales_plane = tensor_blockwise;\n\nusing mxfp8_tensor_inline = tensor,\n tensor_inline,\n scales_plane>;\n\n// Construct tensor on the stack from buffer pointers\nmxfp8_tensor_inline matrixA(dataBufferA,\n dextents(K, M),\n array({ 1, K }),\n scales_plane(scalesBufferA));"
+ },
+ {
+ "timestamp": "7:19",
+ "title": "Slice tensors and run a quantized matmul",
+ "language": "swift",
+ "code": "// Slice the tensors to extract the relevant tile\nauto tA = matrixA.slice(0, tgid.y * TILEM);\nauto tB = matrixB.slice(tgid.x * TILEN, 0);\nauto tC = matrixC.slice(tgid.x * TILEN, tgid.y * TILEM);\n\n// Set up the matmul descriptor\nconstexpr auto descriptor = matmul2d_descriptor(TILEM, // M\n TILEN, // N\n dynamic_length_v, // K\n false, // Left matrix transposed\n false); // Right matrix transposed\n\nmatmul2d> op;\n\n// Run the op — TensorOps handles dequantization automatically\nop.run(tA, tB, tC);"
+ },
+ {
+ "timestamp": "10:27",
+ "title": "Set up simdgroup-scoped QxK multiplication",
+ "language": "swift",
+ "code": "// Setup QxK matrix multiplication op\nconstexpr auto mul_qk_op_desc = matmul2d_descriptor(/* ... */);\nmatmul2d mul_qk_op;\n\n// Slice Q, K, V\nauto tQSlice = tQ.slice(0, sgid * ROWS_PER_SIMD);\nauto tKSlice = tK.slice(0, k);\nauto tVSlice = tV.slice(0, k);\n\n// Create cooperative tensor to store tile of QxK\nauto ctQK = mul_qk_op.get_destination_cooperative_tensor();\n\n// Multiply QxK\nmul_qk_op.run(tQSlice, tKSlice, ctQK);"
+ },
+ {
+ "timestamp": "11:18",
+ "title": "Compute row-wise reduction for SoftMax",
+ "language": "swift",
+ "code": "// Create a cooperative tensor to store row reduction output\nauto ctTileRowMax = mul_qk_op.get_row_reduction_destination_cooperative_tensor<\n decltype(tQSlice),\n decltype(tKSlice),\n float>();\n\n// Compute max over each row of QxK tile\nreduce_rows(ctQK, ctTileRowMax, reduction_operation::max, -INFINITY);"
+ },
+ {
+ "timestamp": "11:56",
+ "title": "Compute element-wise SoftMax with map_iterator",
+ "language": "swift",
+ "code": "// Iterate over elements of QxK tile\n#pragma clang loop unroll(full)\nfor (auto it = ctQK.begin(); it != ctQK.end(); it++) {\n // Fetch row max corresponding to this element\n auto row_it = ctRowMax.map_iterator(it);\n\n // Subtract row max from each element and compute exponent\n *it = exp(*it - *row_it);\n}"
+ },
+ {
+ "timestamp": "12:33",
+ "title": "Reuse cooperative tensor as matmul input",
+ "language": "swift",
+ "code": "constexpr auto mul_sv_op_desc = matmul2d_descriptor(/* ... */);\nmatmul2d mul_sv_op;\n\nif (mul_sv_op.is_compatible_as_left_input(ctQK)) {\n // Directly reuse cooperative tensor as input\n auto ctQKIn = mul_sv_op.get_left_input_cooperative_tensor(ctQK);\n mul_sv_op.run(ctQKIn, tVSlice, ctO);\n} else {\n // Store and reload through threadgroup memory if layout is not compatible\n ctQK.store(tgTensor);\n simdgroup_barrier(mem_flags::mem_threadgroup);\n\n auto ctQKIn = mul_sv_op.get_left_input_cooperative_tensor();\n ctQKIn.load(tgTensor);\n mul_sv_op.run(ctQKIn, tVSlice, ctO);\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Running inline ML operations in a shader with Metal 4",
+ "url": "https://developer.apple.com/documentation/Metal/running-inline-ml-operations-in-a-shader-with-metal-4"
+ },
+ {
+ "title": "Machine learning passes",
+ "url": "https://developer.apple.com/documentation/Metal/machine-learning-passes"
+ },
+ {
+ "title": "Download the Metal Performance Primitives (MPP) Programming Guide",
+ "url": "https://developer.apple.com/download/files/Metal-Performance-Primitives-Programming-Guide.pdf"
+ },
+ {
+ "title": "Metal Performance Shaders",
+ "url": "https://developer.apple.com/documentation/MetalPerformanceShaders"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/330/4/0ff2c290-e47b-4d88-8a8f-0634e11506a4/downloads/wwdc2026-330_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/330/4/0ff2c290-e47b-4d88-8a8f-0634e11506a4/downloads/wwdc2026-330_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "262",
+ "year": "2025",
+ "title": "Combine Metal 4 machine learning and graphics",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/262"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:21.602Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-334.json b/data/wwdc/videos/2026-334.json
new file mode 100644
index 0000000..b663f67
--- /dev/null
+++ b/data/wwdc/videos/2026-334.json
@@ -0,0 +1,98 @@
+{
+ "id": "334",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/334/",
+ "title": "Build AI-powered scripts with the fm CLI and Python SDK",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Design",
+ "SwiftUI & UI Frameworks",
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi! I'm Eric Gourlaouen, an engineer on the Foundation Models Framework team. Today, I'd like to introduce new ways you can leverage the Apple Foundation Models on macOS. At WWDC25, we introduced the Foundation Models Framework in Swift. You can use it to prompt the on-device Apple Foundation Model in your apps. It was introduced along with features like guided generation, to generate structured outputs, and Tool Calling, to let the model interact with the context of your app.\n\nWith macOS 27 and iOS 27 come a number of new features to the framework. Like support for passing images in your prompt. And access to server models, so that your app can leverage any large language model with the same Swift API.\n\nUsing the Foundation Models Framework lets you easily tap into the power of Apple Foundation Models. You can use those models on problems from just text extraction and analysis up to building advanced agentic workflows. And it's easy to set up, with no API key needed and no cloud API costs. But until now, those models were only available from Swift code.\n\nThis year, we're introducing new ways you can access Apple Foundation Models on macOS. We're introducing a new command line tool called fm, and a new Foundation Models SDK for Python. The fm command line tool comes pre-installed with macOS 27. It's a fantastic tool to quickly test prompts, right from a terminal, or to incorporate it in automation. It makes it really easy to test the model with some prompts without rebuilding your project in Xcode.\n\nUsing this command line tool is as easy as opening a terminal window, typing fm respond, typing my prompt, and pressing enter. And after a bit, I'll see the response from the model.\n\nThe Foundation Models SDK for Python is our other new way to access the on-device model. It supports the Foundation Models Framework's core features, like tool calling and guided generation. If you're a Swift developer and you've used the Foundation Models framework, you'll find the API very familiar to you. And if you're a machine learning engineer, you might use more Python than Swift. In that case, using this SDK makes it easy to use the on-device model in your Python code.\n\nPython has a rich ecosystem of open-source packages for machine learning and data science. With the Python SDK, you can write evaluation pipelines in Python and leverage those packages to quantify the quality of your feature. And because Python is a scripting language, it makes it easy to quickly test prompts, see results, and iterate. Let's dive into what's possible with those new options. I'll start by going over the new fm command line tool. We'll go over the basics of using it, and then I'll show you how you can use it to create an automation script. Next, I'll introduce the Python SDK. I'll show you how to interact with the model, and then how to leverage advanced SDK options. I'll then show you through a case study how you can use Python tools to analyze your prompt outputs, and improve the quality of your app.\n\nLet's start by discussing the command line tool fm. Starting from macOS 27, this command line tool comes pre-installed on your Mac. It's available right from your Terminal app. To get started with fm, just open the Terminal and type fm.\n\nYou can see a list of commands that are available. For example, you can use respond to prompt the model and return a response, chat to start an interactive interface, schema to create a schema, and more. To show you what fm is capable of, let's try using fm chat.\n\nWith this new terminal interface, I can start a conversation with the on-device model, right from my terminal. I can start with a first question, then ask a follow-up question.\n\nfm chat comes with a number of commands. For example, with /model, I can switch the conversation to use the Private Cloud Compute model.\n\nOr, with /save, I can save the current conversation to resume later.\n\nInteractive sessions with fm chat are great for getting a first pulse of the model. So if you're exploring a new idea, you can pry the model and see how it performs with your prompts. When you'd rather have inline responses, like in scripts, use the command fm respond instead. Run fm respond with a prompt in a terminal, and you'll receive the response from the model as output.\n\nfm respond has a number of options, like the model option, that lets you prompt the Private Cloud Compute model. Or the image option, to include an image in your prompt. Just like with the Swift framework, I can use the model to produce structured outputs. Using the command fm schema object, I can create a schema, and I can then use it with fm respond with the schema option.\n\nThere's more options that could be useful to you. To check out all the options, use the help option. As you've seen so far, the fm command line tool lets you use either the on-device model, or the Apple Foundation Model on Private Cloud Compute. By default, it uses the on-device model that comes with macOS, and that's always available.\n\nYou can also use the Apple Foundation Model on Private Cloud Compute, which has usage limits. It's a much bigger model than the on-device model, so it will perform better on complex problems.\n\nLet's put together what we learned to solve a practical problem. I just completed a presentation project on my Mac. The folder where I was storing my assets is full of drafts, and I'd like to free up space on disk. I'd like to clean up this folder to only keep the final versions of my assets. I'll use Foundation Models to sort out my files, keep only the essentials, back them up, and move the old ones to my archive disk. I'd like to automate this in a script so that whenever this happens again, I can just rerun this script. Using fm here lets me call into a language model that can sort draft versus final files in my script. So that the script works even if the names are messy and are difficult to sort predictably.\n\nI've prepared a script that uses fm to distinguish draft files from final files, and moves the files accordingly. It's going to sort the working folder.\n\nRight now, this folder has both draft and final files. I'll go ahead and execute the script.\n\nNow that it's complete, I can see that the old files were correctly moved out of the folder. I'll now open the archive folder.\n\nI can see that the draft files were moved there, and, I'll open the backup directory.\n\nI can see that the final files were copied there too as backup. Let's go over the script together to understand how I used fm to sort those files.\n\nWe start by loading a list of the files in the working directory. Next, we prompt the model to sort this list, and provide me with a list of draft files, as well as a list of the final files. I can do this with the fm respond command, passing my instructions and my prompt. To get a structured result from the model, I define a schema using the fm schema object command further up. The structured output will have two fields, a list of final files, and a list of draft files. I then use fm respond's schema option to use this schema to generate the output.\n\nThe output of fm respond contains a result in a JSON that's generated by the model. I can then use this result to first, copy the final files to my backup, and move the draft files to the archive.\n\nThere's more to discover with fm, so check out the tool on macOS 27 today, and try using the tool in automation. Let's talk now about the Python SDK. The Python SDK gives you access to Apple Foundation Models right from Python code. You can install it on a Python environment on your Mac, provided that the Python version is at least Python 3.10, that you have Xcode installed, and that you're using an Apple Silicon Mac. It's installed through pip, or any other package manager of your choice. The Python SDK includes the core features of the framework. If you've already used it in Swift, the APIs and abstractions will quickly feel familiar. You can use it to prompt a model with text inputs and image inputs, and you can use it to stream responses. Just like in Swift, you can use guided generation to have the model generate structured outputs. And you can use tool calling to enable the model to interact with code. Let's go over a practical example. I'm building an app to order groceries, and I'd like to let the user prompt the app using the on-device model. As I'm starting to add features, I'd like to evaluate the accuracy of my prompts. So I'll prototype them in Python, before implementing them in Swift. Prompting the model is done just like in Swift. I start by creating a LanguageModelSession, to which I can pass instructions if I'd like. Then, I call session.respond, passing my prompt as an argument. The result of method contains the output of the model.\n\nJust like in the Swift Framework, I can expose tools to the model to interact with the user's context. For example, I can define a tool that the model can call to fetch the last few orders, so that it can provide more personalized information. Just like in the Swift Framework and in the command line tool, I can also constrain the model to produce structured outputs. For example, in this code, I'm using guided generation to ensure the output of the model is captured in an ItemsSuggestion object. Here, using the fm.generable decorator, I define the desired output structure, and I pass it to fm.respond as the generating argument. One of the main benefits of our Python SDK is easy integration with Python's ecosystem. Let me illustrate this with a use case where we'll use some open-source Python packages to set up an evaluation pipeline. As I'm designing an app to order groceries, one feature I'm working on is the ability to prepare the user's next order. Using a language model, I'd like to predict what users would like to add to their cart based on their previous orders.\n\nAs I'm designing this feature, I'd like to make sure the output reliably works off of the previous orders. And also, that the prediction accounts for any items already in the cart. I've prepared a few different implementations for this feature, each with different prompts. And I'd like to quantify their accuracy, so I can select the best one and make sure it performs well.\n\nTo evaluate their prompt and iterate, Swift developers can leverage the Evaluations framework. It's available with Xcode 27, and it makes it easy to create evaluations, and track the accuracy of your features across multiple iterations. But many data scientists might be more familiar with Python than with Swift. If you fall under this scenario, let me show you how I can perform this analysis in Python by using the Python SDK from a Jupyter Notebook.\n\nFirst, I used a large server model to generate evaluation data. I now have some inputs, and for each of those, data on what I expect in the output.\n\nI'll write a number of implementations that use different prompts. Then, for each of my evaluation inputs, I'll generate outputs using each of those different implementations. I'll then save this data as rows in a Pandas DataFrame. Next, I've designed some judge functions that rely on a server model. They will score each output on the criteria of my choice. I'll then save those metrics in the Pandas DataFrame. I can now generate some charts to see them visually. Let's see it in action.\n\nMy notebook contains evaluation data, with inputs and expected outputs. I prepared three different implementations of how to complete the user's cart. Each of those leverage the on-device model by prompting it differently. The first method uses a very minimal prompt.\n\nThe second one uses a more descriptive prompt, and describes the task more in detail.\n\nAnd the third one has the most comprehensive prompts, and describes a list of rules to the model.\n\nFor each row in my evaluation dataset, I went ahead and generated the outputs for each of those implementations. I then stored those inputs and outputs in a Pandas DataFrame.\n\nI passed this data to a third party model that I'm using as a judge model. Which will score each result on a set of criteria. With the gradings generated, I can use matplotlib to generate charts. So that I can quickly see how each set of prompts performs. Here, since the data has already been generated and graded, I can run this cell and the below to generate the charts.\n\nLet's look at the charts.\n\nFirst, by looking at the errors generated by setup, I can see that the detailed prompt leads to a high percentage of generation errors. This can happen, for example, when we reach the model's max context window size. Next, we can see that the two less detailed prompts tend to lead to excess items added to the cart, while the more detailed one has less excess items. However, with the more detailed prompts, we tend to miss more items that were expected.\n\nThe first prompt also tends to lead to more hallucinated items added to the cart. I can use those insights to iterate on those prompts. With Python, I can make those iterations quickly right from my notebook without having to rebuild the whole project. It makes it so convenient to test and make changes! With this example, we saw how you could generate outputs, grade them, and create charts with the Python SDK. Python has a strong open-source ecosystem of machine learning and data science packages, and we used some of those today in our automation. If you develop automation in Python, I encourage you to explore the ecosystem and see if you can reuse existing packages. Let's wrap this up. We just went over the new ways you can interact with Apple's Foundation Models. I encourage you to try them out today on macOS 27. You can use those tools alongside your Xcode project, as a way to prototype and evaluate prompts. Or you can use them on their own, to use the model in novel ways. To get more familiar with those tools, here are a few next steps that I recommend.\n\nFirst, start by exploring the command line tool from the Terminal app. Explore the different options and features, and try them out. Next, to learn more about how to use the Python SDK, head to the GitHub repository. You'll find some example snippets and some documentation that you can use as a reference on how to build advanced workflows. Once you've gotten the hang of the Python SDK, put this new knowledge in practice to create an evaluation pipeline. Think of a way you can use the model, and after you find some working prompts, quantify the results of the model against an evaluation dataset to measure the effectiveness of your prompts. I hope using those new tools will inspire you to use language models in new and exciting ways. Happy building!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "5:07",
+ "title": "Prompt the on-device model with fm respond",
+ "language": "swift",
+ "code": "$ fm respond \"Provide a basic regex in Swift to parse an email address\"\n# Here is a basic regex to parse an email address in Swift: [...]\n\n$ fm respond \"Provide a comprehensive regex in Swift to parse an email address\" --model pcc\n# [...] Here's a robust Swift implementation using 'NSRegularExpression' to validate a typical email address:\n\n$ fm respond \"What app is the user using in this screenshot?\" --model pcc \\\n\t--image Screenshot.png\n# The user is using the Mail app.\n\n$ fm schema object --name AppsIdentified --string app_names --array > schema.json \n$ fm respond \"What apps are the user actively using in this screenshot?\" \\\n\t--image Screenshot.png --model pcc --schema schema.json\n# {\"app_names\": [\"Messages\", \"Mail\", \"Calendar\"]}\n\n$ fm respond --help"
+ },
+ {
+ "timestamp": "7:55",
+ "title": "Sort files with fm respond and a schema",
+ "language": "swift",
+ "code": "fm schema object --name \"TriagedFileList\" \\\n --string 'final_files' --array \\\n --string 'draft_files' --array > /tmp/schema.json\n\noutput=$(fm respond \\\n --instructions \"I just completed a project, and I need help triaging the latest version of the files from the previous versions. I will give you a list of files. Return a list of the latest files (i.e., all files that, you can infer from their name in the list, are the latest versions), and then return separately a list of all draft files (i.e., all files that weren't considered final).\" \\\n \"This is the list of all files:\\n\\n${files_list}\" \\\n --schema /tmp/schema.json\n)\n\necho \"${output}\" | jq -r '.final_files[]' | while read -r file; do\n cp \"${DIRECTORY_TO_TRIAGE}/${file}\" \"${FINAL_FILES_STORAGE_DIRECTORY}\"\ndone\n\necho \"${output}\" | jq -r '.draft_files[]' | while read -r file; do\n mv \"${DIRECTORY_TO_TRIAGE}/${file}\" \"${DRAFT_FILES_STORAGE_DIRECTORY}\"\ndone"
+ },
+ {
+ "timestamp": "8:54",
+ "title": "Install the Foundation Models Python SDK",
+ "language": "swift",
+ "code": "pip install apple_fm_sdk"
+ },
+ {
+ "timestamp": "10:00",
+ "title": "Create a session and respond to a prompt",
+ "language": "swift",
+ "code": "import apple_fm_sdk as fm\n\nINSTRUCTIONS = \"You're an AI assistant for Cupertino Mart, a grocery store with in-app ordering.\"\n\nasync def answer_question(prompt: str) -> str:\n\tsession = fm.LanguageModelSession(instructions=INSTRUCTIONS)\n return await session.respond(prompt)"
+ },
+ {
+ "timestamp": "10:21",
+ "title": "Define a Tool for the language model",
+ "language": "swift",
+ "code": "class GetPastOrdersTool(fm.Tool):\n name = \"get_past_orders\"\n description = \"Retrieves information about this user's past orders.\"\n\n @fm.generable(\"Past orders query parameter\")\n class Arguments:\n \tnumber_orders: str = fm.guide(\"How many of the last orders to retrieve\")\n\n @property\n def arguments_schema(self) -> fm.GenerationSchema:\n \treturn self.Arguments.generation_schema()\n\nasync def call(self, args: fm.GeneratedContent) -> str:\n\tnumber_orders = args.value(int, for_property=\"number_orders\")\n return await Orders.load_last_orders(user_id=user_id, amount=number_orders)"
+ },
+ {
+ "timestamp": "10:35",
+ "title": "Generate structured output with @fm.generable",
+ "language": "swift",
+ "code": "@fm.generable(\"Suggested items\")\nclass ItemsSuggestion:\n\titem_names: list[str] = fm.guide(\"Names of the suggested items\")\n\nINSTRUCTIONS = \"You're an AI assistant tasked with returning potential grocery items that the user might be interested in.\"\n\nasync def generate_suggested_cart_items(user_input: Optional[str]) -> ItemsSuggestion:\n\tsession = fm.LanguageModelSession(instructions=INSTRUCTIONS, tools=load_tools())\n\tprompt = \"\"\"Using the tools to load the user's previous orders, \\\n return a list of items the user has already ordered \\\n and that they might be interested in again \\\n as they're getting ready to place a new grocery order.\"\"\"\n\tif user_input is not None:\n prompt += f\"\\nAccount for the following request from the user: {user_input}\"\n return await session.respond(prompt, generating=ItemsSuggestion)"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Foundation Models SDK for Python on GitHub",
+ "url": "https://github.com/apple/python-apple-fm-sdk"
+ },
+ {
+ "title": "Foundation Models SDK for Python Documentation on GitHub",
+ "url": "https://apple.github.io/python-apple-fm-sdk/"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/334/4/65b71eea-f323-4f86-9096-889b6da91bdd/downloads/wwdc2026-334_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/334/4/65b71eea-f323-4f86-9096-889b6da91bdd/downloads/wwdc2026-334_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "339",
+ "year": "2026",
+ "title": "Bring an LLM provider to the Foundation Models framework",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/339"
+ },
+ {
+ "id": "242",
+ "year": "2026",
+ "title": "Build agentic app experiences with the Foundation Models framework",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/242"
+ },
+ {
+ "id": "319",
+ "year": "2026",
+ "title": "Build with the new Apple Foundation Model on Private Cloud Compute",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/319"
+ },
+ {
+ "id": "241",
+ "year": "2026",
+ "title": "What’s new in the Foundation Models framework",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/241"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:21.794Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-335.json b/data/wwdc/videos/2026-335.json
new file mode 100644
index 0000000..8b69ed5
--- /dev/null
+++ b/data/wwdc/videos/2026-335.json
@@ -0,0 +1,110 @@
+{
+ "id": "335",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/335/",
+ "title": "Improve your prompts by hill-climbing with Evaluations",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Essentials"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "HI! My name is Marcus, a manager on the Evaluations framework team.\n\nI'm excited to show you how to use Evaluations to improve your intelligence-powered feature. As you probably know by now, using AI in your app is a powerful way to provide new levels of personalization to your users. This technology can add a level of depth to your app that was not previously possible with traditional software. However, it's also a challenge to know whether or not your intelligence-powered feature behaves as you'd expect in all cases. To help with this, we're releasing the Evaluations framework to provide you with the tools you need to ship with confidence. Shipping with confidence takes more than just a framework. The Evaluations framework also allows you to hill climb, which is a process of iteratively improving the quality of your feature using the scores of your evaluation as a guide. Hill-climbing starts with development, that is making some change you want to measure against your existing feature.\n\nOnce all your changes are made, you then need to run the evaluation. And see if the results have passed your expectations.\n\nFrom there, you analyze the results to understand how your feature could be further improved.\n\nLeveraging the hill-climbing process is a great way to systematically improve your feature, but effective hill-climbing takes a little bit more than just following the loop. It also takes a little bit of… Science! So, in this video, I'll walk you through how to improve prompts by following the hill-climbing loop, while incorporating some scientific thinking along the way.\n\nNext, I'll walk you through how to conduct comparative Evaluations to make the process of hill-climbing easier. And finally, we'll go beyond changing prompts, by improving other aspects of an intelligence-powered feature.\n\nBut, before we go any further, this video is about the process of hill-climbing an existing evaluation. That means you've already written the foundations of an evaluation pipeline, which provides a wholistic understanding of the strengths and weaknesses of your intelligence-powered feature.\n\nIf you're unfamiliar with how to do that, go check out our other video, \"Meet the Evaluations framework\". It covers everything you need to know in order to build a great evaluation pipeline.\n\nWith that covered, let's get started.\n\nIn the \"Meet the Evaluations framework\" video we introduced you to Book Tracker. In case you forgot, Book Tracker allows readers to catalog and review books.\n\nRecently, I've been reading a lot of classics, so I've added the books to my catalog. In fact, I just finished reading \"Treasure Island\"! It's such a thought-provoking read that covers the tension between loyalty and betrayal.\n\nOne of Book Tracker's new features is a tagging service, which uses a model to generate tags based on the readers review. While the tags for this review cover the general themes of the book, I feel like something is missing. I would have expected to see tags like \"tense\" or \"morally grey\", which speak to the themes of the story. The tags generated for \"Little Women\" had a similar problem. Tags like poignant has more to do with how the reader felt than the contents of the book. Emotion in book reviews is great, but it should not make it into the list of tags. Also, a tag like quiet-steadiness, which is pulled directly from the review, isn't going to be a very useful tag when I want to search my library later.\n\nIt seems like Book Tracker's tag generator isn't as good as I think it could be.\n\nFortunately, when my colleagues wrote this feature, they wrote an Evaluation, which measures the tags against a set of criteria.\n\nAnd here is my Evaluation for Book Tracker's book tags. I'm particularly interested in how we are judging the quality of my tags so I'll scroll down and take a look.\n\nThe qualitative aspects of my app are captured by the score dimensions type.\n\nRelevance tracks the how well the tags represent information about the book's plot, theme, or other relevant information. Usefulness, measures how good the tags are as search terms.\n\nThe ModelJudgeEvaluator uses the score dimensions and a prompt to generate a score for each set of tags.\n\nMy plan is to add these two books to my Evaluation, and review the tags I get back.\n\nIt will also be a good opportunity to see how my ratings stack up against the ratings of my model judge.\n\nWanting to improve some part of an intelligence powered feature is how the hill-climbing process starts. So, to start the loop you begin in the development phase. Here, you make any changes you need to feature as well as your Evaluation.\n\nIn this case, I'll add my review of \"Treasure Island\" to my Evaluation dataset. And I've done the same thing for \"Little Women\".\n\nWith those two entries now in my dataset, I now want to run my Evaluation and see what tags the model produces and how the judge scores them.\n\nBut, to get there we need to first ask if the Evaluation we ran met our expectations.\n\nAs a reminder, your expectations can be defined using Swift Testing's expect macro.\n\nThat way, you can tell if your expectations are met by whether or not your tests pass.\n\nIn this case, my Evaluation met all of my expectations; however, because I know the tags aren't as good as I'd like them to be, I need to investigate further.\n\nAnd that brings us to the analyze phase.\n\nXcode's new evaluations report gives me in depth information about my last Evaluation run.\n\nTo see more details I can click on my BookTaggingEvaluation run.\n\nThis brings up the Evaluation detail view.\n\nOn the top are our aggregate metric charts.\n\nAnd below is our table of results.\n\nWhat I'd like to do now is compare the response of the model to the list of expected tags I generated before. I can do that by opening up the Assistant Editor.\n\nAnd now I can see detailed information about the tags generated for each item in my dataset.\n\nI want to focus on the difference between what the model generated and what I expected.\n\nI can review that in detail in this table. The collection of terms isn't bad but it leaves out a number of key details from the story which the user might want to search for.\n\nBecause of that, I would have scored these tags a 4 for relevance and a 2 for usefulness.\n\nMy model judge also gave the tags a relevance score of 4 which is great, but it also gave usefulness a score of 4, which isn't right.\n\nI should see if I feel the same way about the tags for my review of \"Little Women\".\n\nThe tags don't really contain all the useful information that I'd expect.\n\nAnd it turns out I disagree with the judge's scores here as well.\n\nOnce again, I think relevance should be a 4 and usefulness should be a 2.\n\nDoing this analysis has made it clear to me that there is a discrepancy between how my model judge and I rate tags.\n\nThis discrepancy between model and human is known as drift, and it is a problem faced by all developers trying to evaluate intelligent features. Here's why. Say I have an evaluation with 10 samples. I then ask a model judge and a person to rate each sample.\n\nThe model and person then give their ratings on a scale from 1 to 4, and at the end we average those scores to build an aggregate.\n\nIf the model and the human tend to disagree in their ratings, then their average scores will diverge from one another, hence the name drift.\n\nAs your data set continues to grow and grow the drift will get wider and wider. At which point, it'll be hard for you to know whether or not your feature is being properly evaluated.\n\nTo help with this, you can align your judge to a person's expert opinion. Now that we know drift is a problem, we need a way to know how much our model judge has drifted from our expert ratings.\n\nOne way to accomplish this would be to line up the ratings of the expert and mark where the two match. You can then use this to generate a percentage. This percentage is called accuracy, and it is a great way to measure alignment if every value in your scoring scale is equally likely to appear. However, it's more likely that your dataset will contain values that have an uneven distribution of scores. Think about it, datasets often contain examples of high quality output.\n\nTherefore it is often the case that a human rater is likely to rate items in the dataset with higher scores.\n\nIf a model then happens to judge your smaller dataset with high scores, it may seem like the two are aligned. But then when unleashed on a larger dataset with more variations in scores, it's tendency to score high will still result in drift.\n\nSo we need an alternative to accuracy, one that accounts for the weighted nature of our dataset and the chance that the model might guess the right answer. Fortunately there is a solution! Cohen's kappa coefficient is a mathematical formula made popular by statistician and psychologist Jacob Cohen in 1960.\n\nCohen's kappa measures alignment, that is how often do two raters agree.\n\nTo do that, we need to know what percentage of the time the raters agreed, better known as accuracy.\n\nAnd this is exactly what the accuracy metric from before was calculating. But now we need to calculate a new value. Coincidence, which represents the chance that one rater might get lucky and happen to align.\n\nThis luck is then weighted based on the chances certain answers are more likely to appear. So now the question is, how do you calculate it? To calculate alignment, we start with our accuracy score.\n\nFrom the accuracy score we subtract the possibility of two raters randomly agreeing.\n\nFinally, we divide the difference by the inverse of random agreement, namely the chance that the two raters intentionally agreed.\n\nThe result of that gives us alignment.\n\nCohen's kappa is a powerful way to measure the alignment between a model judge and your expert opinion. I can use this to hill climb the alignment scores between me and my model judge.\n\nSo now, we start back at the beginning of hill-climbing loop in the develop phase.\n\nTo do this, I am going to set up an evaluation to compare my ratings against my judge and produce and alignment score. To do that, I need to write an evaluation, which is made up of four components. First is my dataset.\n\nThen the subject of my evaluation. Then, I need to define my evaluators. And finally, I need to aggregate my results. So let's start with the dataset.\n\nFor this evaluation to work properly, both my model judge and I need to evaluate the exact same dataset. In this case the model judge reviews tags, so I need to produce a common set of tags for the judge and I to review. And I have just the perfect dataset.\n\nMy evaluation from before contains a collection of reviews and tags.\n\nBecause I ran this evaluation in a test, Xcode generated an attachment containing all the of evaluation data that was generated. I can retrieve that attachment and extract summary and tag pairs. Now, with the summary and tag pairs extracted, I need to add my ratings. After that, I can pass the contents of this file as the input to my evaluation.\n\nNext, I need to capture the subject of my evaluation. Normally, the subject method is for calling API related to your feature, but since the generated model responses are part of our dataset, we can simply return the already generated tags. Now, I need to define my evaluators. As you might have guessed, my evaluator is the exact same model judge evaluator as in our book tags evaluation. This is where the judge provides its rating.\n\nFinally, I need to aggregate my results.\n\nHere is where we compare my ratings against the judge's. To do that, we need to calculate Cohen's kappa, which I can do that with a custom aggregation method. In addition to just Cohen's kappa, I'll also calculate the mean and standard deviation of each score dimension. This will be helpful to know if the scores of the judge are going up or down. Now, I can setup my test with my evaluation. For this test, I've set an expectation that my ratings and the judges ratings should produce an alignment score of 0.6. We've chosen this number because according to statisticians, an alignment score of 0.6 represents a meaningful level of agreement. Now, it's time to evaluate and get a baseline for our alignment. And then determine if my evaluation has passed my expectations.\n\nIt appears that the tests failed, which means my expectations weren't met. So once again, it's time to analyze the results in detail. I now know that my alignment scores didn't match my expectations. I can now go to the evaluation report to get more information. As I expected, the scores for both usefulness and relevance are quite low, meaning my model judge and I aren't aligned.\n\nNow, I want to get more information about how each sample in my dataset performed. To do that, I need to open the assistant and view the results in detail.\n\nAs I scanned through the results, this review of \"Frankenstein\" caught my eye. I can see a pretty large discrepancy between my rating of the tags and the judge's.\n\nIt seems like our judge thinks tags like self-help and self-improvement are relevant to the story. Also psychological is an okay search term, but probably not a term a user is likely to search for.\n\nI then started looking through other items in my dataset that had a similar problem and came across this review of \"The Ramakien\".\n\nThe judge and I agree that these collection of terms are helpful and relevant to the contents of the book.\n\nWhere we disagree is on usefulness.\n\nTerms like visual-dimension and quaint-dignity are way too specific.\n\nSo what's the problem here? I believe the model doesn't have enough knowledge on it's own to distinguish between a good tag and bad one. That's likely because the prompt of my judge doesn't provide enough context. To do that, I need to develop a new prompt. That way I can compare the alignment scores of my current prompt against my scores of my new prompt. Fortunately, in Xcode 27, we've made so you can compare the results of two evaluations against each other. When doing comparisons, some scientific thinking can go a long way. In a science experiment, you have two groups. The control group, which represents the baseline and the experimental group which represents the change we are trying to compare against. We can think of the two versions of our instructions in the same way, where the control group is represented by our base prompt and our experimental group is represented by our newly changed prompt. I now need to create a second version of our evaluation with an experimental prompt. For our baseline, we will use the same evaluation with the same model judge prompt as before. For our experimental prompt, I've written a more thorough description about how to judge the set of tags.\n\nIt starts by providing the judge context about the app and what it's about to be judging. Then it gives examples of good tags.\n\nAs well as ways to identify bad tags.\n\nWith both prompts written, I can add both evaluations to a test suite, which will run both evaluations. So, I'll run that suite now and compare the results. With the evaluation now finished I can return to the evaluation report.\n\nLooks like my alignment scores for relevance improved. While my alignment score for usefulness dropped considerably.\n\nBalancing tradeoffs like this are tricky so I need to think carefully how to proceed.\n\nBut before in depth analysis comes checking if we passed.\n\nAnd my test confirms the obvious, we haven't.\n\nAfter thinking about it further, I am going to keep this prompt change and focus the next round of iteration on improving my usefulness score. Therefore, the most effective way to review my results is to compare the usefulness scores of both judges against one another. To do that, I can use the new comparison view in the evaluation report.\n\nFrom the evaluation report I can open the comparison button and open my baseline evaluation.\n\nHere, I can review the scores of the two prompts side by side.\n\nOne thing that jumped out to me immediately is the discrepancy between usefulness scores of this review of \"Picture of Dorian Gray\". It seems to me that the model may be judging too harshly on usefulness. The usefulness column of the experimental evaluation seems to corroborate my guess.\n\nI noticed that all the scores are either a 3 or 2, which is way too harsh.\n\nI think what could help here is being more specific about how to grade each scoring dimension.\n\nTo do that, I'll need to make some changes to my experimental evaluation.\n\nBut before I can make changes to the experimental evaluation, I applied the new prompt from my experimental evaluation into my baseline. This ensures there's only one different variable.\n\nNamely, the changes to my scoring dimensions. For relevance, I've provided a slightly longer description which emphasizes the need for a genre tag.\n\nAnd here is the one for usefulness. Which emphasizes being more critical of overly specific tags.\n\nAnd once again I'll wait for my evaluation to run.\n\nAnd the scores both improved greatly over the baseline. It looks like these specific scoring dimensions are going to be a lot more helpful.\n\nBut, we've still not quite hit our alignment goals. So now, I need to do another comparison to see where we might be able to make improvements. To analyze further, I've gone back to the experimental evaluation. I want to review the results in detail so I'll bring up the assistant view. Thumbing through the results brought me to the review of \"Moby Dick\".\n\nMy relevance score is starting to align.\n\nBut my usefulness score could still use some work.\n\nWhile some results are looking promising, others are still way off. This review of \"Frankenstein\" continues to give our judge trouble.\n\nWhat I think our judge needs now is some examples of the way I judge things, which should give it a pattern for how to judge according to my scale.\n\nThat means we need another round of hill-climbing.\n\nI've already added the new score dimensions to my baseline evaluation. Now, I've reworked my main judge prompt to give it more detail about the goal of the tag generation feature to help ground the model in the problem space.\n\nFrom there, I've written out a number of examples for the model to use as a guideline for reviewing. I've made sure to only give the model a few examples. By giving it a longer list I am prone to overfit the alignment score, which would make it hard to tell if my judge is actually aligned with me. Now that it's a fair comparison, I need to run my evaluation and view the results. And now finally my scores are over my expected value! Which means I've finally passed and can exit out of the loop! This now means I can be confident that when my model judge provides ratings, I can confidently say that the tags are good or bad according my standards. That means I can now put my judge to work on evaluating Book Tracker's Book Tagging Service. So far we've seen how to hill climb on prompts, making them incrementally better and better, now I'd like to show you how to improve your feature through something other than your prompts.\n\nTo generate its tags, Book Tracker uses the on-device model. We use it because readers tend to be in all kinds of places when cataloging books, so using the on-device model ensures they can generate tags no matter where they are.\n\nWhat I want to do is give the model some more context about the book it's generating tags for. I think the additional context will help the model generate more relevant and useful tags.\n\nBetter still, Book Tracker already has the data needed for this because we store the author's name and book title when they write their review. So, to help the tag generator, I've created a tool to get additional information on the book, which provides the book title and author if they are available. Adding this tool is a form of hill-climbing because we are attempting to improve the quality of our feature through an incremental change.\n\nFor this evaluation we will use the book tagging Evaluation, now with an improved model judge.\n\nBut I need a way to compare the quality of my feature without the tool to the quality of my feature with it. So to do that, I'll need to make a change to Book Tagging Service BookTaggingService now takes a list of tools as input. I also set the default to an empty array so my existing evaluation won't need any changes. But now I need to write a new evaluation to compare the service with the tool to the service without the tool.\n\nHere is the new evaluation I wrote. It's exactly the same as the other evaluation. The only difference is I now pass my new lookup tool in the tools array.\n\nSo all I have to do is define two instances of my evaluation. One without the tool and one with it.\n\nAnd now, let's evaluate it and determine if I'm ready to ship.\n\nWell, my service which uses tools met all my expectations, so things are looking good. But, my dataset for Book Tracker contains only 13 book and review pairs, that doesn't cover the wide variety of books and reviews a user might submit for tagging.\n\nIn addition, I was looking at the results of the evaluation of my service with the tool. I can see that the service with the tool is performing better, however it does seem like my tool isn't being called in all the places I think it needs to. What I really need is a way to tell whether or not my tool has been called in the right situations. Fortunately, the Evaluations framework can help with both of those problems. To learn more about our APIs for evaluating tool usage and generating comprehensive datasets, take a look at the \"Create robust evaluations for agentic apps\" video. There, you'll learn about tool call Evaluators and how to use the Sample Generator API to test the wide variety of uses cases your app might see. Before we wrap up, I'd like to recap what we covered today.\n\nHill-climbing works best when you focus on making one change at a time. To do this, treat every iteration of the loop like a science experiment.\n\nBeing able to isolate your changes will help you to understand how each part of your feature contributes to the overall quality.\n\nKnowing how each part works individually will also help you to know where you might need to make changes to resolve a bug or unwanted pattern later down the line.\n\nSecond, this process takes time. Not every change you make will result in positive change. However, failed experiments tell you just as much as successful ones.\n\nThird, good experiments require creativity. In an intelligent feature there are so many things you can change.\n\nIn your feature you can change the instructions, the tools, as well as the model or models you use to generate responses.\n\nOn the evaluation side you can change the dataset, aggregation methods, and even the evaluators themselves. Everything is fair game. Make sure to consider all of these when thinking about how to hill climb.\n\nFinally, watch out for drift. It can feel a bit meta to evaluate your evaluators but a well tuned model evaluator will save you time in the long run. Models can generate ratings much faster than humans can. So by keeping them aligned, you get useful signal as your dataset grows to cover more and more use cases.\n\nIf you want to learn more about what we've covered here today, you can review the Book Tracker app I've been using as well as the evaluations for aligning the model judge. You can also get a comprehensive rundown of all our new APIs on the developer documentation website. Thank you for taking the time to learn about how to improve your evaluation scores by hill-climbing. Your dedication will pay off as you deliver high quality experiences to your users. Thanks for watching and happy hill-climbing!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:54",
+ "title": "The BookTaggingEvaluation",
+ "language": "swift",
+ "code": "// MARK: - Evaluation\n struct BookTaggingEvaluation: Evaluation {\n func subject(from sample: ModelSample) async throws -> ModelSubject {\n let result = try await BookTaggingService.generateTags(for: sample.promptDescription)\n return ModelSubject(value: result)\n }\n\n // MARK: - Dataset\n var dataset = ArrayLoader(samples:\n Book.sampleBooks.map { book in\n ModelSample(prompt: book.review, expected: BookTags(tags: book.tags))\n }\n )\n\n // MARK: - Evaluators & Metrics\n var tagCount = Metric(\"Tag Count\")\n let hasGenreTag = Metric(\"Has Genre Tag\")\n let noDuplicates = Metric(\"No Duplicates\")\n\n let relevance = ScoreDimension(\n \"Relevance\",\n description: \"\"\"\n Whether each tag describes a quality, theme, or tone of the\n book itself rather than incidental details or the reader's\n personal reactions.\n \"\"\",\n scale: .numeric([\n 4: \"Every tag describes the book itself\",\n 3: \"Most tags describe the book, one picks up a reader reaction or minor detail\",\n 2: \"Most tags are surface details or personal reactions, not book descriptors\",\n 1: \"Tags don't meaningfully describe the book\"\n ])\n )\n\n let usefulness = ScoreDimension(\n \"Usefulness\",\n description: \"\"\"\n Whether tags are at the right granularity for browsing — broad\n enough that multiple books could share the tag, specific enough\n to help filter.\n \"\"\",\n scale: .numeric([\n 4: \"Every tag could group multiple books while still narrowing a search\",\n 3: \"Most tags are at the right level, one is either too broad or too narrow\",\n 2: \"Most tags are too broad to filter or too narrow to group\",\n 1: \"Tags would not help with browsing\"\n ])\n )\n\n var evaluators: Evaluators {\n // 1. Tag count is within the required 3–8 range\n Evaluator { _, subject in\n let count = subject.value.tags.count\n if (count >= 3 && count <= 8) {\n return tagCount.passing(rationale: \"\\(count) tags\")\n }\n return tagCount.failing(rationale: \"Got \\(count) tags, expected 3–8\")\n }\n \n // 2. At least one tag identifies the genre or literary form\n Evaluator { _, subject in\n let tags = subject.value.tags.map { $0.lowercased() }\n let knownGenres = await BookTaggingService.knownGenres\n for tag in tags {\n if knownGenres.contains(tag) {\n return hasGenreTag.passing(rationale: \"Matched \\(tag)\")\n }\n }\n return hasGenreTag.failing()\n }\n\n // 3. No duplicate tags\n Evaluator { _, subject in\n let uniqueCount = Set(subject.value.tags.map { $0.lowercased() }).count\n if (subject.value.tags.count - uniqueCount) > 0 {\n return noDuplicates.failing(rationale: \"Found \\(subject.value.tags.count - uniqueCount) duplicates\")\n }\n return noDuplicates.passing()\n }\n \n // 4. Overall tag quality — groundedness, coverage, specificity\n ModelJudgeEvaluator(\n judge: .default,\n dimensions: [relevance, usefulness],\n prompt: ModelJudgePrompt(\n instructions: \"\"\"\n You are evaluating automatically generated tags for Shelf, a personal\n book tracking app. Users write a short summary of their reading\n experience, and the app generates tags to make their library browsable.\n A good tag describes the book itself — its genre, themes, tone, or\n setting. A bad tag picks up incidental details or the reader's personal\n reactions that don't describe the book.\n \"\"\",\n evaluationTarget: { output in output.tags.joined(separator: \", \") },\n reference: { input, _ in\n [\"Expected Tags\": input.expected?.tags.joined(separator: \", \") ?? \"\"]\n }\n )\n )\n }\n\n // MARK: - Analysis\n func aggregateMetrics(using aggregator: inout MetricsAggregator) {\n aggregator.group(\"Heuristics\") { group in\n group.computeMean(of: tagCount)\n group.computeMean(of: hasGenreTag)\n group.computeMean(of: noDuplicates)\n }\n aggregator.group(\"Quality\") { group in\n group.computeMean(of: relevance.metric)\n group.computeMean(of: usefulness.metric)\n }\n }\n }"
+ },
+ {
+ "timestamp": "4:05",
+ "title": "Refined Relevance & Usefulness score dimensions",
+ "language": "swift",
+ "code": "let relevance = ScoreDimension(\n \"Relevance\",\n description: \"\"\"\n Whether each tag describes the book itself — its genre, themes,\n tone, or setting — rather than the reader's reactions, meta-\n commentary about the review, or facts about the author. A book\n can be \"suspenseful\" (a property of the text); a reader is\n \"exhausted\" (a reaction). Mis-labeling the genre is a serious failure.\n \"\"\",\n scale: .numeric([\n 4: \"Every tag describes the book itself\",\n 3: \"Most tags describe the book, one picks up a reader reaction or minor detail\",\n 2: \"Most tags are surface details or personal reactions, not book descriptors\",\n 1: \"Tags don't meaningfully describe the book\"\n ])\n )\n\n let usefulness = ScoreDimension(\n \"Usefulness\",\n description: \"\"\"\n Whether tags work as library shelf labels — broad enough that\n several books could plausibly share the tag, specific enough to\n meaningfully narrow a search. Standard genre and theme tags work;\n made-up phrases, character names, hyper-specific descriptors, and\n overly generic words like \"interesting\" don't.\n \"\"\",\n scale: .numeric([\n 4: \"Every tag could group multiple books while still narrowing a search\",\n 3: \"Most tags are at the right level, one is either too broad or too narrow\",\n 2: \"Most tags are too broad to filter or too narrow to group\",\n 1: \"Tags would not help with browsing\"\n ])\n )"
+ },
+ {
+ "timestamp": "11:56",
+ "title": "The alignment dataset, extracted to JSON",
+ "language": "swift",
+ "code": "// Model judge alignment dataset\n [\n {\n \"input\": \"I have read this book more times than I can count…\",\n \"response\": \"[\\\"literary-fiction\\\", \\\"historical-fiction\\\", \\\"family-drama\\\", \\\"romantic-drama\\\", \n \\\"character-driven\\\", \\\"emotional-intensity\\\", \\\"multigenerational-narrative\\\", \\\"penned-by-a-woman\\\"]\"\n }\n // ... add your expert ratings to each entry\n ]"
+ },
+ {
+ "timestamp": "12:31",
+ "title": "The judge alignment evaluation: dataset, subject, evaluator",
+ "language": "swift",
+ "code": "// Model judge alignment evaluation\n struct BookTagJudgmentCalibration: Evaluation {\n\n // MARK: Dataset — load the extracted summary/tag pairs\n static let samples: [ModelSample] = {\n guard let url = Bundle(for: BundleToken.self).url(\n forResource: \"BookTaggingEvaluation-extracted\", withExtension: \"json\"),\n let data = try? Data(contentsOf: url) else { return [] }\n // Build ModelSample array (adding expert ratings)\n // ...\n }()\n\n var dataset: some Loader { ArrayLoader(samples: Self.samples) }\n \n // MARK: Capture Subject — tags are already generated, so just return them\n func subject(from sample: ModelSample) async throws -> ModelSubject {\n ModelSubject(value: sample.expected ?? BookTagJudgmentValue(\n tags: [], expertRelevanceScore: 0, expertUsefulnessScore: 0))\n }\n\n // MARK: Evaluators — the same model judge as the book-tags evaluation\n var evaluators: Evaluators {\n ModelJudgeEvaluator(\n judge: .default,\n dimensions: [relevance, usefulness],\n prompt: ModelJudgePrompt(\n instructions: \"You are evaluating automatically generated tags for Book Tracker…\",\n evaluationTarget: { output in output.tags.joined(separator: \", \") },\n reference: { input, _ in\n [\"Expected Tags\": input.expected?.tags.joined(separator: \", \") ?? \"\"]\n }\n )\n )\n }\n }"
+ },
+ {
+ "timestamp": "13:00",
+ "title": "Cohen's kappa aggregation",
+ "language": "swift",
+ "code": "func aggregateMetrics(using aggregator: inout MetricsAggregator) {\n let expertRelevance = Self.samples.map { Double($0.expected?.expertRelevanceScore ?? 0) }\n let expertUsefulness = Self.samples.map { Double($0.expected?.expertUsefulnessScore ?? 0) }\n\n aggregator.group(\"Relevance\") { group in\n group.computeMean(of: relevance.metric)\n group.computeStandardDeviation(of: relevance.metric)\n group.custom(of: relevance.metric, label: \"Relevance Alignment Score\") { judge in\n cohensKappa(ratings1: expertRelevance, ratings2: judge) ?? 0\n }\n }\n aggregator.group(\"Usefulness\") { group in\n group.computeMean(of: usefulness.metric)\n group.computeStandardDeviation(of: usefulness.metric)\n group.custom(of: usefulness.metric, label: \"Usefulness Alignment Score\") { judge in\n cohensKappa(ratings1: expertUsefulness, ratings2: judge) ?? 0\n }\n }\n }"
+ },
+ {
+ "timestamp": "13:24",
+ "title": "The judge calibration test",
+ "language": "swift",
+ "code": "// Model judge alignment tests\n @Suite(\"Book Tag Judge Calibration\")\n struct BookTagJudgmentCalibrationTests {\n static let evaluation = BookTagJudgmentCalibration()\n\n @Test(\"Judge Calibration\", .evaluates(evaluation))\n func evaluateJudgeCalibration() async throws {\n let result = EvaluationContext.current.result\n\n let usefulnessMetric = BookTagJudgmentCalibrationTests.evaluation.usefulness.metric\n let relevanceMetric = BookTagJudgmentCalibrationTests.evaluation.relevance.metric\n\n #expect(result.aggregateValue(.custom(label: \"Relevance: Judge vs Expert\")) > 0.6)\n #expect(result.aggregateValue(.custom(label: \"Usefulness: Judge vs Expert\")) > 0.6)\n }\n }"
+ },
+ {
+ "timestamp": "16:33",
+ "title": "The experimental judge prompt",
+ "language": "swift",
+ "code": "// Experimental evaluation\n struct BookTagJudgmentCalibrationExperimental: Evaluation {\n var evaluators: Evaluators {\n ModelJudgeEvaluator(\n judge: .default,\n dimensions: [relevance, usefulness],\n prompt: ModelJudgePrompt(\n instructions: \"\"\"\n You are an experienced reader and librarian evaluating tags\n automatically generated for Book Tracker... Score the tag set on two\n independent dimensions: Relevance and Usefulness.\n\n ## What a good tag looks like\n - Genre/form, theme/subject, tone/atmosphere, setting/era\n\n ## Common failure modes\n - Reader reactions, meta-commentary, author facts, genre contradictions\n \"\"\", // ← full prompt is ~40 lines; abbreviated here\n evaluationTarget: { output in output.tags.joined(separator: \", \") },\n reference: { input, _ in\n [\"Book Review\": input.promptDescription,\n \"Tags Generated for the Review\": input.expected?.tags.joined(separator: \", \") ?? \"\"]\n }\n )\n )\n }\n }"
+ },
+ {
+ "timestamp": "20:12",
+ "title": "Few-shot worked examples in the judge prompt",
+ "language": "swift",
+ "code": "struct ExperimentalBookTagJudgmentCalibration: Evaluation {\n var evaluators: Evaluators {\n ModelJudgeEvaluator(\n judge: SystemLanguageModel(),\n dimensions: [relevance, usefulness],\n prompt: ModelJudgePrompt(\n instructions: \"\"\"\n You are calibrating with an expert librarian who scores\n automatically generated tags for Book Tracker... Your goal is to\n match how the librarian scores. Use the worked examples to calibrate.\n\n ## Worked examples\n ### Example A — clean fit (Pride and Prejudice)\n Tags: romance, historical-fiction, love, redemption, passion\n Librarian: Relevance 4, Usefulness 4\n\n ### Example E — flat genre contradiction (Frankenstein)\n Tags: horror, science-fiction, ... self-help, self-improvement\n Librarian: Relevance 2, Usefulness 3\n ... (6 examples A–F; keep the set small to avoid overfitting)\n \"\"\", // ← full prompt is ~60 lines; abbreviated here\n evaluationTarget: { output in output.tags.joined(separator: \", \") },\n reference: { input, _ in\n [\"Book Review\": input.promptDescription,\n \"Tags Generated for the Review\": input.expected?.tags.joined(separator: \", \") ?? \"\"]\n }\n )\n )\n }\n }\n\n 9. The BookLookupTool — slides 166–167"
+ },
+ {
+ "timestamp": "22:03",
+ "title": "The BookLookupTool",
+ "language": "swift",
+ "code": "// Book Information Lookup Tool\n struct BookLookupTool: Tool {\n let name = \"lookupBook\"\n let description = \"Looks up the title and author of a book given distinguishing details — such as character names, \n settings, quoted lines, or notable plot points — extracted from a reader's review.\"\n\n @Generable\n struct Arguments {\n @Guide(description: \"Distinguishing details from the review that identify the book, such as character names, \n settings, quoted lines, or notable plot points.\")\n var details: String\n }\n \n @Generable\n struct Output {\n @Guide(description: \"The title of the identified book, or an empty string if no match was found.\")\n var title: String\n\n @Guide(description: \"The author of the identified book, or an empty string if no match was found.\")\n var author: String\n }\n \n func call(arguments: Arguments) async throws -> Output {\n let needles = arguments.details\n .lowercased()\n .split(whereSeparator: { !$0.isLetter && !$0.isNumber })\n .map(String.init)\n .filter { $0.count >= 4 }\n\n let best = Book.sampleBooks\n .map { book -> (book: Book, score: Int) in\n let review = book.review.lowercased()\n let score = needles.reduce(0) { partial, needle in\n partial + (review.contains(needle) ? 1 : 0)\n }\n return (book, score)\n }\n .max(by: { $0.score < $1.score })\n\n guard let match = best, match.score > 0 else {\n return Output(title: \"\", author: \"\")\n }\n return Output(title: match.book.title, author: match.book.author)\n }\n }"
+ },
+ {
+ "timestamp": "22:36",
+ "title": "BookTaggingService with a tools parameter",
+ "language": "swift",
+ "code": "// Book Tagging Service\n struct BookTaggingService {\n static func generateTags(for review: String, tools: [any Tool] = []) async throws -> BookTags {\n let prompt = tagsPrompt(review: review)\n let session = LanguageModelSession(\n model: SystemLanguageModel(guardrails: .permissiveContentTransformations),\n tools: tools,\n instructions: instructions\n )\n let response = try await session.respond(to: prompt, generating: BookTags.self)\n return response.content\n }\n }"
+ },
+ {
+ "timestamp": "22:57",
+ "title": "Evaluation with the lookup tool",
+ "language": "swift",
+ "code": "// Evaluation of tags with tool\n struct BookTaggingWithLookupEvaluation: Evaluation {\n func subject(from sample: ModelSample) async throws -> ModelSubject {\n let result = try await BookTaggingService.generateTags(\n for: sample.promptDescription,\n tools: [BookLookupTool()]\n )\n return ModelSubject(value: result)\n }\n // ... same dataset, evaluators, and aggregation as BookTaggingEvaluation\n }"
+ },
+ {
+ "timestamp": "23:09",
+ "title": "Compare with/without the tool in one suite",
+ "language": "swift",
+ "code": "@Suite(\"Book Tag Evaluations\")\n struct BookTagEvaluationTests {\n static let evaluation = BookTaggingEvaluation()\n static let lookupEvaluation = BookTaggingWithLookupEvaluation()\n\n @Test(\"Book Tag Evaluations\", .evaluates(evaluation, info: evaluationInfo))\n func evaluateBookTagging() async throws {\n let result = EvaluationContext.current.result\n let rangeMetric = BookTagEvaluationTests.evaluation.tagCount\n let dupeMetric = BookTagEvaluationTests.evaluation.noDuplicates\n #expect(result.aggregateValue(.mean(of: rangeMetric)) >= 0.8)\n #expect(result.aggregateValue(.mean(of: dupeMetric)) == 1)\n }\n\n @Test(\"Book Tag Evaluations (with BookLookupTool)\", .evaluates(lookupEvaluation, info: lookupEvaluationInfo))\n func evaluateBookTaggingWithLookup() async throws {\n let result = EvaluationContext.current.result\n let rangeMetric = BookTagEvaluationTests.lookupEvaluation.tagCount\n let dupeMetric = BookTagEvaluationTests.lookupEvaluation.noDuplicates\n #expect(result.aggregateValue(.mean(of: rangeMetric)) >= 0.8)\n #expect(result.aggregateValue(.mean(of: dupeMetric)) == 1)\n }\n }"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Book Tracker: Using Evaluations to evaluate an intelligent feature",
+ "url": "https://developer.apple.com/documentation/Evaluations/book-tracker-using-evaluations-to-evaluate-an-intelligent-feature"
+ },
+ {
+ "title": "Designing effective model-as-judge evaluators",
+ "url": "https://developer.apple.com/documentation/Evaluations/designing-effective-model-judges"
+ },
+ {
+ "title": "Designing specific, measurable criteria in an evaluation suite",
+ "url": "https://developer.apple.com/documentation/Evaluations/designing-evaluation-criteria"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/335/4/a464d330-6aa2-456d-9a07-eae997aef08c/downloads/wwdc2026-335_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/335/4/a464d330-6aa2-456d-9a07-eae997aef08c/downloads/wwdc2026-335_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:22.003Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-338.json b/data/wwdc/videos/2026-338.json
new file mode 100644
index 0000000..a8c6daf
--- /dev/null
+++ b/data/wwdc/videos/2026-338.json
@@ -0,0 +1,73 @@
+{
+ "id": "338",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/338/",
+ "title": "Build live production tools for Apple Immersive Video",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Audio & Video",
+ "Spatial Computing"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi there and welcome to \"Build Live Production Tools, for Apple Immersive Video\". I'm Jared King and I lead the Apple Immersive Video Live Engineering team. Apple Immersive Video is an incredibly exciting medium, and live streaming unlocks entirely new ways for customers to experience sports, music, and entertainment events! Earlier this year, Apple did something remarkable. For the first time fans were transported, courtside to select LA Lakers games — live in Apple Vision Pro, through the Spectrum SportsNet and NBA apps.\n\nReal games and arenas, experienced like you were there! Live Immersive cameras gave fans access to unreachable seats. Data-driven graphics augmented the action and spatial audio, embedded customers inside the roaring crowd. Behind the scenes, a live broadcast platform, built by Apple, powered the events, and transported this unique experience to customers around the world.\n\nMy goal is to inspire you to build the next generation of immersive tools, workflows, and live experiences. For many, live broadcast technology might be new. The systems are complex, but the opportunity to build for this space is enormous. So to set the right foundation… First I'll provide a high level overview of the systems that make up a modern live production pipeline - along with some of the creative tools used across studios, production trucks, and broadcast facilities worldwide to create content.\n\nUnderstanding this foundation will be invaluable as you begin building immersive production tools of your own.\n\nSecond, I'll cover what makes live immersive broadcast different from traditional 2D production. Transporting customers from their homes, directly into an event — introduces an entirely new set of technical challenges. The media formats, production tools, and the way content moves between tools within a production workflow, have been rebuilt.\n\nI'll start today with a top level review of a live production pipeline. Whether you're building for immersive, or traditional 2D broadcast, understanding the fundamental components of the end-to-end system is crucial.\n\nA live production pipeline is a system where video audio and data is captured and creatively produced at one end - what I'll call the production domain, then encoded and streamed live to an audience through what I'll call the delivery domain.\n\nBroadcasts can be large-scale productions, such as TV studios, and broadcast trucks, that manage many live cameras, audio sources, and graphics for sporting or entertainment events, Or, they can be smaller systems. Such as podcast studios, theaters or local music venues where only a few of these elements might be used within a production. Either way, the end goal is the same: live content is captured and produced in the production domain, then encoded and transmitted to the viewer in the delivery domain, broadcasting the event to an audience, in real time. But to keep focused, I'll concentrate primarily on what makes the production domain unique, when building for immersive live. Regardless of scale, most live pipelines rely on many of the same creative tools within the workflow simply scaled in quantity and sophistication, depending on the level of production.\n\nLive Cameras are used to capture video of the scene, or event.\n\nOftentimes, multiple cameras are used on a production, to capture different angles, or points of view.\n\nFor example, in this production, here is camera 1. This is camera 2.\n\nAnd that is camera 3.\n\nGraphics may be generated, and keyed onto the video to provide additional context, and creative flair to the production. These can be elements like: a name - which just appeared as a lower third, a scoreboard, displayed in the top right, or a complex video animation. Like the one down below.\n\nReplay systems are used to record media and replay the footage back out when called for, as part of the live production. Or, they can archive it for later use, in post production and editorial.\n\nTo assemble all these elements, video switchers let operators cut between cameras, overlay graphics, and produce the final creative stream the viewer sees.\n\nOn the audio front, microphones are used to pick up announcers, interviews, musical elements, and other sound sources on the production.\n\nAudio consoles ingest all these sources! And artfully combine them together into the final product or \"mix\" the audience hears! Finally all of these tools need to exchange media with one another. For example: camera feeds - connected into the video switcher inputs, or microphone sources - fed into an audio console.\n\nTo do that, every tool connects through a centralized media router that handles content exchange between them. Think of it as a unified network layer, that lets every device send and receive signals to one another. Now that you've got a handle on the basics of live production, here's where things start to diverge for immersive and where the real story begins! In this format, fidelity, presence, and preserving these through every step of the workflow is everything when you are transporting customers inside of the content. And that translates into truly big numbers! Video resolution is 32 times larger than what is typically used in a 2D broadcast production - in order to match human visual acuity and it's produced at two times the frame rate! Audio mixes supporting Apple Immersive Live are far more resolute than traditional stereo audio or even 5.1 surround sound.\n\nApple Spatial Audio Format, or ASAF mixes, can contain 64 or more channels, in order to immerse the audience in a rich spatial audio experience. These massive formats, and quality requirements - ripple through every part of the production pipeline. Unfortunately, not all the traditional tools, transport methods, and formats, support media at this scale. So, immersive live requires building an entirely different workflow. I'll break down three key concepts that will enable you to begin building tools and exciting new workflows in the ecosystem! First, a media format standard - designed to deliver the required quality while remaining efficient, and practically useful, when building live production tools. Second, a method for transporting immersive content in real time, between production devices, over an important standard, called SMPTE 2110.\n\nFinally, saving a live stream to file, and playing it out again, is a fundamental function within any broadcast. I'll go over how live streams from devices can be saved to file within an application, and played back out again without any compromise to quality. I'll start at the top. Within any production, there are three classes of devices. Devices that output media, for example, a camera, a microphone, or a graphics generator. Devices that ingest media such as a video encoder, or a color grading monitor. And devices that do both, for example, a video switcher, receiving camera sources on its inputs, and switching the different angles to air, on its outputs! In any workflow, all devices have to agree on a unified set of media formats, so they can exchange content between them through the media router, seamlessly, in real time. Like a common language.\n\nTo do that, three existing standards have been combined into one format to support live immersive production.\n\nApple Immersive Live Video is composed entirely of streamed ProRes frames, as opposed to uncompressed video frames, typical of regular broadcast cameras.\n\nProRes is a powerful video codec that strikes an exceptional balance between image quality and bandwidth, reducing video signals to a practical size that can be processed by tools, but maintains the high fidelity required of the image. And because Apple Silicon is optimized for ProRes processing, it's the perfect platform for building production tools and pipelines! To learn more, refer to the \"Apple ProRes\" developer documentation.\n\nASAF audio mixes are composed of standard, uncompressed PCM audio tracks carrying high-order ambisonic beds and spatial audio objects.\n\nMetadata is delivered as per-frame JSON objects that contain elements describing attributes of related video and audio feeds - such as lens calibrations, creative events, spatial audio behavior, and much more.\n\nTogether these three standards define the live, immersive, production format.\n\nAll tools and processes must be compliant with each media type - to ensure interoperability with the rest of the ecosystem! Next, devices need a standardized transport layer, to exchange live video, audio, and metadata feeds between them. To achieve this, live feeds from devices are exchanged as individual SMPTE 2110 media streams — the industry standard, for professional media transport over IP.\n\n\"2110\", as it's commonly known, is widely deployed across broadcast facilities worldwide, and is interoperable with a broad ecosystem of professional tools.\n\n2110 uses multicast RTP or, Real-time Transport Protocol to move media across the network. RTP streams carry timing information user flags, and other metadata, alongside a main media payload.\n\nA 2110 stream will transmit either a video, an audio, or a metadata payload - and the transport of each media type is defined by a sub-standard within the broader 2110 specification. I'll break down how each fits into that model. Immersive ProRes video is transported as 2110-22 streams on the network, the defined standard for compressed media over IP. This 2110-22 flow contains both the left and right eye of the immersive content transmitted as two separate data essences, but contained within the single stream. This means there is no need to frame pack each eye, side by side into a single image raster or produce separate IP streams per eye. This is hugely advantageous, as this eliminates the complex management of independent left and right eye video feeds within the production architecture.\n\nASAF Audio is transported as standard, 2110-30 streams on the network.\n\nThese contain the high order ambisonics and audio object channels that compose ASAF spatial audio mixes.\n\nJSON objects are transmitted per-frame over 2110-41, the standard for the transport of user defined metadata over IP. This carries the important metadata information — like lens calibrations, creative events, and motion data — in real time alongside the -22 video and -30 audio feeds within the production. Lastly, the ability to record feeds, edit them, and play them back out again - for example Instant Replay is a crucial part of any live workflow.\n\nIn traditional 2D workflows, recording live video to file often introduces visual quality loss as content is encoded, decoded, and re-encoded many times throughout a typical workflow. In Apple Immersive Video, even these small reductions in quality can significantly impact the customer experience especially as generational loss, due to multiple cycles of compression and decompression, compound over time.\n\nFortunately, this is solved by the immersive format: Everything is already ProRes! Because live media is natively generated in a file-friendly ProRes payload, recording to disk requires no additional encode, or decode steps as the content moves through the workflow. The same ProRes frames are simply copied directly into MOV files, and read back out again, into live 2110 streams during playout — untouched.\n\nPractically, this means that live content can be produced by a camera, transported between devices, recorded to disk, edited, and played back out live on repeat with no impact to quality throughout the entire process! Save video feeds to QuickTime MOV video tracks, using AVFoundation's AVAssetWriter. The resulting MOV file, now contains the same untouched resolution, framerate, and stereo image data as the live stream, saved to a file that can be used in editorial, replay, or post production.\n\nWhen writing the MOV video track, it's important to set the constant kVTProjectionKind_AppleImmersiveVideo in the AVVideoCompressionPropertiesKey. This is a new VideoToolbox property and will add the correct video extended usage, or vexu, static metadata for Apple Immersive Video to the file, signaling it as immersive to other applications.\n\nSave audio feeds in the usual way — uncompressed PCM carried in the 2110 stream is written directly into the MOV's audio tracks using AVAssetWriter.\n\nFinally, the streamed JSON data is written into the Metadata, Box, Exchange format, or MEBX tracks, within the MOV container, using AVAssetWriter.\n\nPrior to storage, the streamed JSON data must be deserialized, parsed and then Immersive Media Support framework — or IMS — is used to create lens calibration objects, camera IDs, and other metadata objects that are written into the MOV, synchronized with the video and audio. IMS was first introduced in visionOS 26. It enables reading and writing the essential metadata for Apple Immersive Video, and provides capabilities for previewing content in creative workflows. While immersive video and audio is already powered by well known technologies — like AVFoundation, VideoToolbox, and Core Audio — IMS is a powerful framework, purpose-built for Apple Immersive Video. As you're building the next generation of production tools, IMS will be one of the most important frameworks to understand! Check out the \"Immersive Media Support\" developer documentation to learn more.\n\nThen, during a file playback scenario, all processes are reversed. Video, audio, and metadata media types, are read directly from their tracks within the MOV, and retransmitted back into 2110 output streams for use within the wider production — using all the same frameworks and libraries.\n\nNow that you understand the foundations of live immersive production, it's the perfect time to get started! Build your own immersive tools, using frameworks like AVFoundation, VideoToolbox, AudioToolbox, and Immersive Media Support. Every layer of the stack is open for innovation.\n\nDive deeper into the world of 2110 and connect your tools together into a true live workflow. Level up, by visiting the SMPTE website to learn more about the various standards and best practices during network implementation. Finally, be sure to watch: \"Learn about Apple Immersive Video technologies\" and \"Support immersive video playback in visionOS apps\". Together, they provide valuable context around the creative and technical principles behind the format.\n\nThe future of immersive live has just started. While it builds on the foundations of traditional broadcast it introduces entirely new creative and technical possibilities! And many of the best ideas haven't been invented yet! This is your chance to be a part of getting this new format off the ground, and on air. I'll see you next time!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "13:17",
+ "title": "Set compression properties for vexu metadata",
+ "language": "swift",
+ "code": "import VideoToolbox\n\nlet compressionProperties: [String: Any] = [\n // ...\n kVTCompressionPropertyKey_ProjectionKind as String: kVTProjectionKind_AppleImmersiveVideo\n // ...\n]"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "kVTCompressionPropertyKey_ProjectionKind",
+ "url": "https://developer.apple.com/documentation/VideoToolbox/kVTCompressionPropertyKey_ProjectionKind"
+ },
+ {
+ "title": "CMVideoCodecType",
+ "url": "https://developer.apple.com/documentation/CoreMedia/CMVideoCodecType"
+ },
+ {
+ "title": "Apple ProRes RAW White Paper",
+ "url": "https://www.apple.com/final-cut-pro/docs/Apple_ProRes_RAW.pdf"
+ },
+ {
+ "title": "Apple ProRes White Paper",
+ "url": "https://www.apple.com/final-cut-pro/docs/Apple_ProRes.pdf"
+ },
+ {
+ "title": "Immersive Media Support",
+ "url": "https://developer.apple.com/documentation/ImmersiveMediaSupport"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/338/5/4549be24-44c7-4214-ab9b-f21f9ed04691/downloads/wwdc2026-338_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/338/5/4549be24-44c7-4214-ab9b-f21f9ed04691/downloads/wwdc2026-338_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "403",
+ "year": "2025",
+ "title": "Learn about Apple Immersive Video technologies",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/403"
+ },
+ {
+ "id": "296",
+ "year": "2025",
+ "title": "Support immersive video playback in visionOS apps",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/296"
+ },
+ {
+ "id": "10090",
+ "year": "2020",
+ "title": "Decode ProRes with AVFoundation and VideoToolbox",
+ "url": "https://developer.apple.com/videos/play/wwdc2020/10090"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:21.933Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-339.json b/data/wwdc/videos/2026-339.json
new file mode 100644
index 0000000..70952b6
--- /dev/null
+++ b/data/wwdc/videos/2026-339.json
@@ -0,0 +1,148 @@
+{
+ "id": "339",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/339/",
+ "title": "Bring an LLM provider to the Foundation Models framework",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi there!\n\nI'm Christopher Webb, an engineer on the Machine Learning Research team, and I'm excited to talk to you about a new way to use the Foundation Models framework. We previously introduced the Foundation Models framework to give you access to Apple's on-device language model. And now, we're opening up the framework to work with nearly any LLM, local or serverbased. This allows anyone, from large companies to individual developers, to easily build their own model-integration on top of the framework.\n\nThe on-device System Language Model has been rebuilt from the ground up: it's smarter, better at instruction following, and accepts images directly in your prompts. Beyond the system model, we've added three more options. Private Cloud Compute brings you the model behind many Apple Intelligence features: now with reasoning, a 32K token context window, and the privacy guarantees you'd expect.\n\nCore AI lets you run local models efficiently and take advantage of the ANE. And MLX unlocks the thousands of models available via the MLX-Community on Hugging Face.\n\nAnd because these are built on top of a brand new public protocol, developers can bring frontier AI models into their apps using the same framework. Anthropic and Google will soon extend the Foundation Models framework with Swift packages of their own, making state-of-the-art Claude and Gemini models available to all Swift developers. Which ever model you use, Apple's, yours, or the community's, you call them the same way, because every model conforms to the Language Model protocol.\n\nFor app developers, I'll show you how to call any of these models through the same familiar API. For model providers, I'll walk you through how to create a Language Model package of your own.\n\nBut first, let me show you a preview of how easy it is to use.\n\nHere's our on-device Foundation Model. Create it, pass it into a session, and call the respond function. And there are even more model options. If you need more horsepower, try Private Cloud Compute. Just swap the model.\n\nIf you want to ship your own model, just point CoreAI at your resources.\n\nAnd if you want to try the latest open source models, simply pass in a model ID, and let the framework handle the rest.\n\nAnd using a model built on top of the Language Model protocol means you get access to all kinds of great Foundation Models features, like Dynamic Profiles.\n\nFor an overview of everything we’re adding, check out \"What’s new in the Foundation Models framework\".\n\nThe reason it’s possible to swap out models so easily is that every LanguageModel honors the same protocol, the System Language Model, PCC, Core AI, MLX, and those built by the community. If you're a model provider, you should join the fun! Let me show you how.\n\nThere are four steps to bring your model into the framework. We’ll start with packaging. A well-crafted Swift package makes it easy for developers to get started.\n\nThen you implement the protocol by defining the types that describe your model and the EXECutor that runs it.\n\nNext, we’ll talk about how to implement authentication for server-based models, including some best practices.\n\nAnd finally, customization. If you need to tailor the protocol’s building blocks to meet your needs, you can do that. From attaching response metadata, all the way to defining entirely new modalities.\n\nFirst up, packaging.\n\nWe recommend using Swift package manager so that developers can simply add your package as a dependency of their app. We'll cover how to set up Package.swift, and how to publish a release.\n\nAn important consideration is which platforms you'll want to support. Foundation Models supports iOS, macOS, visionOS, and watchOS, allowing developers to create a variety of experiences. We recommend you try to do the same.\n\nAnd because the Foundation Models framework is being released as open source, your package could also be useful to developers who deploy Swift on their servers, so consider supporting Linux too.\n\nThird, your dependencies. Every dependency translates to bytes that a developer ships to their users. Carefully consider what dependencies are linked by your package.\n\nPublishing your package is as easy as creating a git tag. Swift Package Manager is decentralized, so your repo URL is your distribution channel. Developers can paste the URL into Xcode and start integrating your model into their apps. For more, see \"Creating Swift Packages”.\n\nWith the package in place, we move on to the protocol. The protocol is the bridge between your model, and the Foundation Models framework.\n\nThe protocol has two key pieces. The first is LanguageModel. It describes the model to the framework. It declares what the model can do, through capabilities, and provides the configuration the framework needs to set up the model's EXECutor.\n\nThe second piece is LanguageModelExecutor where the work happens. It has an initializer that takes a Configuration, a prewarm function for preparing resources ahead of the first request, and a respond function that streams generation back to the session.\n\nThe Configuration is what links the two types: the Model provides it, and the framework uses it to construct the EXECutor.\n\nNow you've seen the protocol in code, let's build an intuition for how the model's configuration links it to the EXECutor.\n\nEach session holds an EXECutor store.\n\nWhen Model1 arrives, the framework checks the store using the model's configuration, but there's no matching EXECutor.\n\nSo, the LanguageModelSession creates a new EXECutor and stores it.\n\nModel2 produces the same configuration, and because Configuration is Hashable, the framework knows it matches, and resolves to the same EXECutor.\n\nThe configuration is the lookup key, not the model.\n\nModel3 produces a different configuration, so it gets its own EXECutor. Each unique configuration maps to exactly one EXECutor in the store.\n\nSo what does this look like in your code? Here's a LanguageModel implementation. It declares its capabilities and returns the configuration the framework uses to find its EXECutor.\n\nThe Executor is where the real work lives, loading weights, managing resources, and streaming tokens back to the session.\n\nThe framework constructs it from a configuration your model provides, then hands the model in on every request.\n\nThat split is what keeps your Model trivial to construct.\n\nWhen the session deallocates, the store goes with it. Every stored EXECutor gets released, your deinit runs, weights are freed, and connections closed, all automatically. You don't write any of that teardown code yourself.\n\nWithin that lifecycle, your EXECutor has one more function: prewarm. Before a request arrives, the developer can ask the framework to prewarm. It's your chance to do expensive setup ahead of time, like loading weights, opening connections, or anything that would otherwise slow down that first response. Let's look at how to use it.\n\nOne approach is to put that setup in a private helper that loads the weights once and caches them. prewarm calls the helper eagerly, so the weights are ready before the first request arrives. But prewarm isn't guaranteed to run.\n\nEither way, weights load exactly once, and if your EXECutor has no expensive setup, like a server-backed model, prewarm can simply be a no-op.\n\nOnce your respond function is called, your EXECutor goes to work. It converts the transcript of the conversation into the format your model expects.\n\nIt applies the options the developer has set and it streams generation events to the session.\n\nFrom the developer's side, the session is the entire interaction surface. They initialize the model, create the session, call respond, and wait. Your EXECutor and the rest of your package, all of that lives behind the session, out of sight. The developer never sees that machinery, but here's what's happening behind the scenes.\n\nThe framework hands you transcript entries, but your inference engine can only process its native types.\n\nSo your EXECutor sits in the middle, translates the entries into messages your inference engine understands, and passes them along for inference.\n\nWhen your inference engine answers, the same translation runs in reverse: your messages back to transcript entries, streamed to the session.\n\nFor now, let's focus on the transcripts that flow in and out of the EXECutor.\n\nA transcript is the conversation so far, expressed as a sequence of entries. Each entry plays a role. Instructions, set by the developer, prompts, from the user, tool calls your model made, and the outputs they returned, and the responses your model has produced.\n\nZooming back out: your EXECutor's job is to turn each transcript entry into a message your inference engine knows how to read.\n\nSo, what's inside a transcript? Foundation Models defines these six entry types.\n\nYour model defines its own roles. Your EXECutor's job is to map between the two, no matter the shape your model takes.\n\nIn this example, instructions, prompt, and response map to system, user, and assistant.\n\nHere, tool calls, tool outputs, and reasoning all map to assistant too. They're part of what the model did during its turn, and since this model doesn't have dedicated roles for these, we just map them to assistant.\n\nIf your model does define something like a dedicated tool role, you can route there instead. Either way, your EXECutor stays in control. Your EXECutor reads the conversation. But every request carries more than history, it carries the developer's intent for how the model should respond, expressed through two additional properties.\n\nEvery request object can include ContextOptions and GenerationOptions. ContextOptions control what goes into the prompt, like the reasoning level you want the model to use, or a response schema. GenerationOptions control the decoder loop: sampling strategy, temperature, and maximum response length.\n\nHere's what that looks like inside respond. Both types of options come in on the request, your EXECutor pulls them out and passes them along when calling the model. So that's everything coming in: transcript, options, all parsed. Now for the half your developer sees: the response.\n\nOn the response side, there are a few things to send: the text your inference engine generates, any tool calls or reasoning, and the metadata that travels with them. They all go out as events on the channel.\n\nEach chunk that the inference engine emits, a token or tool-call fragment, becomes an event. A textDelta, a toolCallDelta, and so on.\n\nThe framework writes them to the transcript. Foundation Models exposes both one-shot and streaming responses, but the implementation is always streaming; the one-shot API just collects the deltas internally.\n\nSo far we've looked at this from your model's side, events going out as the model produces them. But put yourself in the developer's seat for a moment. They've called respond and they're waiting. What do they need first? Here's your EXECutor's side of the handshake with the developer. There's a deliberate order to it.\n\nFirst, a metadata update, model and request IDs the developer can use for logging and debugging.\n\nThen a usage update, prompt token counts for accounting. Sending these upfront means the developer isn't waiting through the whole stream to learn what each request costs.\n\nFinally, for each token your model produces, send a text delta the moment it arrives. The framework streams those deltas to the session as they arrive, so users see the response appear word-by-word instead of all at once.\n\nEarlier we saw how the framework caches EXECutors by configuration.\n\nIf your integration is stateful, holding a KV cache or persistent session between calls, that caching is what lets you minimize network churn and avoid redoing work. Now let's look at how to design yours to take advantage of that, and how your EXECutor can preserve work across calls. Your EXECutor receives the full transcript on every call to respond. Here's what you processed last time, an instruction, a prompt, and the response you generated.\n\nWhen the next call comes in you compare the new transcript to the one you saved from last time.\n\nIn most cases, new entries have simply been appended, a new prompt after the last response.\n\nWhen that's the case, you can preserve your existing state and only process what's new.\n\nBut sometimes your comparison finds that entries have been removed or modified, for example, when the developer trims older entries to save context.\n\nWhen that happens, you'll need to invalidate back to where the transcripts diverge. The framework gives you the full transcript on every call. Your EXECutor decides what counts as a match, and how to handle any changes. Sometimes your model can't do exactly what the developer asked. When that happens, your EXECutor has two choices: approximate or throw.\n\nBe flexible where you can, and honor the developer's intent.\n\nBut sometimes there's no honest approximation. If a developer sets a token limit, but also specifies a schema with required fields, there might not be a way to satisfy both. So you throw. Foundation Models ships LanguageModelError for exactly these cases: context window overflows, rate limits, refusals, and more. Throw one of these, and any developer who's used the framework already knows how to handle it.\n\nWhen the built-in LanguageModelError cases don't cover your situation, define your own error type. Some failures only make sense in the context of your service: your subscription tiers, your features, your account states. A purpose-built case name carries the intent, so a developer catching it knows exactly what happened. Custom errors are powerful, and sometimes you need them. But each one is a new case developers must learn, catch, and handle in their app. Try to use a built-in LanguageModelError when it fits, and save the custom ones for failures only your service can produce.\n\nWe've finished implementing the protocol requirements. Let's discuss how to handle authentication next.\n\nYour job as a package author is to make it easy for developers to do the right thing. If your initializer takes an API key as a string, developers will be tempted to take the path of least resistance. Instead, help developers do the right thing by offering a token provider or sign in flow.\n\nAnd if your package fetches access tokens on behalf of developers, make sure to persist them securely using Keychain. Credential handling is half the story. Device at-test-ation is the other half. If you're shipping a cloud-based LanguageModel package, this is worth a deep look.\n\nThis related session walks through verifying the device, catching tampered builds, signing payloads, and using Apple's fraud signal to keep bad traffic off your service. Check it out in \"Secure your apps with App Attest\". You've packaged your model, implemented the protocol, and handled authentication. That means you've got a solid package for your LanguageModel, with all the fundamentals covered. Now it's time to differentiate. The protocol gives you room to shape LanguageModelSession around the abilities only your model offers. Response metadata is a lightweight option to attach additional information to your responses, and give developers clear ways to access it.\n\nYou can attach your own custom metadata to the response. Here, after streaming completes, our EXECutor sends tokensPerSecond and timeToFirstToken through the channel.\n\nWe recommend providing utilities or documentation that make it easy for developers to work with your metadata; clear keys, typed accessors, whatever makes sense. Underneath, metadata is just a dictionary. It can contain strings, numbers, and other built-in types. But in some cases, you may need something more flexible.\n\nCustom segments are the answer. You'll define a new segment type, receive it in your EXECutor, and stream results back through the same channel, and the developer never has to leave LanguageModelSession to use them. Custom segment types let you extend the protocol. When a new modality comes along, audio, video, whatever's next, developers have a typed, structured way to send that data to your model.\n\nHere's how it works. First, you'll define a type that conforms to custom segment. Because custom segments are required to be PromptRepresentable, developers can pass it directly in their prompts, just like text.\n\nIn your EXECutor, you'll receive this as a customSegment in the transcript, alongside the text entries you're already handling. When your model responds, you emit the result back through the channel as a custom segment update.\n\nThe segment ID controls whether you're adding a new segment, or updating one you've already started streaming. This gives you full control over how results stream into the app. With custom segments in hand, there's one more thing worth calling out: a recommendation for server-side tools.\n\nServer-side tools are capabilities your model runs on its own, like web search, code execution, or image generation. The model invokes them, the server runs them, and your EXECutor watches the results stream in. We'll walk through three levels of detail, each surfacing more of the tool's work, using web search as an example.\n\nServer-side tools are named, typed values on your model. The developer constructs the model with the tools they want, and your EXECutor receives them through the model on every request, the same way it receives every other capability the model declares.\n\nFirst, the simplest pattern: run the tool privately and stream only the answer back. The tool grounds the model's response, but its work stays inside your EXECutor.\n\nEach text delta you append gets streamed into the transcript by the framework, with no trace of the tool that produced it.\n\nIn addition to grounding the answer on the tool's output, you can also attach additional metadata to the response.\n\nWhen a text delta carries metadata, like a citation, forward both to the channel, and the framework attaches the metadata to the text segment in the transcript.\n\nAnd finally, you can choose to surface the tool's work itself. With custom segments, you forward the tool's structured output to the channel, alongside the text and any metadata, giving apps everything the model produced along the way.\n\nThrough one channel, the events you forward, the metadata you attach, and the custom segments you design, server-side tools shape what apps using your package can show their users.\n\nThere's one more thing to keep in mind: whether you're choosing a package or shipping one, make sure everyone in the chain understands the privacy implications of the model behind it. On-device and cloud-based models have very different privacy characteristics, and your users deserve to know which they're getting.\n\nYou've seen how to bring your model to the framework. These sessions show what developers will build with it. Check out \"Integrate On-Device AI Models into Your App Using Core AI\" for bundling local models directly into an app.\n\n\"Build with the new Apple Foundation Model on Private Cloud Compute\" goes deep on serverscale inference with Apple's privacy guarantees. And \"Build agentic app experiences with the Foundation Models framework\" shows how developers use dynamic profiles to build multi-step, tool-using workflows on top of models like yours.\n\nWe're excited about what's ahead. We hope to see a thriving ecosystem of LanguageModel packages, giving Swift developers the freedom to choose the model that's right for their app. We can't wait to see what you build.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "2:00",
+ "title": "Choose a language model",
+ "language": "swift",
+ "code": "import FoundationModels\nimport MLXFoundationModels\n\n// On-device Apple Foundation Model\nlet model = SystemLanguageModel()\n\n// Private Cloud Compute model\n// let model = PrivateCloudComputeLanguageModel()\n\n// Custom Core AI model\n// let model = try await CoreAILanguageModel(resourcesAt: modelURL)\n\n// Open-source MLX model from HuggingFace\n// let model = MLXLanguageModel(modelID: \"mlx-community/my-model\")\n\nlet session = LanguageModelSession(model: model)\nlet response = try await session.respond(to: \"...\")\nprint(response.content)"
+ },
+ {
+ "timestamp": "3:46",
+ "title": "Configure Package.swift for your model package",
+ "language": "swift",
+ "code": "// Package.swift\n\nlet package = Package(\n name: \"MyModel\",\n platforms: [\n .macOS(.v27), .iOS(.v27), .visionOS(.v27), .watchOS(.v27)\n ],\n products: [\n .library(name: \"MyModel\", targets: [\"MyModel\"])\n ],\n dependencies: [\n .package(url: \"...\", .upToNextMinor(from: \"1.0.0\"))\n ],\n targets: [\n .target(name: \"MyModelRuntime\"),\n // public: LanguageModel conformance\n .target(name: \"MyModel\", dependencies: [\"MyModelRuntime\"]),\n .testTarget(name: \"MyModelTests\", dependencies: [\"MyModel\"])\n ]\n)"
+ },
+ {
+ "timestamp": "4:56",
+ "title": "LanguageModel and LanguageModelExecutor protocols",
+ "language": "swift",
+ "code": "// LanguageModel protocol\n\npublic protocol LanguageModel: Sendable {\n var capabilities: LanguageModelCapabilities { get }\n var executorConfiguration: Executor.Configuration { get }\n}\n\n// LanguageModelExecutor protocol\n\npublic protocol LanguageModelExecutor: Sendable {\n init(configuration: Configuration) throws\n func prewarm(model: Model, transcript: Transcript)\n func respond(\n to request: LanguageModelExecutorGenerationRequest,\n model: Model,\n streamingInto channel: LanguageModelExecutorGenerationChannel\n ) async throws\n}"
+ },
+ {
+ "timestamp": "6:25",
+ "title": "Implement LanguageModel and Executor conformances",
+ "language": "swift",
+ "code": "// LanguageModel conformance\npublic struct MyLanguageModel: LanguageModel {\n typealias Executor = MyLanguageModelExecutor\n\n public var capabilities: LanguageModelCapabilities {\n LanguageModelCapabilities(capabilities: [\n .toolCalling, .guidedGeneration, .reasoning\n ])\n }\n\n public var executorConfiguration: Executor.Configuration {\n Executor.Configuration(/* ... */)\n }\n}\n\n// Executor conformance\npublic struct MyLanguageModelExecutor: LanguageModelExecutor {\n public typealias Model = MyLanguageModel\n\n public struct Configuration: Hashable, Sendable { /* ... */ }\n\n public init(configuration: Configuration) throws { /* ... */ }\n\n public func respond(\n to request: LanguageModelExecutorGenerationRequest,\n model: MyLanguageModel,\n streamingInto channel: LanguageModelExecutorGenerationChannel\n ) async throws { /* ... */ }\n}"
+ },
+ {
+ "timestamp": "7:28",
+ "title": "Manage model resources with prewarm and respond",
+ "language": "swift",
+ "code": "// One approach to managing resources\n\nstruct MyLanguageModelExecutor: LanguageModelExecutor {\n\n private mutating func loadModelIfNeeded() throws -> LoadedWeights {\n let weights = try loadedModel ?? loadWeights()\n loadedModel = weights\n return weights\n }\n\n func prewarm(transcript: Transcript) {\n loadedModel = try? loadModelIfNeeded()\n }\n\n func respond( ... ) async throws {\n let weights = try loadModelIfNeeded()\n // ...generate with 'weights'...\n }\n}"
+ },
+ {
+ "timestamp": "9:00",
+ "title": "Map Transcript entries to model messages",
+ "language": "swift",
+ "code": "// Transcript entries\n\nlet transcript = Transcript(entries: [\n .instructions( ... ), // \"You are a helpful assistant\"\n\n .prompt( ... ), // \"What's the weather in Pittsburgh?\"\n .toolCalls( ... ), // getWeather(location: \"Pittsburgh\")\n .toolOutput( ... ), // 65°F, sunny\n .response( ... ), // \"It's 65°F and sunny in Pittsburgh\"\n\n .prompt( ... ), // \"What's the address of Apple Park?\"\n .response( ... ), // \"One Apple Park Way, Cupertino, CA 95014\"\n])"
+ },
+ {
+ "timestamp": "10:42",
+ "title": "Read generation and context options from the request",
+ "language": "swift",
+ "code": "// Parse generation and context options\n\nfunc respond(\n to request: LanguageModelExecutorGenerationRequest,\n model: MyLanguageModel,\n streamingInto channel: LanguageModelExecutorGenerationChannel\n) async throws {\n let reasoningLevel = request.contextOptions.reasoningLevel\n let temperature = request.generationOptions.temperature\n let maxTokens = request.generationOptions.maximumResponseTokens\n}"
+ },
+ {
+ "timestamp": "11:47",
+ "title": "Stream tokens and metadata through the channel",
+ "language": "swift",
+ "code": "// Streaming text tokens\n\nfunc respond( ... ) async throws {\n // 1. Report metadata\n await channel.send(.response(action: .updateMetadata([\n \"modelID\": \"my-model-2026-06-08\",\n \"requestID\": request.id.uuidString\n ])))\n // 2. Report prompt token usage before generating\n await channel.send(.response(action: .updateUsage(\n input: .init(totalTokenCount: promptTokens, cachedTokenCount: cachedTokens),\n output: .init(totalTokenCount: 0, reasoningTokenCount: 0)\n )))\n // 3. Stream text deltas as the model generates\n for try await token in tokens {\n await channel.send(.response(action: .appendText(token)))\n }\n}"
+ },
+ {
+ "timestamp": "13:33",
+ "title": "Honor the developer's intent or throw",
+ "language": "swift",
+ "code": "// Honor the developer's intention where possible\n\n// The developer set sampling: .greedy, but our service only takes temperature\nif request.generationOptions.sampling?.kind == .greedy {\n serviceRequest.temperature = 0\n}\n\n// Otherwise, throw an error\n\n// The token budget is too small to satisfy the schema\nif let schema = request.schema,\n let budget = request.generationOptions.maximumResponseTokens,\n budget < minimumTokens(for: schema) {\n throw LanguageModelError.unsupportedCapability(\n .init(\n capability: .guidedGeneration,\n debugDescription: \"Token budget too small to satisfy this schema.\"\n )\n )\n}"
+ },
+ {
+ "timestamp": "13:57",
+ "title": "Built-in errors that any model can throw",
+ "language": "swift",
+ "code": "// Built-in errors that any model can throw\n\npublic enum LanguageModelError: LocalizedError, CustomDebugStringConvertible {\n // Transcript grew past the model's context window. Trim entries and retry.\n case contextSizeExceeded( )\n // Too many requests in a short window. Space them out or reduce load.\n case rateLimited( )\n // Model declined to answer. Fall back to a message of your choosing.\n case refusal( )\n // Safety guardrails tripped on the prompt or the response.\n case guardrailViolation( )\n // Model lacks a feature you used, such as guided generation or tools.\n case unsupportedCapability( )\n // Prompt contains content the model can't process (bad files, unknown formats).\n case unsupportedTranscriptContent( )\n // A generation guide (e.g., a regex pattern) isn't supported by this model.\n case unsupportedGenerationGuide( )\n // Prompt asked for output in a language or locale the model doesn't support.\n case unsupportedLanguageOrLocale( )\n // Request timed out before the model produced a response.\n case timeout( )\n}"
+ },
+ {
+ "timestamp": "14:14",
+ "title": "Handle errors from your model executor",
+ "language": "swift",
+ "code": "// Custom errors\n\npublic enum MyModelError: Error, LocalizedError {\n // User hit monthly token limit. Prompt upgrade or wait for reset.\n case exceededSubscriptionTierLimit\n // Model variant isn't enabled on this account.\n case modelNotProvisioned\n // Billing or policy review locked this account.\n case accountSuspended\n\n public var errorDescription: String? {\n switch self {\n case .exceededSubscriptionTierLimit:\n String(localized: \"Your plan limit has been reached.\")\n // ...\n }\n }\n}"
+ },
+ {
+ "timestamp": "16:08",
+ "title": "Attach custom metadata to responses",
+ "language": "swift",
+ "code": "// Attach service-specific performance metadata\n\nlet elapsed = Date().timeIntervalSince(startTime)\nlet tokensPerSecond = Double(tokenCount) / elapsed\nlet timeToFirstToken = firstTokenTime?.timeIntervalSince(startTime) ?? 0\n\nawait channel.send(.metadataUpdate([\n \"tokensPerSecond\": tokensPerSecond,\n \"timeToFirstToken\": timeToFirstToken\n]))"
+ },
+ {
+ "timestamp": "17:05",
+ "title": "Define and use custom Transcript segments",
+ "language": "swift",
+ "code": "// Define a custom segment\npublic struct AudioSegment: Transcript.CustomSegment {\n public var id: String\n public var content: URL\n}\n\n// Pass it in a prompt\nlet recording = AudioSegment(id: UUID().uuidString, content: URL(filePath: \"/path/to/recording.m4a\"))\nlet response = try await session.respond {\n \"Where was Frank Lloyd Wright's original architecture school located?\"\n recording\n}\n\n// Emit a custom segment from the executor\nfor try await event in stream {\n switch event {\n case .audioFileGenerated(let file):\n await channel.send(.response(action: .updateCustomSegment(\n AudioSegment(id: file.id, content: file.url)\n )))\n }\n}"
+ },
+ {
+ "timestamp": "18:09",
+ "title": "Implement server-side tools in your model",
+ "language": "swift",
+ "code": "// Configure server-side tools\npublic struct MyLanguageModel: LanguageModel {\n public struct ServerTool: Sendable {\n public static let webSearch: ServerTool = ...\n }\n public init(serverTools: [ServerTool] = []) { }\n}\n\n// Surface tool results through the channel\nlet client = MyServerClient(serverTools: model.serverTools)\nlet response = try await client.send(prompt: .init(request))\nfor try await chunk in response {\n switch chunk {\n case .webSearch(let webSearch):\n await channel.send(.response(action: .updateCustomSegment(\n WebSearchSegment(url: webSearch.url, content: webSearch.html)\n )))\n case .textDelta(let textDelta):\n await channel.send(.response(action: .appendText(\n textDelta.text, tokenCount: textDelta.tokenCount\n )))\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Foundation Models",
+ "url": "https://developer.apple.com/documentation/FoundationModels"
+ },
+ {
+ "title": "Core AI Models",
+ "url": "https://github.com/apple/coreai-models"
+ },
+ {
+ "title": "MLX Swift LM on GitHub",
+ "url": "https://github.com/ml-explore/mlx-swift-lm"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/339/4/334f1ee9-4263-4c86-9b10-632f0f2edab1/downloads/wwdc2026-339_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/339/4/334f1ee9-4263-4c86-9b10-632f0f2edab1/downloads/wwdc2026-339_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "242",
+ "year": "2026",
+ "title": "Build agentic app experiences with the Foundation Models framework",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/242"
+ },
+ {
+ "id": "334",
+ "year": "2026",
+ "title": "Build AI-powered scripts with the fm CLI and Python SDK",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/334"
+ },
+ {
+ "id": "319",
+ "year": "2026",
+ "title": "Build with the new Apple Foundation Model on Private Cloud Compute",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/319"
+ },
+ {
+ "id": "241",
+ "year": "2026",
+ "title": "What’s new in the Foundation Models framework",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/241"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:22.407Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-341.json b/data/wwdc/videos/2026-341.json
new file mode 100644
index 0000000..6391544
--- /dev/null
+++ b/data/wwdc/videos/2026-341.json
@@ -0,0 +1,80 @@
+{
+ "id": "341",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/341/",
+ "title": "Support the Center Stage front camera in your iOS app",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I’m Tracy! I’m an engineer on the Camera Software team.\n\nI’m excited to show you how to support the Center Stage front camera in your iOS app. I love taking selfies! Whether I’m traveling solo or with a group of friends. Getting the right framing isn’t always easy. If you’re anything like me, you’ve probably reached for a selfie stick or passed your phone to your friend with the longest arms. The Center Stage front camera on iPhone 17, iPhone Air, and iPhone 17 Pro takes care of this. It gives you greater flexibility in framing your shots. I'll start with an overview of the Center Stage front camera. Then, I'll cover APIs to support Center Stage for photos. I'll wrap up with how to support Center Stage for video recordings and video calls.\n\nTraditionally, smartphone front camera sensors have a 4x3 aspect ratio, which limits your framing based on the phone orientation. However, the Center Stage front camera has a square image sensor. This square shape lets you choose any aspect ratio.\n\nYou can take a portrait selfie, or a landscape one, without having to rotate your iPhone. This gives you a more secure, one-handed grip.\n\nAnd since the camera is centered, your eye contact feels more natural.\n\nThis square sensor is also paired with a lens that has a 95 degree field of view, the widest on any iPhone front camera. This wide field of view helps you better frame group selfies, stabilize videos, and keep you centered during video calls.\n\nNow that you know what the Center Stage front camera can do, I’ll show you how to bring perfect photo framing experience into your app. The key feature here is Auto Zoom and Auto Rotate. By combining the square sensor, wide field of view, and automatic face and gaze detection, the Center Stage front camera smartly adapts its orientation and adjusts between narrow and wide views. Let me show you how that works. Here, I’m taking a selfie outside. As my friend Karen joins me, the frame zooms out. As more friends get into the shot, the frame rotates to include everyone. To build this experience in your app, I'll first review the capture session setup, and then dive into two APIs: dynamic aspect ratio, and smart framing monitor. Lastly, I’ll cover sensor orientation compensation. Starting with a typical photo capture setup. First, create an AVCaptureSession.\n\nFind the Center Stage front camera, represented in the API as AVCaptureDevice with the .builtInUltraWideCamera device type.\n\nCreate an AVCaptureDeviceInput for that camera.\n\nTo get a camera preview, add an AVCaptureVideoPreviewLayer to the capture session.\n\nAlso, add an AVCapturePhotoOutput to receive photos.\n\nAs you add inputs and outputs, the capture session implicitly forms AVCaptureConnections between those with compatible media types. With that setup in place, I'll move to dynamic aspect ratio, the building block for both manual and automatic framing.\n\nStarting in iOS 26, AVCaptureDevice includes a dynamicAspectRatio property. When you set this property, the capture device crops your chosen aspect ratio out of the square image sensor as shown earlier, without rebuilding the capture session or interrupting preview. The switch is seamless and quick. Here is a table that lists example formats that support dynamic aspect ratio.\n\nIt requires the front-facing .builtInUltraWideCamera as mentioned before.\n\nIt's only supported on the square formats, with resolutions ranging from 1280 up to 4032.\n\nThere are five aspect ratios available: 3x4, 4x3, 9x16, 16x9, and 1x1.\n\nBe aware that the 4032 photo format only supports 3x4 and 4x3 because these two aspect ratios give you the highest resolution for photos.\n\nWith those format requirements in mind, let me show you how to implement a Tap to Rotate button using dynamic aspect ratio to switch between orientations.\n\nFirst, select the Center Stage front camera by creating an AVCaptureDevice.DiscoverySession.\n\nSpecify .builtInUltraWideCamera as the only device type you’re interested in, and .front as a position.\n\nSince you’ve only asked for a single device, you can just get the first element in the discovery session’s device array.\n\nFind a format that supports your desired aspect ratio. Here, I’ll use 4x3 as an example. Check each format's supportedDynamicAspectRatios property. This returns an array of aspect ratios the format supports. For simplicity, I'm picking the first match.\n\nAfter locking the device for configuration, set your chosen format as the activeFormat.\n\nNext, set the dynamic aspect ratio to 4x3.\n\nThis call returns the timestamp of the first buffer when the change takes effect.\n\nWith that code in place, your app now supports Tap to Rotate, letting you change orientation with a single tap.\n\nNext up is the smart framing monitor API. It works alongside dynamic aspect ratio to automatically adjust framing as people move in and out of the scene.\n\nStarting in iOS 26, AVCaptureDevice includes an AVCaptureSmartFramingMonitor object. This monitor gives periodic framing recommendations based on automatic face and gaze detection.\n\nEach recommendation contains an aspect ratio and a zoom factor that your app can apply or ignore.\n\nSince this monitor is designed for photo capture, it only provides recommendations when you use the 4032 photo format. Now I'll walk through how to set up the monitor in code.\n\nWith the ultra-wide front camera already selected from the dynamic aspect ratio setup, you can go ahead and find a format that supports both your desired aspect ratios and smart framing. Once you've found the format, lock the device for configuration and set the activeFormat.\n\nNext, get the smartFramingMonitor from the camera. By default, the monitor provides no recommendations. Here, I’m setting enabledFramings to all of the supportedFramings. But you can limit this to a subset of supported framings, such as just the 4x3 aspect ratio with narrow and wide zoom factors. After you configure your monitor, key-value observe the monitor’s recommendedFraming property. As the monitor recommends a new framing, apply the recommendation by setting the camera’s dynamic aspect ratio and video zoom factor. For a smooth preview transition, set the aspectRatio first, then the zoomFactor.\n\nNow you can start the monitor at any time, including while your AVCaptureSession is running. If your UI allows people to turn off automatic framing, unregister your key-value observation and stopMonitoring.\n\nYour app now supports Auto Zoom and Auto Rotate, giving people the best framing recommendations for group selfies.\n\nOne last topic for photo capture, sensor orientation compensation. Since the earliest iPhone front cameras, the sensor has been mounted in Landscape Left orientation. If you take a selfie while holding the phone in Portrait orientation, the image buffer is delivered to the photo output in the native sensor orientation.\n\nThe buffer carries an EXIF orientation metadata tag, indicating it should be rotated 270 degrees on playback.\n\nHowever, on iPhone 17, iPhone Air, and iPhone 17 Pro, the Center Stage front camera sensor is mounted in Portrait orientation.\n\nIf your app relies on rotation values that worked before, photos may appear sideways or upside down.\n\nTo handle this, AVCapturePhotoOutput automatically applies sensor orientation compensation by default. It physically rotates photos and updates EXIF metadata before delivering them to your app.\n\nThe resulting photos are in landscape left orientation, just like on previous iPhone front cameras.\n\nYou can keep using the same rotation values you used before.\n\nKeep in mind that this compensation is only applied to HEIC, JPEG, and uncompressed processed photos. It is never applied to Bayer RAW or Apple ProRAW captures. Starting in iOS 26, you can control this behavior with the cameraSensorOrientation- CompensationEnabled property. If you use AVCapturePhotoOutput, test with compensation off for best performance, and make sure photo orientation remains correct.\n\nTo learn more about image rotation handling with AVCaptureRotationCoordinator, check out the \"Support external cameras in your iPadOS app” from WWDC 2023. Now, on to Center Stage for videos. I’ll cover video recordings, and video calls.\n\nThe dynamic aspect ratio API also works great for recordings. You can Tap to Rotate for a wider view. However, QuickTime movie tracks require all samples to have the same dimensions. Video recording will need to stop if you change the dynamic aspect ratio during capture.\n\nHere's the photo capture setup I walked through before. To modify it for video recording, you can use AVCaptureMovieFileOutput instead of AVCapturePhotoOutput.\n\nIn such a setup, recording will stop automatically when the aspect ratio is changed. You can also use AVCaptureVideoDataOutput with AVAssetWriter to record videos. The setDynamicAspectRatio completion timestamp allows you to end the current recording and start a new one with the updated aspect ratio.\n\nVideo recordings also benefit from cinematic stabilization modes: cinematicExtended and cinematicExtendedEnhanced. Both are face-aware on the Center Stage front camera. They prioritize keeping the subject stable over the background. Next, I'll talk about Center Stage for video calls.\n\nHere, I’m on a FaceTime call. When a friend joins me on the trail, the camera automatically widens to include both of us. Center Stage is already supported if your video conferencing app uses the Voice over IP background mode to keep calls connected when the phone is locked.\n\nPeople can directly turn on Center Stage from Control Center's Video Effects menu.\n\nIf your app is not using Voice over IP background mode, you can still adopt the Center Stage API. This is available on the iPhone front camera, starting with iPhone 17, iPhone Air, and iPhone 17 Pro.\n\nLike all system-wide video effects, such as Portrait, Studio Light, and Gestures, Center Stage is enabled per process. Once active, it applies to any supported camera in your app.\n\nHere's the photo capture setup again. For video calls, the output is different. Video conferencing apps typically use a video data output to receive video buffers as a stream. Display, encoding, and transmission are all handled at the app layer.\n\nWith this setup in mind, here's how to enable Center Stage. First, find a supported format. Set it as the active one after locking the device.\n\nBy default, people toggle Center Stage through Control Center, not your app. Before turning it on, set a control mode: cooperative or app. Cooperative mode lets people also control it from a button in your app.\n\nThen, set isCenterStageEnabled to true. With that, Center Stage is active.\n\nFraming now automatically adjusts to keep all people centered.\n\nTo learn more about the Center Stage API when it was first added for iPad, check out \"What’s New in camera capture\" from WWDC 2021.\n\nBeyond Center Stage, there's one more way to improve your video call experience. The Center Stage front camera supports a real-time, low-latency stabilization mode starting in iOS 26. It's off by default. To turn it on, set the preferredVideoStabilizationMode on the AVCaptureConnection to lowLatency.\n\nHere’s a side-by-side comparison of the same video call with low latency stabilization on and off.\n\nIn the left video, stabilization is off. There’s noticeable shake as I walk. In the right video, stabilization is on. The footage is much more stable.\n\nWith that, you have everything you need to support the Center Stage front camera in your iOS app. Here are some next steps to further improve your app’s framing experience.\n\nFirst, incorporate framing controls into your app to support orientation switching, toggles for Auto Zoom and Auto Rotate, and Center Stage activation.\n\nSecond, optimize front camera performance. Test with sensor orientation compensation turned off.\n\nMake sure your app handles photo rotation correctly. Also consider supporting 18-megapixel photo capture for exceptional resolution and details. For best practices, check out \"Implement high resolution photo capture” from WWDC 2026.\n\nThe Center Stage front camera has completely changed the way I take selfies and make video calls on my iPhone. I look forward to seeing how it transforms your app. Thank you for watching.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "5:29",
+ "title": "Support dynamic aspect ratio",
+ "language": "swift",
+ "code": "// Select the Center Stage front camera\n\nimport AVFoundation\n\nlet deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInUltraWideCamera], mediaType: .video, position: .front)\n\nguard let camera = deviceDiscoverySession.devices.first else {\n print(\"Failed to find the capture device\")\n return\n}\n\n// Find a format that supports the 4x3 aspect ratio\n\nfor format in camera.formats {\n if format.supportedDynamicAspectRatios.contains(.ratio4x3) {\n try! camera.lockForConfiguration()\n camera.activeFormat = format\n camera.unlockForConfiguration()\n break\n }\n}\n\n// Set dynamic aspect ratio\n\ntry! camera.lockForConfiguration()\n\nlet timestamp = try! await camera.setDynamicAspectRatio(.ratio4x3)\nprint(\"Applied dynamic aspect ratio at timestamp: \\(timestamp)\")\n\ncamera.unlockForConfiguration()"
+ },
+ {
+ "timestamp": "7:39",
+ "title": "Support smart framing monitor",
+ "language": "swift",
+ "code": "// Find a format that supports smart framing\n\nimport AVFoundation\n\nfor format in camera.formats {\n if format.isSmartFramingSupported {\n try! camera.lockForConfiguration()\n camera.activeFormat = format\n camera.unlockForConfiguration()\n break\n }\n}\n\n// Configure the smart framing monitor\n\nlet monitor = camera.smartFramingMonitor!\n\ntry! camera.lockForConfiguration()\nmonitor.enabledFramings = monitor.supportedFramings\ncamera.unlockForConfiguration()\n\n// Monitor framing recommendations\n\nobservation = monitor.observe(\\.recommendedFraming, options: [.new,]) { monitor, change in\n if let framing = monitor.recommendedFraming {\n\n Task {\n try! camera.lockForConfiguration()\n try! await camera.setDynamicAspectRatio(framing.aspectRatio)\n camera.videoZoomFactor = CGFloat(framing.zoomFactor)\n camera.unlockForConfiguration()\n }\n\n }\n}\n\n// Start the smart framing monitor\n\ntry! monitor.startMonitoring()\n\n// Stop the smart framing monitor\n\nobservation?.invalidate()\nobservation = nil\n\nmonitor.stopMonitoring()"
+ },
+ {
+ "timestamp": "14:44",
+ "title": "Support Center Stage for video calls",
+ "language": "swift",
+ "code": "// Find a format that supports Center Stage\n\nimport AVFoundation\n\nfor format in camera.formats {\n if format.isCenterStageSupported {\n try! camera.lockForConfiguration()\n camera.activeFormat = format\n camera.unlockForConfiguration()\n break\n }\n}\n\n// Turn on Center Stage\n\nAVCaptureDevice.centerStageControlMode = .cooperative\nAVCaptureDevice.isCenterStageEnabled = true"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Supporting Center Stage front camera in your iOS app",
+ "url": "https://developer.apple.com/documentation/AVFoundation/supporting-center-stage-front-camera-in-your-ios-app"
+ },
+ {
+ "title": "AVCam: Building a camera app",
+ "url": "https://developer.apple.com/documentation/AVFoundation/avcam-building-a-camera-app"
+ },
+ {
+ "title": "AVFoundation",
+ "url": "https://developer.apple.com/documentation/AVFoundation"
+ },
+ {
+ "title": "Capture setup",
+ "url": "https://developer.apple.com/documentation/AVFoundation/capture-setup"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/341/4/fa1380a3-e2ab-4442-9302-817be212e991/downloads/wwdc2026-341_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/341/4/fa1380a3-e2ab-4442-9302-817be212e991/downloads/wwdc2026-341_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "304",
+ "year": "2026",
+ "title": "Implement high resolution photo capture",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/304"
+ },
+ {
+ "id": "10106",
+ "year": "2023",
+ "title": "Support external cameras in your iPadOS app",
+ "url": "https://developer.apple.com/videos/play/wwdc2023/10106"
+ },
+ {
+ "id": "10047",
+ "year": "2021",
+ "title": "What’s new in camera capture",
+ "url": "https://developer.apple.com/videos/play/wwdc2021/10047"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:22.202Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-343.json b/data/wwdc/videos/2026-343.json
new file mode 100644
index 0000000..c76c639
--- /dev/null
+++ b/data/wwdc/videos/2026-343.json
@@ -0,0 +1,141 @@
+{
+ "id": "343",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/343/",
+ "title": "Explore advanced App Intents features for Siri and Apple Intelligence",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Antonio Cancio, a software engineer on the App Intents team. Let's \"Explore advanced App Intents features for Siri and Apple Intelligence.\" Today, you'll learn techniques to take the experience of your app in Siri and Apple Intelligence beyond the basics, so it feels polished, personal, and unmistakably yours. This talk assumes a basic understanding of App Intents and App Schemas. If they're new to you, I recommend starting with these sessions which cover the fundamentals.\n\nLet's go through today's plan. First, we'll explore how you can make people's conversations with your app through Siri more intuitive and familiar. You'll learn how to build custom responses that match your app's look and feel, and how to set up interaction donations that help Apple Intelligence feel more personal.\n\nThen, we'll discuss how you can make your content more widely available. From semantic index integration to structured search, in-app search, and onscreen awareness, you'll come away knowing how to help Siri find your content and connect what's visible on screen to actions it can understand.\n\nFinally, people can use Siri from anywhere in the system.\n\nWe'll cover how to add entity annotations to existing integrations with notifications, Now Playing, and alarms, so people can act on your content wherever they encounter it. To show this in practice, I wanna share a few apps that my colleagues and I have been building: CosmoTunes, which lets me play music, and create alarms and timers with my favorite songs; UnicornChat, a messaging app to stay in touch with friends; and CometCal to manage my calendar. Hm. Hey, there's something about App Intents that encourages celestial symbolism. You can download these samples from the links below and follow along.\n\nLet's start with shaping the Siri conversation using custom responses.\n\nSiri does the heavy lifting. It understands natural language, picks the right action, and crafts a helpful response. The App Intents framework gives you the tools to shape what Siri does, and refine how it responds. By tailoring how it responds, you can let your app's unique personality shine through. Let me illustrate this with some code. In CosmoTunes, .addToPlaylistIntent lets people add songs to a playlist. To start, I want Siri to handle the response. In the perform method of the intent, I'll add the song to the playlist, and return an empty IntentResult.\n\nThis tells Siri to take care of the response when the intent runs.\n\nAfter trying that out, I want the response to better match the app's personality. I call songs tracks and playlists mix tapes. To customize this, I mark the perform method as providing a dialog response by adding the ProvidesDialog protocol. I'll also change the IntentResult by passing an IntentDialog containing both full and supporting strings. Siri can show the supporting string with UI, and read the full dialog on voice-only devices like AirPods. Because of this, the full string should describe what happened on its own.\n\nThat covers responses when your intent has finished. But what if you want to ask people a question while your intent is running? A well-placed clarifying question lets people finish the action they meant to take.\n\nTo ask a question before your intent result, use a dialog request within your perform method.\n\nPeople can create timers in CosmoTunes that start or stop audio playback after a set amount of time. This intent adopts a schema with required and optional parameters. If there's a timer already running, I want to ask the person to name this new timer to avoid confusion.\n\nI'll request a value for the optional label parameter when one hasn't been provided. If you want to ask people to choose from a list of items, or ask for a confirmation, check out the sample app and documentation to learn about other kinds of dialog requests. Next, I want Siri's visuals to match the app's look and feel. Entity display representation and custom views in intent responses are a great opportunity to visually present information and surface your app's identity alongside the dialog.\n\nDefining an entity's DisplayRepresentation can tell Siri how your entity should look and read when showing your content. Entity display representation can be used in responses, like when an entity has been created or updated. They are also used when asking someone to choose between similar entities, or when answering questions about content in your app. Spotlight and Shortcuts can use them, too.\n\nTo define a basic DisplayRepresentation, provide a title.\n\nYou can make these even richer by providing a subtitle and an image.\n\nI'll provide an image associated with my songs that Siri can show when people ask about their songs in the app.\n\nEntity DisplayRepresentation lets you refine the visual identity of your entities across the system. To define the visual responses of some specific actions, you can use custom view snippets.\n\nBack in AddToPlaylistIntent, Siri already responds automatically using the entity display representation.\n\nTo use a custom view, I add the ShowsSnippetView return type to my perform method.\n\nThis lets me return an IntentResult with a SwiftUI view, like my PlaylistSnippetView which displays the playlist details in familiar colors. When approaching customization, test your intents and decide where customization actually makes sense for your app. Make sure your responses are accurate and sound natural across all platforms, including voice-only devices like AirPods. Remember to ask clarifying questions sparingly to avoid friction.\n\nFinally, use custom visuals to bring your app's identity to Siri, keeping in mind how they'll scale across the ecosystem.\n\nYour intent responses typically come at the end of a Siri interaction. But, Siri can ask additional questions before even calling your intent. For example, if I ask to message someone, Siri may ask me to choose from a list of similarly named contacts, because the system isn't sure which one I mean.\n\nThat brings us to Interaction Donations, an API you can adopt to help Apple Intelligence be even smarter in handling those kinds of requests.\n\nHere's the good news. When people interact with your app through Siri or Shortcuts, the system already knows about it. But, Apple Intelligence can't learn from actions people take through your app's UI without your help. That's where donations come in.\n\nEach donation is a hint that a person took a specific action in your app's UI. The system stores these as schema-conforming App Intents in a temporary transcript, giving Siri the context it needs to make smarter decisions. The transcript contains UI interaction donations over time. Say someone opens UnicornChat and sends a message from the compose view. Right then, the app donates the send message action to the system, using the SendMessageIntent schema.\n\nAfter messaging them frequently in the app, eventually, when someone says: \"Send a message to a contact from the Home Screen,\" Siri might infer the right app to use for that contact.\n\nTo adopt interaction donations for message sending, I started by looking at the ConversationView in UnicornChat.\n\nThe ConversationView and the sendMessage intent both call the same helper to send the message.\n\nThat looks like a good place to start donating the sendMessage intent.\n\nI'll add a donateIntent parameter so I know whether the helper was called from the intent or from the UI. Apple Intelligence already learns from Siri interactions, so I only need to donate UI interactions.\n\nThen, I'll create the intent, populate its parameters and the intent result, and donate it via the IntentDonationManager API. Now when a person opens the app and sends a message, the system can learn when they prefer to use UnicornChat.\n\nBeyond learning preferences, Interaction Donations also keep Siri aware of ongoing activities in your app. This is especially useful for activities people might start or stop with Siri.\n\nIn an app in the Maps domain, a person could start a NavigationSession with the app's UI which donates that interaction. Then, the person gets in the car, and asks Siri to add a stop on their way. Thanks to the Interaction Donation, Siri can know what NavigationSession is active in the app, and help the person with their request.\n\nThis pattern applies to intents that start or stop NavigationSessions in the Maps domain, and stop, start, pause, or lap stopwatches in the Clock domain.\n\nYour interaction donations should accurately represent real user behavior in your app. If your app donates excessively, the system may ignore those donations. Once Siri has gathered all the parameter values and is ready to call your App Intent, there is one final step: confirmation.\n\nAsking people to confirm that the action looks right keeps them informed and protects them from unintended side effects, which are a known risk with Large Language Models.\n\nThis matters most for intents that could have meaningful side effects on your data, or the outside world.\n\nThat's why Siri can automatically confirm these kinds of intents. For example, it can confirm when I say: \"Cancel my expedition next week in CometCal.\" This matters even more for intents that update app content which a person has made public or shared with others.\n\nFor example, Siri may not confirm when I update a personal event, but it may confirm when I ask it to update Crew Lunch since I'm updating an event with attendees. By default, Siri assumes your entities are private to the person, and may skip confirmations for them.\n\nTo tell Siri the owner has made an entity public or shared it with others, conform the relevant entities to the new OwnershipProvidingEntity protocol.\n\nOnly add the protocol to entities that people can share or make public in your app. Then, provide the ownership state.\n\nKeep the ownership state up to date whenever the system requests an entity from your app.\n\nThis ensures Siri has the necessary information when deciding to confirm.\n\nRemember those entity display representations we customized earlier? Siri can use them as visuals in these intent confirmations, too.\n\nGiving people a chance to confirm actions when appropriate builds trust in your app's experience with Siri.\n\nTo learn more about other ways to establish trust and mitigate risks, check out \"Secure your app: Mitigate risks to agentic features.\" So far, I've defined how the actions in these apps work with Siri, given Apple Intelligence context about how people use my apps, and helped Siri protect people from unintended side effects. Next, let's talk about how Siri finds content in the first place. There are three paths I want to cover: the semantic index, structured search, and in-app search.\n\nPlaylists in CosmoTunes are all available locally on device. To help Apple Intelligence find them, I'll adopt IndexedEntity and index those entities in Spotlight.\n\nI'll use the .indexAppEntities method on CSSearchableIndex, which populates the Spotlight semantic index.\n\nNow I can ask Siri: \"Play my WWDC playlist in CosmoTunes.\" I can also search for my playlists in the Spotlight search UI. And depending on the App Intents domain, indexing entities in Spotlight provides semantic search capabilities. This means Apple Intelligence and Siri can understand your entities based on meaning, not just exact keywords.\n\nAdding to the index is the first step. Keeping it up to date is key to helping Siri find your content.\n\nIndex new entities as people add content to your app. Update existing entries when key properties change, especially those used in your display representation.\n\nWhen people remove content, delete those index entries too. Spotlight may need your app to reindex its entities. Your app can support reindexing by adopting the new IndexedEntityQuery.\n\nCheck out IndexedEntityQuery in the sample project.\n\nIf your project already supports reindexing with Core Spotlight-level APIs, you do not need to define an IndexedEntityQuery.\n\nHowever, you might not index your entities if your content dataset is large, lives on a server, or changes too frequently to index ahead of time. For example, I decided to index all the app's playlists, but not songs. To still give people the flexibility in playing songs with Siri, I reached for an IntentValueQuery.\n\nIntentValueQuery is suitable if you don't index all your entities ahead of time. This is very similar to EntityQuery. The key differences are that your app receives a structured search input from the system, and you can return more than one entity type. Siri needs an entity for the audioEntity parameter on the PlayAudioIntent in CosmoTunes.\n\nTo find the entity, Siri calls the IntentValueQuery with an AudioSearch. The query maps the structured properties of that search input to audio entities in the app.\n\nIn the IntentValueQuery, I implemented the values for method to handle the AudioSearch input, and return an AudioEntity.\n\nAudioEntity is a UnionValue type that includes both songs and playlists. The AudioSearch value has a .criteria property that describes the person's query. The .searchQuery case contains the relevant part of what the person said, and I use that to find matching entities. The app also supports an unspecified search. For example: \"Play CosmoTunes\" which isn't specific about what I want to play. In that case, the app jumps straight into playing songs I've previously liked.\n\nThere's also a URL case for when someone references a link from your app. Like: \"Play that playlist Glow sent me.\" Check out the documentation for the full set of AudioSearch criteria.\n\nSometimes people aren't asking Siri to take action, they just want to find something. When I ask: \"Show me running playlists in CosmoTunes.\" Siri can display a list of entity search results. That's a nice default. But I spent a lot of time crafting the app's own search experience, and I'd love to show these results there.\n\nTo do this, I'll adopt the system .searchInApp schema.\n\nThe .system search schema introduced in iOS 17 is now named .system.searchInApp. It is part of the System App Schema domain, and it lets people search in your app with Siri, no matter which other domains you adopt, and even if you don't index your entities.\n\nSiri calls this intent with the same string it searched for, and the intent's perform method finds and shows those results in the app.\n\nSpotlight and structured search let Siri reason about your content. That's great if people ask Siri to play content in the app by name but just like in everyday conversations, people often refer to what they see.\n\nThat's why I want to allow people to interact with the audio content they're looking at on their screen.\n\nOnscreen awareness is how your app connects what's visible on screen to structured information and actions the system understands.\n\nSiri can then resolve references like: \"Play the third one\" or \"that conversation\" without the person naming them explicitly. When people start a Siri request, Siri has an understanding of text on screen, but it's limited to exactly what's in the pixels. For example, Siri can't act on the tracks shown, and it may not be able to tell you about the artist because the artist isn't currently shown on screen.\n\nAdopting onscreen awareness APIs provides Siri with additional context of what entities are on screen, and where they are on screen. This onscreen context means Siri can answer detailed questions about those entities, and take action on them.\n\nWhen adopting onscreen awareness, the NSUserActivity and View Annotation APIs are where you should start.\n\nWith NSUserActivity, attach .userActivity to the view representing your primary onscreen content. Use the View Entity annotation when the entity is one item among many on screen.\n\nAttach .appEntityIdentifier to each view that represents an entity. In CosmoTunes, AlbumView uses a View Entity annotation because both the album and the containing tracks are visible. NowPlayingView uses NSUserActivity because the screen is dedicated to the currently playing item.\n\nNSUserActivity and View Entity annotations are enough when a screen has a handful of entities. But there are two more onscreen awareness APIs. The first is for lists and collections, where you display many entities at once.\n\nTracks in CosmoTunes are displayed in lists in a few views of the app.\n\nCollection annotations help me avoid the overhead of attaching an annotation to every single row. Instead, the system fetches identifiers lazily, as it needs them.\n\nCollection annotations also let Siri discover entities that have been selected and scrolled off screen. Per row annotations disappear as soon as the view leaves the view hierarchy.\n\nIn SwiftUI, use the .appEntityIdentifier (forSelectionType:) modifier on a List, returning the EntityIdentifier for each item's selection ID.\n\nThe second API is the custom canvas view annotation. I built this custom canvas view that looks like a piano roll. It illustrates the notes in the current track and brings the unique retro look CosmoTunes is known for.\n\nI want people to be able to act on the associated song using Siri whenever this canvas is visible.\n\nTo help the system understand this non-standard subview, I used the custom canvas view annotation. If you're using SwiftUI, check out how I adopted this in the PianoRollView in the CosmoTunes sample code.\n\nUIKit and AppKit also support all of the onscreen awareness APIs.\n\nCheck out the documentation for: AppEntityAnnotatable, UICollectionViewAppIntentsDataSource, and appEntityUIElementProvider. And to learn more about how these entity annotations help power contextual menu items in UIKit apps, check out, \"Modernize your UIKit app.\" After adopting onscreen awareness, some of the app's views show many entities at once.\n\nSiri needs to quickly understand if the on-screen entities relate to a request. For example, someone asks Siri to play the third one. If Siri can't understand my on-screen entities quickly enough, it may ask to clarify or play something else entirely.\n\nPeople can abandon the request when that happens. The entity display representations you customized earlier can help.\n\nIn CosmoTunes, I enabled display representation querying on the playlist entity query by implementing the displayRepresentations method.\n\nNow, when Siri is trying to understand the content on screen, it can query just the text representation of the entity and skip the overhead of fetching the full content from the database.\n\nOnscreen awareness provides Siri with additional context when people are looking at your app. Beyond the UI, your app already integrates closely with other parts of the system. To give Siri even more context, you can connect entities to integrations that you already adopt, like user notifications.\n\nWith this added context, your app entities act as a universal language. They let Siri understand not just what's on screen, but how other system integrations and time-sensitive events relate to your content.\n\nI'll add entities to three integrations the apps already use: UserNotifications, NowPlaying, and AlarmKit.\n\nWhen I'm done, I'll be able to say: \"Play the live version\" letting me easily switch to a different version of the currently playing song.\n\nWhen a UnicornChat notification is announced on AirPods I can say: \"Reply, 'ok, I'll swing by the unicorn supply store and pick those up.'\" And Snooze it to snooze an alarm from CosmoTunes.\n\nAll three use the same pattern, and we call these entity annotations.\n\nAnnotating notifications with entities gives Siri concrete entity context when announcing a notification on AirPods. When listening to the announced notification, the person might want to act on the entity behind it, like replying to a message or checking off a reminder.\n\nTo give the additional context to Siri about what entity is associated with the UnicornChat notification, I'll update the posting flow. After importing AppIntents I assign the persistent message EntityIdentifier to the .appEntityIdentifiers property on UNMutableNotificationContent.\n\nNote, that with the three entity annotation APIs I'm describing, you can't use TransientAppEntity. Transient entities are temporary model objects, so they don't have persistent identifiers. To add entity annotations to NowPlaying in CosmoTunes, I followed the same pattern. I'm already providing song attributes using MusicContent in the app's MediaSessionRepresentable conformance.\n\nTo enhance this state, I'll take the existing song, artist, and playlist entities, and add them to the appEntityIdentifiers property in order of most specific to least specific.\n\nThis enables contextual requests like: \"Play the live version.\" With AlarmKit, I add a single EntityIdentifier to the appEntityIdentifier parameter on AlarmConfiguration when creating an alarm or a timer. With this, people can act on firing alarms and timers.\n\nThat's all it takes to connect your entities to notifications, Now Playing, and alarms.\n\nWe've covered several advanced ways to make your app work even better with Siri. As you think about next steps, a great place to start is by customizing your entity display representations. They are used to display your entities across the system.\n\nFrom there, add your entities to the semantic index, and keep the index up to date, so Siri can always find your freshest content.\n\nYou might also consider making your entities accessible through Siri with an IntentValueQuery and in-app search. And annotating your views, activities, and your existing system integrations with entities can give Apple Intelligence even more context to work with.\n\nWhen you're ready, look into donating UI interactions to help Apple Intelligence understand how people use your app, making for a more personalized experience.\n\nTo see any of these concepts applied, take a look at the sample projects.\n\nFor a hands-on look at adopting App Schemas, check out the \"Code-Along: Make your app available to Siri.\" With the 27 releases, Apple Intelligence is transforming what Siri can do, and App Intents puts that transformative power directly in your hands. You now have everything you need to build delightful, elevated experiences that feel like a natural extension of the system. I can't wait to see what you create. Until next time.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "2:42",
+ "title": "Custom dialog response",
+ "language": "swift",
+ "code": "@AppIntent(schema: .audio.addToPlaylist)\n struct AddToPlaylistIntent {\n\n func perform() async throws -> some IntentResult & ProvidesDialog {\n // Adds song to playlist and responds\n return .result(\n dialog: IntentDialog(\n full: \"\"\"\n Added \\(song.title) to the \\\n \\(playlist.title) mix tape.\n \"\"\",\n supporting: \"Added\"\n )\n )\n }\n }"
+ },
+ {
+ "timestamp": "3:42",
+ "title": "Ask a clarifying question within an inten",
+ "language": "swift",
+ "code": "@AppIntent(schema: .clock.createTimer)\n struct CreateTimerIntent {\n // MARK: Schema Parameters\n var duration: Duration\n var label: String?\n var isSleepTimer: Bool\n\n func perform() async throws -> some ReturnsValue {\n // Checks active timers and requests label parameter\n label = try await $label.requestValue(\n \"\"\"\n You already have a timer running. \\\n What should we call this one?\n \"\"\"\n )\n return .result(value: timerEntity)\n }\n }"
+ },
+ {
+ "timestamp": "4:26",
+ "title": "Enhanced DisplayRepresentation",
+ "language": "swift",
+ "code": "// Enhanced DisplayRepresentation\n @AppEntity(schema: .audio.song)\n struct SongEntity {\n\n var displayRepresentation: DisplayRepresentation {\n DisplayRepresentation(\n title: \"\\(title)\",\n subtitle: \"\\(artistName)\",\n image: artworkImage\n )\n }\n }"
+ },
+ {
+ "timestamp": "5:05",
+ "title": "Return a custom snippet view",
+ "language": "swift",
+ "code": "@AppIntent(schema: .audio.addToPlaylist)\n struct AddToPlaylistIntent {\n\n var audioEntity: AudioEntity\n var playlist: PlaylistEntity\n\n func perform() async throws -> some IntentResult & ProvidesDialog & ShowsSnippetView {\n // Adds to playlist and shows dialog and snippet\n let view = PlaylistSnippetView(\n playlist: updatedEntity,\n tracks: updated.tracks\n )\n return .result(dialog: dialog, view: view)\n }\n }"
+ },
+ {
+ "timestamp": "7:44",
+ "title": "Donate a UI interaction",
+ "language": "swift",
+ "code": "@ModelActor\n actor ModelManager {\n func sendMessage(_ /* ... */, donateIntent: Bool = false) async throws -> [Message.ID] {\n\n // Donate intent with parameters and result so Siri can learn user preferences\n if donateIntent {\n let intent = SendMessageIntent()\n intent.destination = .recipients(conversation.recipients.map(\\.entity))\n\n let result = messages.map(\\.entity)\n Task {\n try await IntentDonationManager.shared.donate(\n intent: intent,\n result: .result(value: result)\n )\n }\n }\n }\n }"
+ },
+ {
+ "timestamp": "10:03",
+ "title": "Declare entity ownership for confirmations",
+ "language": "swift",
+ "code": "// Informs system if entity is public or shared with others\n @AppEntity(schema: .calendar.event)\n struct EventEntity: OwnershipProvidingEntity {\n\n var ownership: EntityOwnership {\n // isShared used to compute ownership state: .shared, .public, or .unknown\n attendees.isEmpty ? .unknown : .shared\n }\n }"
+ },
+ {
+ "timestamp": "11:30",
+ "title": "Index entities with IndexedEntity",
+ "language": "swift",
+ "code": "// Indexing IndexedEntity with CSSearchableIndex\n struct EntityIndexingHelper {\n // Indexes playlist entities\n func indexPlaylist(_ playlist: Playlist) async throws {\n let entity = PlaylistEntity(playlist: playlist)\n try await CSSearchableIndex(name: indexName)\n .indexAppEntities([entity])\n }\n }"
+ },
+ {
+ "timestamp": "13:38",
+ "title": "Structured search with IntentValueQuer",
+ "language": "swift",
+ "code": "// Structured search of songs and playlists\n struct AudioIntentValueQuery: IntentValueQuery {\n\n // AudioSearch, IntentPerson, and other system types may be supported as input\n func values(for input: AudioSearch) async throws -> [AudioEntity] {\n switch input.criteria {\n case .searchQuery(let query):\n return try await searchResults(for: query)\n case .unspecified:\n return try await likedSongResults()\n // ... also a .url case\n }\n }\n }"
+ },
+ {
+ "timestamp": "14:49",
+ "title": "Re-run Siri search in your app",
+ "language": "swift",
+ "code": "// Intent that re-runs the Siri search in app\n @AppIntent(schema: .system.searchInApp)\n struct SearchAudioLibraryIntent {\n\n var criteria: StringSearchCriteria\n\n func perform() async throws -> some IntentResult {\n // Perform in-app search with Siri search string\n navigation.searchText = criteria.term\n navigation.selectedTab = .library\n return .result()\n }\n }"
+ },
+ {
+ "timestamp": "16:27",
+ "title": "Onscreen awareness annotations",
+ "language": "swift",
+ "code": "// (a) Single primary entity on screen — NSUserActivity\n struct NowPlayingView: View {\n @Environment(PlaybackController.self) private var playback\n\n var body: some View {\n VStack {\n // Player UI\n }\n .userActivity(\"cosmotunes.nowPlaying\", isActive: playback.currentTrack) { activity in\n activity.title = playback.currentTrack?.title\n activity.appEntityIdentifier = EntityIdentifier(\n for: SongEntity.self,\n identifier: playback.currentTrack.id\n )\n }\n }\n }\n\n // (b) One entity among many — View Entity annotation\n struct AlbumView: View {\n private var header: some View {\n VStack(alignment: .leading, spacing: 6) {\n // ...\n }\n .appEntityIdentifier(\n EntityIdentifier(for: AlbumEntity.self, identifier: session.id.uuidString)\n )\n }\n }\n \n // (c) Lists and collections — Collection annotation\n struct PlaylistDetailView: View {\n var body: some View {\n List {\n ForEach(playlist.tracks) { track in\n PlaylistTrackRow(track: track)\n }\n }\n .appEntityIdentifier(forSelectionType: GeneratedTrack.ID.self) { trackID in\n EntityIdentifier(for: SongEntity.self, identifier: trackID)\n }\n }\n }"
+ },
+ {
+ "timestamp": "17:23",
+ "title": "Component-based display representation query",
+ "language": "swift",
+ "code": "// Component-based display representation queries\n extension PlaylistQuery {\n func displayRepresentations(\n for identifiers: [PlaylistEntity.ID],\n requestedComponents: DisplayRepresentation.Components = .text\n ) async throws -> [PlaylistEntity.ID: DisplayRepresentation] {\n let entities = try await model.playlistEntities(for: identifiers)\n\n // Fetch display representations for fetched entities\n var result: [PlaylistEntity.ID: DisplayRepresentation] = [:]\n for entity in entities {\n result[entity.id] = await entity.displayRepresentation(with: requestedComponents)\n }\n return result\n }\n }"
+ },
+ {
+ "timestamp": "21:07",
+ "title": "Entity annotations on system integrations",
+ "language": "swift",
+ "code": "// (a) User notifications\n import AppIntents\n import UserNotifications\n\n func scheduleNotification(message: Message, author: Contact, conversation: Conversation) {\n let content = UNMutableNotificationContent()\n content.title = author.name\n content.body = message.body\n\n // Annotate with entity identifier\n content.appEntityIdentifiers = [\n EntityIdentifier(for: MessageEntity.self, identifier: message.id)\n ]\n // Schedule the notification\n }\n\n // (b) Now Playing — most specific to least specific\n import NowPlaying\n\n final class CosmoTunesMediaSession: MediaSessionRepresentable {\n var content: (any MediaContentRepresentable)? {\n var content = MusicContent(id: track.id.uuidString, songTitle: track.title /* ... */)\n content.appEntityIdentifiers = [\n EntityIdentifier(for: SongEntity.self, identifier: track.id),\n EntityIdentifier(for: ArtistEntity.self, identifier: track.session.artistName),\n EntityIdentifier(for: PlaylistEntity.self, identifier: currentPlaylist.id),\n ]\n return content\n }\n }\n\n // (c) AlarmKit\n import AlarmKit\n\n func scheduleAlarm(_ alarm: Alarm) async throws {\n let configuration = AlarmManager.AlarmConfiguration.alarm(\n schedule: schedule,\n attributes: attributes,\n appEntityIdentifier: EntityIdentifier(for: AlarmEntity.self, identifier: alarm.id),\n stopIntent: DismissAlarmIntent(),\n secondaryIntent: SnoozeAlarmIntent(),\n sound: sound\n )\n // Schedule alarm\n }"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "App Intents Testing",
+ "url": "https://developer.apple.com/documentation/AppIntentsTesting"
+ },
+ {
+ "title": "Donating your app’s data and actions to the system",
+ "url": "https://developer.apple.com/documentation/AppIntents/donating-your-apps-data-and-actions-to-the-system"
+ },
+ {
+ "title": "Donations and discovery",
+ "url": "https://developer.apple.com/documentation/AppIntents/donations-and-discovery"
+ },
+ {
+ "title": "Making app entities available in Spotlight",
+ "url": "https://developer.apple.com/documentation/AppIntents/making-app-entities-available-in-spotlight"
+ },
+ {
+ "title": "Making actions and content discoverable by Apple Intelligence",
+ "url": "https://developer.apple.com/documentation/AppIntents/making-actions-and-content-discoverable-by-apple-intelligence"
+ },
+ {
+ "title": "Providing contextual cues to Apple Intelligence and Siri",
+ "url": "https://developer.apple.com/documentation/AppIntents/providing-contextual-cues-to-apple-intelligence-and-siri"
+ },
+ {
+ "title": "Apple Intelligence and Siri AI",
+ "url": "https://developer.apple.com/documentation/AppIntents/apple-intelligence-and-siri-ai"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/343/4/00190d1d-55b6-4eb2-9ee3-e09f3d8d1c7d/downloads/wwdc2026-343_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/343/4/00190d1d-55b6-4eb2-9ee3-e09f3d8d1c7d/downloads/wwdc2026-343_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "240",
+ "year": "2026",
+ "title": "Build intelligent Siri experiences with App Schemas",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/240"
+ },
+ {
+ "id": "344",
+ "year": "2026",
+ "title": "Code-along: Make your app available to Siri",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/344"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:22.328Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-344.json b/data/wwdc/videos/2026-344.json
new file mode 100644
index 0000000..d4b66e0
--- /dev/null
+++ b/data/wwdc/videos/2026-344.json
@@ -0,0 +1,74 @@
+{
+ "id": "344",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/344/",
+ "title": "Code-along: Make your app available to Siri",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hi, I'm Justin, an engineer on the Swift Intelligence Frameworks team. Welcome to this code-along. In this video, I'll take an app and make it available to Siri, step by step. Imagine I start a conversation with Siri on my iPhone and ask: \"Who's coming to my picnic?\" Siri searches for the event, its attendees, and shows me the guest list.\n\nI realize it includes that one friend that's always late, so I say: \"Move it to noon… actually 11:30 am\".\n\nSiri asks to confirm the details, and I say: \"sounds good, thanks\".\n\nSiri makes the changes and renders a custom view.\n\nNow I need to update my friends: \"Text everyone going to the picnic and let them know about the change using emojis\".\n\nAfter double-checking the message, I say: \"Send it\".\n\nAnd just like that, Siri sends the message. Then I remember the event's note had a reminder to bring something important to the picnic. So I ask: \"What did I need to bring to the picnic again?\" Siri searches the event and finds a note to myself to bring a chocolate cake.\n\nI'm curious how much time I have to get ready, so I say: \"How long will it take to drive there?\" Siri finds the event's location, and gives me an estimate.\n\nThis is how Siri can make everyday things effortless… simply with a conversation. That's the mission for today.\n\nI'll take an existing app and make it available to Siri. Siri is powered by Apple Intelligence, Apple's personal intelligence system. Developers integrate their apps with Apple Intelligence through the App Intents framework, so their app's content and actions become available within Siri and other system experiences. The companion video \"Build intelligent Siri experiences with App Schemas\" covers the concepts behind the framework and how Apple Intelligence and Siri work with your app's content and actions. If App Intents is new to you, the video \"Get to know App Intents\" covers the core fundamentals like intents, entities, and queries, and how they connect to system features like Siri and Shortcuts.\n\nI've been working on a sample project called CometCal. It's a SwiftUI calendar app with a fun cosmic twist, and the source code is available for download on the Apple Developer website for you to follow along.\n\nIt does all the basics of a calendar app: showing today's events, viewing and editing them, creating more, and even managing different calendars.\n\nRight now, the only way to interact with it though is through the screen… but that's about to change! In this video, I'm going to update CometCal so that Siri can: understand the app's content and answer my questions. And take actions like updating an event… making Siri more helpful than ever. The first step is giving Siri an understanding of my app's content. Right now, Siri has no idea what a calendar or an event means inside CometCal. That's where App Schemas come in. App Schemas describe my app's content and actions in terms Siri can already understand. They define the structure of my entities, the parameters of my actions, and the outputs. No training phrases, no natural language processing on my end. App Schemas are organized into App Schema Domains.\n\nThe calendar domain covers everything related to scheduling: events, calendars, attendees, and the actions that operate on them. Now that the groundwork is set… time to suit up, jump into Xcode, and start the countdown! CometCal already has a CalendarModel in its data layer, that's the SwiftData model for calendars, like the difference between a Personal calendar and a Work calendar. The goal is to create an app entity that represents it using App Schemas, so Siri can understand what a calendar is in this app.\n\nI'll create a new Swift file called CalendarEntity, and import AppIntents.\n\nNext, I'll type calendar_ in the editor. Xcode offers every schema in the Calendar domain, right in autocomplete. Since the goal is a calendar entity, I'll select calendar_calendar.\n\nThe snippet fills in the structure: the @AppEntity macro, properties, DisplayRepresentation and query stubs. This is now a schematized entity, a type Siri can reason over. A few things to fill in. First, I'll set the id type to UUID to match the data model.\n\nTo enable matching by meaning and not just text, I'll conform to the IndexedEntity protocol. Conforming to IndexedEntity allows my app to donate entities using the Spotlight index to get the benefits of semantic understanding. When an entity is donated, Siri can resolve it by name, by property, or by context, without requiring a custom property query.\n\nTo keep things moving, I added ways to convert between the data model and the entity.\n\nThe CalendarEntity's initializer maps from CalendarModel to pull out what the entity needs. And the convenience .entity property on CalendarModel produces a CalendarEntity using the initializer. I'll use this soon in places like the query to convert between the two.\n\nNext, I'll add a @Dependency property for the CalendarManager in the query. That's CometCal's data layer that handles all the SwiftData operations. The @Dependency property wrapper is how App Intents injects shared resources into intents and queries, so instead of creating new instances, it provides the same object I register once as I've done on the right.\n\nThe CalendarManager is main-actor isolated, so I'll also annotate the query struct as @MainActor.\n\nI'll implement the method required by the EntityQuery protocol. This fetches calendars by ID using the CalendarManager dependency. Now, EntityQuery covers cases where the system already knows an entity's ID. But later, when creating events, the system will need to know which calendars are available so Siri can offer them as options. For that, I'll conform to EnumerableEntityQuery and add an allEntities method that returns all calendars.\n\nFor the DisplayRepresentation, I'll set the title to the calendar title and the image to a system image of a calendar. Siri and Spotlight use this when displaying the entities. That's one entity ready for orbit… almost. There's one more piece that's easy to miss. IndexedEntity defines the shape of my indexed content, but entities still need to be donated. To do that, I'll open the CalendarManager file.\n\nAnytime calendars, or any indexed entity for that matter, are changed, the index needs to be updated. For that, I have a CSSearchableIndex instance initialized in the CalendarManager's initializer that uses a unique name for CometCal. In the createCalendar method, just before returning the new calendar, I'll donate the entity by calling indexAppEntities using the searchable index instance from before.\n\nSimilarly, in the updateCalendar method, I'll index the updated calendar entity.\n\nAnd in the deleteCalendar method, I'll remove the entity from the index by calling deleteAppEntities, passing in the entity's id and type.\n\nI'll fire up the engines and give it a go… I'll open CometCal and create a new calendar called \"Lunar Orbit Log\" Then, I'll swipe down to search for \"Lunar Orbit Log\"… and there it is with the calendar icon and the title. With CalendarEntity working, the next two entities follow the same pattern, but each introduces something new and shiny. Here's a new file called AttendeeEntity with AppIntents already imported. Same pattern as before… type calendar_attendee and select the snippet.\n\nThe familiar parts are the same… All of that is wired up off-camera so I can keep moving at light speed. Feel free to pause here and check the completed source code. Unlike CalendarEntity, AttendeeEntity conforms to the TransientAppEntity protocol instead of IndexedEntity. That's intentional.\n\nA transient app entity is one that represents a temporary entity that doesn't require a unique identifier and isn't meant to be queried. That's the right fit here. In CometCal, an attendee represents a person's participation in a specific event, not the person themselves. The same person can attend multiple events, and indexing each attendance separately would create duplicative results in Spotlight. Since attendees are always accessed through the event that holds them, there's no need for an independent look up path. TransientAppEntity makes that explicit… no query to write, no index to maintain. Attendees have properties that are required by the schema like the boolean property to tell whether this attendance is optional.\n\nOne new item is the IntentPerson type… the system's standard way to represent a person with a name and contact information. This is useful when sharing this data between apps, like sending an attendee's email address to draft a message in the Mail app. The schema also includes two @AppEnum types. The schema defines the set of possible cases, and my app adopts the ones that apply. These are also schematized, so I'll create them quickly using code snippets. I'll use the calendar_attendeeStatus snippet for status.\n\nThe snippet comes with all the cases the schema supports. CometCal's model already maps directly, so no changes are needed, but if an app uses different terminology, simply map the existing model to the schema's cases so Siri can recognize the shape. Similarly, I'll use calendar_attendeeType for the attendee type.\n\nThe schema requires at least one case to describe what kind of attendee this is and since CometCal's attendees are all people, I'll add a person case.\n\nI'll fill in the types for status and type respectively.\n\nAnd that takes care of the AttendeeEntity.\n\nThat's two entities in orbit. One more to launch. The calendar and attendee entities find their orbit with the next one… the gravitational center that pulls them all together... the EventEntity. The system's search index really shines for the event entity. When someone asks \"When is my crew lunch?\", Siri can search the title. When they ask \"What events mention oxygen?\", it can search the note content. People can ask questions about their data, and Siri answers directly.\n\nHere's EventEntity with the calendar_event snippet already applied. Just like CalendarEntity, the EventEntity conforms to the IndexedEntity protocol and includes the indexing in CalendarManager to take advantage of the semantic index. There's a lot here. The schema covers a wide range of properties. But don't panic, the same patterns from the previous entities apply. The main difference is the number and variety of parameters.\n\nTo keep this mission on schedule, the familiar pieces are wired up off-camera. Feel free to pause and check the source code. The schema defines which properties are required and which are optional. The essentials like title or startDate are straightforward to wire up. Optional properties that my app doesn't use, like travelTime or virtualLocation, can simply stay unset. Properties that aren't part of the schema but exist on the data model, like isFavorite, can also be added to the entity. What makes this entity interesting is how it composes with the other entities built earlier. The calendar this event belongs to is a CalendarEntity… and the attendees is an array of AttendeeEntity.\n\nSiri understands these relationships with App Schemas.\n\nThe recurrence property is also one worth quickly pointing out.\n\nIt can be used to represent events that repeat, like a weekly workout or an important yearly anniversary. It uses Foundation's Calendar.RecurrenceRule type and converts to and from CometCal's simple frequency enum for cases like daily, weekly, monthly or yearly.\n\nAs part of the schema, there are also union values for an event's location and alarms.\n\nA union value is a property that can hold one of several different types. For example, the location can be either a PlaceDescriptor from the GeoToolbox framework, or a String. Again, we can simply implement these via code snippets.\n\nLike so. For alarms, it can be either a Duration or a Date.\n\nAssign the properties to these types accordingly.\n\nLike the attendee, there are also schematized enums here like the EventEntityStatus. Both enums related to events come complete from the snippets, so I'll add those and wire them up.\n\nLastly, assign the status property to the new status type. And with that, the content layer is fully fueled and ready for launch.\n\nTime to find out if this thing has liftoff! On the left is the detail view of the Meteor Shower Watch Party. The time, location, and note are all there on screen… But imagine I'm mid-conversation with Siri talking about meteors, and I suddenly remember about the party. Instead of leaving the conversation to open CometCal and find the details, I can just ask… \"Is the Meteor Shower Party happening anytime soon?\" \"What's the weather like out there?\" I can also switch to typing: \"When is the peak viewing time?\" I can also tap on the result to take me to CometCal.\n\nSiri answers every question using the app's content. No need for custom natural language... just entities and schemas. Now, you may have noticed that tapping the event from my conversation with Siri opens CometCal, but it just lands on the main screen.\n\nIt doesn't navigate to the event like I would expect. Siri doesn't know how to open a specific event in the app yet. I'm going to take a short detour to make this experience even better. To get this working, here's an OpenEventIntent. It's a small intent that conforms to the system.open schema. It takes an EventEntity as its target and tells the NavigationManager to navigate to that event. The system calls this whenever someone taps an event result in Spotlight or Siri, or asks Siri to open one.\n\nAnd that's it! Now if I tap on an event… this time, CometCal opens straight to the detail view of my Meteor Shower Watch Party. That's the OpenIntent bridging the gap. Siri can now understand the app's content better than ever, so people can ask Siri about any of it. Not bad for three structs and filling out a few code snippets, right? There's one more thing I can do to make my conversation with Siri feel even more natural. When someone has a specific event on screen, they might want to say something like \"email the people in this event\" without having to say the event's title. That's onscreen awareness... and it takes just two view modifiers. In CometCal's CalendarListView, where it lists all of my events, I'll add an .appEntityIdentifier modifier to the list, passing in an EntityIdentifier for each of the event entities. This connects the list to its entities, so when someone is browsing the list, the system knows which events are on screen. In the event detail view, when a single event's details are on screen, I'll append a .userActivity modifier with an EntityIdentifier. This tells the system that one specific event is front and center so Siri can resolve this event to exactly the one being viewed.\n\nAnd that's it! Here's what it enables.\n\nNow that Siri can understand what's currently onscreen and open event entities, I'll ask Siri to open the event in a natural way: \"Hey Siri, open that third event.\" Now that I am currently on the Meteor Shower Watch Party detail view, I'll try referring to this event rather than saying the entire title… \"Hey Siri, email the people in this event and ask someone to bring chocolate and marshmallows.\" Siri can use it's understanding of the onscreen event to find the attendees and hand them off to Mail.\n\nSeriously… two modifiers… that's all it takes to connect what's on screen to the app's content.\n\nSiri can now talk about the content. But to truly take off, I'll give Siri the ability to act. Once again, App Schemas lead the way, and this is where things get really interesting. I'll start with creating events. Just like entities, intents use code snippets too. I'll find the calendar_createEvent snippet and select it. It scaffolds the intent with the @AppIntent macro, the schema, all the parameters the schema requires, and a perform stub. The parameters, from title to note, come from the schema and I can use them in the intent's perform logic.\n\nI'll start by filling in the types. Then I'll add a @Dependency for the CalendarManager to use in the perform method. In the perform method, I'll mark it @MainActor and set EventEntity as the return value type.\n\nThe general pattern is straightforward: resolve the intent's parameters into something the data layer understands, perform the action, and return the result as an entity. For creating a calendar event, that means resolving the parameters like extracting the location from the union value, and converting recurrence if provided. I'll pass everything to calendarManager's createEvent method and return the result as an EventEntity.\n\nHere's what's remarkable. Because this conforms to an App Schema, Siri can handle all the heavy lifting. Interpreting language, asking for clarification, and confirming details… so people can just have a natural conversation with Siri. Time to take it for a launch… Imagine I want to create a new event, but I'm mid-spacewalk and my iPhone is floating just out of reach. Instead of grabbing it, I'll just ask Siri... \"Hey Siri, create a new event in the Lunar Orbit Log.\" \"Call it Zero Gravity Yoga for June 15th, 8am.\" Siri can resolve the title, the date, and the time of the new event. When it's done, the event is added to the calendar.\n\nJust like that, a few lines of code and the app works with Siri. That's the power of App Schemas. Now that events can be created with Siri, I'll keep the momentum going with updating events.\n\nHere's UpdateEventIntent, already filled out using the calendar_updateEvent snippet. The structure is similar to the create intent, but the key difference is that most parameters are optional since someone might only change one or two things. The event parameter is what Siri resolves; everything else is optional. The perform logic follows the same pattern: resolve each parameter if provided, then pass everything to the CalendarManager's updateEvent method and return the updated event. It might be more code than the create intent, but there's nothing completely new happening here.\n\nHowever, there's one important subtlety with optional parameters in update intents worth calling out. For example, when recurrence is nil, does that mean \"don't change it\" or \"remove it\"? A simple nil check doesn't tell me which case I'm dealing with. Zooming into the recurrence logic in the perform method, the @AppIntent macro wraps each property in an IntentParameter which exposes a valueState. This is how I tell the difference. .set with an actual value means a new value is provided. .set with a nil value means it's explicitly cleared. .unset means the parameter isn't part of the request.\n\nThis pattern applies to any optional parameter where clearing the value is a meaningful action. Now that the update intent is wired up, I'll send a few commands into orbit: \"Hey Siri, move this to 10 in the evening.\" \"Yep, that's fine\" \"Also, change this to repeat weekly and move it to my Deep Space calendar\" \"Sounds good\" \"Actually, do not repeat this event\" The detail view reflects every change. All from a few lines of code and an App Schema.\n\nThat update works, but Siri displays a default result card. This is CometCal, it deserves something with more... atmosphere. By default, Siri builds the result card from the display representation. Snippet views let me replace that with a custom SwiftUI view. I've prepared a SwiftUI view that takes an EventEntity and lays out the details in a stellar way. You can get really creative here… but also remember to keep it simple and lightweight.\n\nTo wire this all up, I'll open UpdateEventIntent.\n\nI'll add ShowsSnippetView to the perform method's return type. Then in the return statement, I'll pass the EventSnippetView. The same approach works for any other intent that returns a result, like the create intent.\n\n\"Hey Siri, push the crew lunch out by an hour\" I now get the new snippet! The cosmic gradient accent, dark blue background, the updated event details and a star icon… That's the app's personality shining through within Siri. That covers the update intent. The last action to wire up is delete, and it's the simplest of the three. Here's the DeleteEventIntent. This is pretty simple… just the event and an optional span for recurring events. The perform logic finds the event and deletes it.\n\nSiri automatically handles the confirmation dialog before anything is removed.\n\nTime to test the ejection sequence: \"Siri, delete that party.\" \"Yes, delete it.\" \"Also, delete the event happening June 9th.\" \"Oh… actually, never mind.\" Siri asks for confirmation before deleting any event, and also makes sure to disambiguate when more than one event matches. Three intents. Full event management with Siri. Mission accomplished! CometCal has gone from screen-bound to fully voice-piloted. A lot got built during this video. Here are some next steps for making your own apps work with Siri. Download the CometCal sample project and explore the full implementation. Browse the App Intents documentation to explore all the available App Schemas and domains For automated testing, check out this video to learn how to write tests for CometCal using the new AppIntentsTesting framework.\n\nAnd watch \"Explore advanced App Intents features for Siri and Apple Intelligence\" to go deeper into ways to refine how your app works with Siri that weren't covered in this video. You're now mission-ready to take your app to the final frontier. Thank you so much for following along!",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Integrating your calendar app with Apple Intelligence",
+ "url": "https://developer.apple.com/documentation/AppIntents/integrating-your-calendar-app-with-apple-intelligence"
+ },
+ {
+ "title": "Donating your app’s data and actions to the system",
+ "url": "https://developer.apple.com/documentation/AppIntents/donating-your-apps-data-and-actions-to-the-system"
+ },
+ {
+ "title": "Donations and discovery",
+ "url": "https://developer.apple.com/documentation/AppIntents/donations-and-discovery"
+ },
+ {
+ "title": "Making app entities available in Spotlight",
+ "url": "https://developer.apple.com/documentation/AppIntents/making-app-entities-available-in-spotlight"
+ },
+ {
+ "title": "Making actions and content discoverable by Apple Intelligence",
+ "url": "https://developer.apple.com/documentation/AppIntents/making-actions-and-content-discoverable-by-apple-intelligence"
+ },
+ {
+ "title": "Providing contextual cues to Apple Intelligence and Siri",
+ "url": "https://developer.apple.com/documentation/AppIntents/providing-contextual-cues-to-apple-intelligence-and-siri"
+ },
+ {
+ "title": "Apple Intelligence and Siri AI",
+ "url": "https://developer.apple.com/documentation/AppIntents/apple-intelligence-and-siri-ai"
+ },
+ {
+ "title": "Calendar",
+ "url": "https://developer.apple.com/documentation/AppIntents/app-schema-domain-calendar"
+ },
+ {
+ "title": "App schema domains",
+ "url": "https://developer.apple.com/documentation/AppIntents/app-schema-domains"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/344/4/ee45cb19-e252-41f4-a2e0-e9b59238c7aa/downloads/wwdc2026-344_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/344/4/ee45cb19-e252-41f4-a2e0-e9b59238c7aa/downloads/wwdc2026-344_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "240",
+ "year": "2026",
+ "title": "Build intelligent Siri experiences with App Schemas",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/240"
+ },
+ {
+ "id": "343",
+ "year": "2026",
+ "title": "Explore advanced App Intents features for Siri and Apple Intelligence",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/343"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:22.462Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-345.json b/data/wwdc/videos/2026-345.json
new file mode 100644
index 0000000..288b859
--- /dev/null
+++ b/data/wwdc/videos/2026-345.json
@@ -0,0 +1,76 @@
+{
+ "id": "345",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/345/",
+ "title": "Discover new capabilities in the App Intents framework",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, my name is Moe, an engineer on the App Intents team. I'm excited to share with you, the new App Intents capabilities we're introducing with our 2027 releases. App Intents is the framework that lets you express your app's actions and content to other parts of the system in ways that feel natural and deeply integrated.\n\nFrom Siri and the Shortcuts app to Spotlight and Widgets, App Intents has been the engine behind some of the most delightful experiences on Apple platforms.\n\nAnd is now a key pillar of Apple Intelligence. In our 2027 releases, driven by your feature requests, we're bringing more control, more flexibility, and a significantly smoother developer experience.\n\nToday, we'll explore three areas. We'll start with entities. I'll show you new ways to share them across apps, tell the system when they're relevant, and process them at scale.\n\nThen, I'll walk you through new support for native types and union values with full Shortcuts integration.\n\nAnd finally, I'll show you how your intents can run longer, handle cancellation gracefully, and target the right process for execution.\n\nI'm going to focus on new features today. But, if you're new to App Intents or need a refresher on the basics, I'd recommend checking out \"Get to know App Intents\" from WWDC25.\n\nThat video introduces the Landmarks Travel Tracking app. I'll be building on it with the APIs we cover today. You can download the sample code and follow along.\n\nLet's dive into what's new. Your entities, your app's content, like a landmark or a playlist, live inside your app. But the people using your app don't. They move between apps all the time. Let's go through a pair of examples using Mail and Maps with my travel tracking app. I built a shortcut to share trip ideas with friends. It finds a nearby landmark and sends it along with a message. My entity conforms to transferable from the CoreTransferable framework. So the shortcut can share it in a format Mail can use. And that works great.\n\nBut what if instead of sending it to a friend, I want to get directions to that landmark? Well, that won't work. Maps needs some structured information — a coordinate, an address, or something it can navigate to. But that kind of data doesn't have an associated data format that can be put in a file or data.\n\nThe existing file and data representations work great for known formats like PDFs or images — but not for structured types that don't have any.\n\nThis is where ValueRepresentation comes in. It's a new representation type that lets you share structured types that the system already understands.\n\nHere's my LandmarkEntity — it represents a place in the travel tracking app. It already conforms to Transferable, so I just need to add a ValueRepresentation alongside any existing representations. Inside my ValueRepresentation, I export my landmark's coordinate and name, as a PlaceDescriptor from the GeoToolbox framework. PlaceDescriptor carries coordinates and other metadata that Maps needs to navigate.\n\nIf my entity already has a PlaceDescriptor @Property, I can skip the closure entirely and use a key-path. Same result, much less code.\n\nSo, going back to my shortcut, I tap run — my landmark flows to Maps as a PlaceDescriptor, and Maps opens with directions to the landmark.\n\nYour entities now have more ways to travel across apps. Now, let's talk about helping the system suggest them when they are relevant. Suppose you're building a music app like CosmoTunes, the sample app from the video \"Explore advanced App Intents features for Siri and Apple Intelligence\".\n\nYour app has a brand new, high-tempo playlist, that's perfect for running. When someone sets up a running workout in the Fitness app, they get a list of suggested playlists.\n\nHow do you get your playlist into those suggestions? Today, you have two ways to make your content available to the system. The first is to index your content with Spotlight. This makes it available to people searching for your content in the Spotlight UI, including semantic search. This is also the primary way Siri is able to retrieve your content.\n\nThe second approach is interaction donation. When people take actions in your app, you donate those interactions to the system through the IntentDonationManager API. Over time, the system learns patterns and can suggest similar actions in the future. Siri also uses these interactions to deliver a more personalized experience.\n\nBut what about that new playlist? Nobody's searched for it in Spotlight since they don't know it exists.\n\nAnd since nobody's played it, there's no interaction to donate either. You need a way to tell the system this playlist is relevant so it can surface it at the right moment.\n\nIntroducing RelevantEntities. With RelevantEntities, you can suggest entities to the system and provide context about when and why they're relevant. Here's how this works.\n\nYou start by identifying the relevant entities — in this case, your running playlists. Next, you create a context to tell the system these playlists are relevant when someone starts a run. Then you call updateEntities to register them.\n\nThe system surfaces these playlists as suggestions in the right context — even if they were never played before.\n\nEntities stay registered until you remove them. You can removeAllEntities for a specific context, remove specific entities from a context, or clear all your entities across all contexts.\n\nNow you have more options for helping people discover your content. How do you choose between them? Use Spotlight when you want your content to be searchable and retrievable by Siri.\n\nUse interaction donation to teach Siri and the system how people use your app — so it can identify patterns and suggest actions people may want to repeat. And use RelevantEntities to hint to the system which content is relevant in specific situations — so the system can suggest it at the right moment.\n\nFor more on these topics, check out our new documentation on Spotlight and interaction donation.\n\nYour entities are shared and the system knows when they're relevant. Now, let's make them more efficient.\n\nBack in the travel tracking app. The app has landmark photos, but I also wanted to let people save their own travel photos.\n\nSo I added a photo album view — and to make the photos available to the system, I defined a PhotoEntity with an app schema for photos. This gives the system the context it needs to work with my photos across Siri, Shortcuts, and Spotlight.\n\nI also created an intent to tag my photos by keyword so people could organize and find them easily.\n\nAs the photo library grew, I noticed something. Tagging a lot of photos at once was slower than expected.\n\nLet's walk through the code to understand why.\n\nThe intent is very simple, it just adds a keyword to my photos. It takes a list of photo entities and a tag as @Parameter.\n\nAnd the perform method just applies the tag to each photo item. So why was that, actually a problem? Well, it has to do with how app intents resolve parameters.\n\nBefore an intent runs, the system resolves every entity. That means calling the entity query to populate all of its properties, so the intent has everything it may need. For most intents, that's exactly what you want. But in my case, this meant resolving hundreds or thousands of photo entities, even though my code only needs the entity ID to update my data model. So, how do I fix this? EntityCollection fixes this. It's a new type that stores an array of entity identifiers, instead of the fully resolved entities.\n\nWhen you use EntityCollection as your parameter type, the system passes just the identifiers to the intent's perform method, without resolving the full entities. Here's the updated code. I changed my @Parameter type to EntityCollection, and passed the identifiers directly to my tagging method. And that's all it took.\n\nTo confirm the fix worked, I built a Shortcut to find and tag 1000 photos. First, with a regular array of photo entities.\n\nThen with EntityCollection, which was almost instant.\n\nThe code change is small, but the performance difference is significant.\n\nNow, what happens when the same entity needs to work on multiple devices? With our 2027 releases, Siri can continue conversations across devices — and your entities can be part of those conversations.\n\nIf your app runs on multiple devices, people might start a conversation with Siri on one device and continue on another. But there's a challenge. If I ask Siri on my iPhone to add a photo to an album, then switch to my other device and ask Siri to tag that photo — Siri might not be able to find that photo. To understand why, let's think about how entities are identified. Every entity needs an ID, that's how the system finds it. Your entity's ID might be generated locally on each device. Local IDs work great on the device they were created on. But each device generates its own local IDs.\n\nSo the same entity can end up with a different ID on each device.\n\nFor Siri to reference your entities across devices, it needs a stableID that's the same everywhere. That could come from your server, or from CloudKit record IDs. Then, you need a way to tell the system your entity's ID is stable.\n\nThat's what SyncableEntity does — it declares to the system that your entity's ID is stable and can be used across devices. Here's how to adopt it.\n\nI start by adding the SyncableEntity protocol to my entity. Then, I need to provide the stable ID.\n\nIf your entity already uses an ID that's the same across all devices — like a server-assigned UUID or a CloudKit record ID — no more change is needed.\n\nBut if you use local identifiers, like CoreData row IDs, you need both: a local ID and a stable one. SyncableEntityIdentifier pairs them into a single ID. On-device, your code uses the local ID. And across devices, the system uses the stable one.\n\nSo far, we've focused on entities. Now, let's talk about the intents that use them.\n\nYour intents take parameters — the inputs people provide, like a date, a name, or an address.\n\nWhen you declare a @Parameter, the system gives you a native picker, Siri understanding, and localization for free. We're extending that same support to more native types.\n\nWe're adding native support for Duration, so no more building custom time pickers. And PersonNameComponents for structured name input instead of a plain string.\n\nAnd more.\n\nEach one gets a native picker and works everywhere your intent does — Siri, Shortcuts and Widgets.\n\nThose are individual types — one type per @Parameter. But sometimes a parameter needs to accept more than one type.\n\nA union value is a Swift enum where each case wraps a different type, letting a single parameter represent one of several options.\n\nNow that I have both landmarks and travel photos in the app, I wanted a widget that shows photos from either a photo album or a landmark collection. With @UnionValue supporting input parameters, I can use one widget for both.\n\nHere's the code. I define my union value as an enum, the @UnionValue macro. And each case wraps a different entity type — one for landmark collections, and one for photo albums.\n\nThe macro generates everything the system needs — type information, case metadata, and picker support.\n\nI also configure how each option appears in the picker.\n\ntypeDisplayRepresentation is the label for the overall type and caseDisplayRepresentations maps each case to the name shown in the picker.\n\nAnd this isn't limited to Widgets — @UnionValue parameters work everywhere your intent does, including the Shortcuts app.\n\nTo learn more, check out the travel tracking sample code project and its corresponding article.\n\nEverything we've covered so far makes your entities and parameters more expressive and efficient. Now, let's talk about execution.\n\nWhen your intent runs — from Siri, Shortcuts, or any system surface — it only has 30 seconds to finish. That works for most everyday actions. But not every intent is that quick. Now that my app supports tagging and organizing photos, I wanted to let people share their travel photos — uploading them to a shared album without opening the app. So I created an upload intent and added a button to my widget to trigger it. But with large photos, the upload takes time — and the intent kept failing because it couldn't finish within the 30-second limit. LongRunningIntent fixes this. It lets your intent run beyond the 30-second limit — and manages the background task lifecycle of your app. And as your intent runs, progress updates appear automatically as a Live Activity. Now, lets check out the code.\n\nHere's the intent I wrote to upload my photos. I'm conforming to LongRunningIntent.\n\nI take a photo file as input.\n\nThen I wrap my work in performBackgroundTask for extended execution.\n\nLongRunningIntent requires the intent to report progress, so the system knows it's still working and hasn't stalled. And because it builds on ProgressReportingIntent, I get a built-in progress object to track my work.\n\nI calculate the number of chunks for the file and set the total count, then upload each chunk and update the progress as I go.\n\nHere's what happens when my intent runs. It can run longer and there's a stop button right on the Live Activity, so the person can cancel it at anytime.\n\nThough, It'd be great if my intent got a heads-up before being stopped.\n\nCancellableIntent lets your intent clean up gracefully when cancelled — whether the person tapped cancel, the system timed out or needed to reclaim resources.\n\nHere's how I can add cancellation support.\n\nI add CancellableIntent and implement the onCancel handler. When cancellation reason happens, handler gives me the reason, and I can use it to cleanup partial uploads or cancel in-flight requests.\n\nLongRunningIntent also supports background GPU access on supported devices — for tasks like photo processing or on-device inference. Just make sure to add GPU access to your app's entitlement. To learn more about the mechanics of running tasks in the background, check out this video from WWDC25. So far we've covered how long your intent runs and what happens when it stops. Let's talk about which process runs it.\n\nAs your app grows, you may move some intents into a Widget extension, or an App Intents extension.\n\nLightweight, separate processes that can handle requests without launching your app.\n\nYou may also create a shared Swift package or framework where your intents and entities live, and import it into your app and extensions. In fact, that's exactly what I did with the travel tracking app — all my intents live in a shared package, imported by both the main app and the widget extension.\n\nWhen your intents, entities, and queries live in a shared package like this — linked by your app and extensions — the system has to decide, which process runs each intent when a request comes in. It picks a target based on heuristics like if the app is already running, it prefers the app. and if not, it launches the extension. But sometimes that's not the right choice.\n\nFor example, I wanted to add a favorite button to my widget so people can mark a photo as favorite right from the Home Screen.\n\nMy widget shares the data model with the app — but having two processes write to the same data store can cause conflicts.\n\nSo I gave the widget read-only access and the main app handles all the writes. When someone taps that button, the intent needs to run in the main app. ExecutionTargets lets you tell the system exactly which process should run your intent. Here's how.\n\nYou can target the main app, an appIntentsExtension, a widgetKitExtension, or any combination. With ExecutionTargets, you override the system's heuristics and control exactly which process handles your intent. That wraps up the new features I wanted to share.\n\nAs next steps: add ValueRepresentation to your entities so they can carry structured data across apps. Register relevant content with the system — so it gets surfaced at the right moment.\n\nAdopt EntityCollection to make your intents faster when working with large numbers of entities. And add LongRunningIntent to any intent that needs more than 30 seconds to finish.\n\nTo build your app's Siri experience step by step, check out \"Code-along: Make your app available to Siri\". And to test your intents with the new AppIntentsTesting framework, check out \"Validate your App Intents adoption with AppIntentsTesting\".\n\nI can't wait to see what you build and thanks for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "0:01",
+ "title": "Share structured entities with ValueRepresentation",
+ "language": "swift",
+ "code": "struct LandmarkEntity: AppEntity, Transferable {\n var id: Int\n var landmark: Landmark // contains CLLocationCoordinate2D\n\n static var transferRepresentation: some TransferRepresentation {\n ValueRepresentation(\n exporting: { entity in\n PlaceDescriptor(\n representations: [.coordinate(entity.landmark.locationCoordinate)],\n commonName: entity.landmark.name\n )\n }\n )\n }\n }\n\n // If the entity already has a PlaceDescriptor property, use a key-path — much less code:\n struct LandmarkEntity: AppEntity, Transferable {\n var id: Int\n @Property var placeDescriptor: PlaceDescriptor\n\n static var transferRepresentation: some TransferRepresentation {\n ValueRepresentation(exporting: \\.placeDescriptor)\n }\n }"
+ },
+ {
+ "timestamp": "5:18",
+ "title": "Register relevant entities with RelevantEntities",
+ "language": "swift",
+ "code": "// Suggest playlists for the workout session\n let playlistEntities = [dailyRun, runningMix]\n let workoutContext = AppEntityContext.audio(.workout(activityType: .running))\n\n try await RelevantEntities.shared.updateEntities(\n playlistEntities, for: workoutContext\n )\n \n // Clear all entities for a context\n try await RelevantEntities.shared.removeAllEntities(for: workoutContext)\n\n // Remove specific entities from a context\n try await RelevantEntities.shared.removeEntities(playlistEntities, from: workoutContext)\n\n // Or remove all entities across all contexts\n try await RelevantEntities.shared.removeAllEntities()"
+ },
+ {
+ "timestamp": "7:15",
+ "title": "Handle large entity sets with EntityCollection",
+ "language": "swift",
+ "code": "struct TagPhotosIntent: AppIntent {\n static let title: LocalizedStringResource = \"Tag Travel Photos\"\n\n @Parameter var photos: EntityCollection // was: [PhotoEntity]\n @Parameter var tag: String\n\n func perform() async throws -> some IntentResult {\n modelData.tagPhotos(ids: photos.identifiers, tag: tag) // was: tagPhotos(photos, tag: tag)\n return .result()\n }\n }"
+ },
+ {
+ "timestamp": "10:14",
+ "title": "Make entity IDs stable with SyncableEntity",
+ "language": "swift",
+ "code": "// If your ID is already stable across devices (server UUID, CloudKit record ID):\n struct PhotoEntity: AppEntity, SyncableEntity {\n var id: Int // Already stable across devices — that's it\n }\n \n // If you use local IDs, pair a local and a stable ID:\n struct PhotoEntity: AppEntity, SyncableEntity {\n var id: SyncableEntityIdentifier\n\n init(localID: String, stableID: String) {\n self.id = SyncableEntityIdentifier(local: localID, stable: stableID)\n }\n }"
+ },
+ {
+ "timestamp": "11:58",
+ "title": "Accept multiple types with @UnionValue",
+ "language": "swift",
+ "code": "@UnionValue\n enum TravelGalleryContent {\n case landmarkCollection(LandmarkCollectionEntity)\n case photoAlbum(PhotoAlbumEntity)\n\n static let typeDisplayRepresentation: TypeDisplayRepresentation = \"Travel Gallery\"\n static let caseDisplayRepresentations: [Cases: DisplayRepresentation] = [\n .landmarkCollection: \"Landmark Collection\",\n .photoAlbum: \"Photo Album\"\n ]\n }"
+ },
+ {
+ "timestamp": "13:41",
+ "title": "Run beyond 30 s with LongRunningIntent + CancellableIntent",
+ "language": "swift",
+ "code": "struct UploadPhotoIntent: LongRunningIntent, CancellableIntent {\n static let title: LocalizedStringResource = \"Upload Photo\"\n\n @Parameter var photo: IntentFile\n \n func perform() async throws -> some IntentResult & ProvidesDialog {\n let result = try await performBackgroundTask {\n let chunks = calculateChunks(for: photo)\n progress.totalUnitCount = Int64(chunks)\n\n for chunk in 1...chunks {\n try Task.checkCancellation()\n try await uploadChunk(chunk)\n progress.completedUnitCount = Int64(chunk)\n }\n return \"Upload complete!\"\n } onCancel: { reason in\n cleanup(for: reason)\n }\n return .result(dialog: \"\\(result)\")\n }\n }"
+ },
+ {
+ "timestamp": "16:54",
+ "title": "Control which process runs your intent with ExecutionTargets",
+ "language": "swift",
+ "code": "// Write operation — needs the main app\n struct UpdateFavoriteIntent: AppIntent {\n static var allowedExecutionTargets: ExecutionTargets { .main }\n }\n\n // Standalone download — runs in the extension\n struct DownloadPhotoIntent: AppIntent {\n static var allowedExecutionTargets: ExecutionTargets { .appIntentsExtension }\n }\n\n // Display-only — runs in the widget extension\n struct GetLandmarkStatusIntent: AppIntent {\n static var allowedExecutionTargets: ExecutionTargets { .widgetKitExtension }\n }\n\n // Works in either — lets the system choose\n struct TagPhotosIntent: AppIntent {\n static var allowedExecutionTargets: ExecutionTargets { [.main, .appIntentsExtension] }\n }"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Adopting App Intents to support system experiences",
+ "url": "https://developer.apple.com/documentation/AppIntents/adopting-app-intents-to-support-system-experiences"
+ },
+ {
+ "title": "App Intents",
+ "url": "https://developer.apple.com/documentation/AppIntents"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/345/4/bc719e14-772a-4737-aceb-6e54cda6b511/downloads/wwdc2026-345_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/345/4/bc719e14-772a-4737-aceb-6e54cda6b511/downloads/wwdc2026-345_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:22.532Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-347.json b/data/wwdc/videos/2026-347.json
new file mode 100644
index 0000000..113b127
--- /dev/null
+++ b/data/wwdc/videos/2026-347.json
@@ -0,0 +1,110 @@
+{
+ "id": "347",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/347/",
+ "title": "Secure your app: mitigate risks to agentic features",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Privacy & Security"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Howdy, I'm Willy.\n\nToday, I'll be telling you how you can identify and mitigate new risks to agentic features within your app. Later, my colleague Akshay will provide the concrete actionable steps you can take to secure your app using APIs available on our platform. With large language models, or LLMs, becoming commonplace, many apps are looking at ways to use them to enable new, intelligent features, making the LLM a key system component. Within your application, you can send instructions and a prompt containing the user's request and extra context to an LLM to have it execute one or multiple actions to get intermediate results, until finally providing a response to the user. Our platform lets you create agentic experiences using either the Foundation Models framework to design your own agent, or the App Intents framework to let your app work with Siri.\n\nNow, with new capabilities comes new security risks.\n\nLLMs introduce a new probabilistic engine within your application that is both powerful, but risks being tricked. The purpose of this talk is to highlight the new security risks with agentic features, and provide techniques and APIs you can use to protect your users. The key thing is, we want to make sure that your app works as you intend and with the user's security in mind.\n\nBefore going forward, we want to make clear what this talk is not covering. We won't be talking about model safety, which refers to ensuring that what the model outputs is safe. And we also won't be discussing model guardrails and protecting against circumvention.\n\nWhile some of the principles we will discuss could be used to handle such cases, we'll be focusing on an external attacker trying to compromise your application.\n\nIf you want to review model safety, check out the great talk linked below.\n\nLet's start by talking about the new risks that come with agentic systems. To begin, we'll start by considering why an attacker may try to target your app. Your application may do things an attacker is interested in, such as: host sensitive user data, perform financial transactions, access system resources like the microphone or camera, or even control physical devices. An attacker may want to exploit your application to achieve their goals. To help illustrate our attacks and mitigations, we'll be working with the Loose Leaf app, an example app that could be the next generation social network for all things tea, from hot to cold to boba, we're certain Loose Leaf is the future of social networking.\n\nLoose Leaf already ships with some exciting features such as being able to message tea recipes to friends, or the ability to share the incredible tea-party photos you took.\n\nWe previously talked about how you can threat model and mitigate these traditional features. It's worth reviewing that video to ensure we don't forget our essentials. Now, the Loose Leaf developers have been brewing and they're excited to announce a spicy new feature called\" \"Organize a tea party,\" which uses the Foundation Models and App Intents framework to: look at your calendar to find the best time to host a tea party, determine what friends should be invited along what teas to serve based on their profiles, and also order the teas that everyone likes! Wow! This new feature relies on Loose Leaf's agentic loop and has two notable properties.\n\nFirst, this feature takes in context from multiple locations to help the agent make decisions. Second, the agent can call one or more actions on the user's behalf that can have different kinds of side effects.\n\nStarting with the first property, we introduce a new risk: indirect prompt injection.\n\nIndirect prompt injection refers to instructions embedded in extra context provided to the model with the intent to redirect control flow.\n\nIn our agentic loop, we see that this refers to instructions that may be embedded in the initial extra context in the prompt, or within a tool result.\n\nWhat this may look like in practice is a user requesting to start a tea party with their calendar appended, but the calendar containing an event with instructions to the model to perform another action, such as deleting sensitive user data! Yikes! Part of the threat modeling exercise we'll go through is identifying all the sources of untrusted context. The second property of our agentic system is the action calling capability, which could potentially have side effects, or unintended consequences of executing the action.\n\nWhen combined with indirect prompt injections, an attacker could cause an action with side effects to be executed that achieves their goal, such as exfiltrating your user's data, stealing money, controlling physical devices, or deleting data.\n\nWhen an indirect prompt injection leads to an unintended action, we can consider the injection having two different effects. First is data poisoning, which we refer to an attacker influencing the parameters of an executed action. For example, a user may want to send a message to their mom, but an attacker injects an instruction to send a message to themselves instead.\n\nSecond is action poisoning, where the attacker influences what action to execute. A user may simply ask to summarize an email, but an attacker could steer the LLM to open a malicious web page with the email appended to an attacker-chosen URL instead.\n\nConceptualizing these risks, we can look to Simon Willison's Lethal Trifecta, which describes that a user is in most danger whenever an agentic system has: access to private data, exposure to untrusted content, and the ability to externally communicate. This last bullet we can further generalize to consider the risk of actions with any side effect.\n\nSummarizing this section, we want to emphasize that solving indirect prompt injection is an active research area, meaning that our best approach at the moment is to understand how much your app is at risk, and aim to mitigate that risk. Now that we've discussed the risks that come from agentic systems, we will go through a threat modeling exercise that you can do on your app to identify untrusted sources of data and identify potentially risky actions. We start by performing a data flow analysis for our agentic loop input, aka the prompt.\n\nWe want to identify the data sources you'll use to construct your prompt. For this exercise, we want to pinpoint the sources of untrusted context that may contain prompt injections. Going back to our Loose Leaf app, there are a few data sources that feed into prompt construction. First, is the instructions that provides the LLM some guidance about its purpose and role. Next, is the user's prompt, the task the LLM will work on and the goal it's trying to achieve.\n\nThe prompt can also include extra context to help the LLM achieve the goal. Such as including past tea orders, tea recipes that user has stored, upcoming calendar events to help determine the best time for a tea party and a friend feed, to incorporate content our friends are sharing.\n\nOnce we have an understanding of data sources that feed into our prompt, we need to identify what is considered untrusted. As a general rule of thumb, we can consider any inputs coming from an external entity as the attack surface.\n\nIn our case, we identify the calendar content and the friend feed as untrusted because anyone could send the user a calendar invite that may get fed into the model and a user's \"friend\" could post anything on their feed that gets fed into the prompt, which could all contain prompt injections to influence the action to execute.\n\nAfter identifying the sources of untrusted context, we want to examine the actions available to the agent and what side effects they may have. First, we have the OrderTeaTool(), which is an essential action for getting tea for your tea party.\n\nThe PostAndFetchPublicFeedTool() will post on the user's feed with a message generated by the model, helpful for getting the word out to friends.\n\nThe BrewingTimerIntent() will help you during your tea party to ensure your tea is brewed for the right amount of time. Finally, Delete Photo will remove a photo from the user's feed, in case the tea leaves didn't look just right.\n\nAs we consider all these actions, we need to identify what side effects each action may have.\n\nThe OrderTeaTool() has a financial risk associated with it, meaning the user could lose money if unintentionally called.\n\nThe PostAndFetchPublicFeedTool() on the other hand has a data exfiltration risk as the model could leak sensitive information via a public post.\n\nBrewingTimerIntent() may not have side effects on its own, but if it takes a label, it could allow a prompt injection to write more instructions for later attacks.\n\nDelete Photo has a data loss risk, especially if there is no undo capability.\n\nNow that we've identified where malicious inputs could go into the LLM and the side effects actions may have, we can begin to design and implement mitigations that will protect the user. We want to highlight that we should try to focus on deterministic mitigations as a baseline because their security guarantees are easier to audit and reason about.\n\nGiven the rapid development of model capabilities, we can also consider other mitigations that have more probabilistic guarantees.\n\nHere, we present a few different mitigations that can be used to protect your application by either adding checks at the prompt level, or at the action execution stage. We've used some of these as we've designed Siri AI. Let's walk through these now.\n\nFirst, we can look back at our prompt and begin to add prompt mitigations. We can redact sensitive data, such as personally identifiable information, or PII, that may be stored in past orders. That way sensitive data never reaches the LLM and thus cannot be exfiltrated. Next, we can incorporate spotlighting to the model to indicate that this content is considered untrusted. This is a probabilistic mitigation because the prompt injection could be constructed in a way that negates the spotlighting. We suggest incorporating it, though, as different models could more effectively enforce these restrictions.\n\nNow, let's look at the action mitigations you can implement.\n\nFirst, consider which actions should have a user confirmation. These are actions that are worth having a human check before continuing due to the side effects they contain. Next, consider which tools should only work whenever the device is authenticated, or unlocked.\n\nBecause the agent may be reachable from the lock screen, actions with significant risk to the user should not be accessible.\n\nWe've walked through different kinds of mitigations and how they can apply to your system, but there are many more types that exist and we welcome you to go and explore them to mitigate the risks to your app.\n\nThe key thing to remember when threat modeling is that you want to identify what an attacker may want from your application and from there apply mitigations to address risks at the prompt level, or at the action execution stage. And now Akshay will show you concrete tools that you can use to protect your app. Take it away, Akshay! Thanks Willy. Hi, I am Akshay, and I'll show you how to secure your agentic app with some of the guardrails that Willy just discussed.\n\nIf you are building your app using Foundation Models framework, I will show you how to inject security checkpoints into your agent execution.\n\nIf you are integrating with Apple Intelligence using App Intents, I will cover the security mitigations available there.\n\nLet's start with Foundation Models.\n\nThe Foundation Models framework provides a powerful API for building agents. I am going to highlight the lifecycle event modifier API, and use it for injecting security guardrails. I will assume basic familiarity with the framework. To learn more, do checkout the excellent talk linked below.\n\nLet's first build a simple agent for our Loose Leaf app using Foundation Models.\n\nNow I don't know about you, but I can't build any agents without a cup of my Darjeeling black tea. So before anything else, we will build a tool to order teas. To define a tool, we conform to the Tool protocol. We specify the name, description, and Arguments to the tool. The model uses this metadata to understand our tool's purpose, and how to call it.\n\nThen, we provide the actual Implementation that is run when this tool is called.\n\nLet's define one more tool. The PostAndFetchPublicFeedTool posts your message to the public feed, and retrieves newly posted messages. The next step in building our agent, is to create a Profile. In the Profile, we first add model Instructions and the tools we just defined. Then we attach session properties, like which model to use. Here, we are using the on-device model.\n\nThis Profile is then used to instantiate a LanguageModelSession, which can then be used in an agentic loop.\n\nNow that we have a basic agent, we will inject our security policy.\n\nTo do this, we will use lifecycle event modifiers. These modifiers are callbacks that deterministically trigger at certain lifecycle points in a session execution.\n\nThus we can use these lifecycle events as checkpoints to implement security policy.\n\nWe will look at two of these modifiers now.\n\nLet's go back to our simplified agentic loop. Like Sisyphus, the LLM outputs an action at each iteration; this action is run by the Executor, and its output rendered back to the LLM for the next iteration. The first modifier lets us intercept tool calls before they run. This is the .onToolCall modifier. It is guaranteed to trigger when the LLM outputs a tool call, before the executor runs the tool. The important point here is if this callback throws an error, then the tool is never executed. Control returns to the loop immediately. This makes this the perfect place to enforce confirmations.\n\nGoing back to our Loose Leaf agent, we notice that the OrderTeaTool has financial impact, and that makes me very nervous. So I want to always ask for user confirmation before running this tool and transferring money.\n\nTo do this, we add an .onToolCall callback to our profile.\n\nAs this callback runs before every tool call, we first check if the current tool is the OrderTeaTool. If not, we return immediately, and the tool is run. But if it is, we ask the user for confirmation. If the user does not confirm, we throw an error, which stops the tool from running. You will replace confirmWithUser() function with your own implementation.\n\nThe point is that by adding confirmation logic to just this one point in our code, we get full coverage for all our tool calls.\n\nSo in summary, remember that this modifier runs before every tool execution, and the tool itself is not run until this callback returns. You can block tool execution by throwing an error.\n\nSo conceptually, the .onToolCall modifier runs on the output of the model. Let's now look at a modifier that helps us check the input to the model. The .historyTransform fires before the transcript is rendered to the model for inference. This happens both when a new user request arrives, and at each iteration of the loop.\n\nThe transformation modifies the tail of the transcript, and we will use that for spotlighting and redacting PII.\n\nReturning to our example, note that PostAndFetchPublicFeedTool() returns posts from a public feed. An attacker can easily post a prompt injection to that feed. We must treat this feed's output with suspicion. So we want to demarcate this output with special tags to tell the model that this is untrusted data.\n\nWe do this by adding Spotlighting delimiters inside the .historyTransform.\n\nIn the callback, we first iterate over the entries and focus only on toolOutput entries from our tool.\n\nAll other entries are copied to the output transcript unmodified.\n\nWe then modify the toolOutput entries. We iterate over the segments, and for each relevant segment, add delimiter tags.\n\nWe are using angled brackets \"<>\" in this example. You will use tags that are relevant for your model. The delimit() function, whose implementation we will skip, transforms a text segment to one with delimited content. And now let's look at redaction. Actually, we can use exactly the same idea for redacting sensitive data too. We just need to replace the delimit() function with a redaction function, that replaces sensitive data with the placeHolder string literal \"\".\n\nHere's an important thing to remember. The transformed entries are scoped to the current inference iteration only. This means that these modifications will not be visible to the next inference call. You must apply them again.\n\nFor expensive transformations that you want to persist, use the @SessionProperty annotation. This lets you apply stateful transformations to your session history. See the documentation for details.\n\nOk, so we saw how lifecycle event modifiers provide deterministic hooks for injecting security policy. But I did not cover all the modifiers. The framework provides many more, that trigger at other critical points in the agentic loop.\n\nThe framework also allows you to build your own profile modifiers, and package them in reusable components.\n\nDo see the Foundation Models documentation to learn more about these and many other powerful features. Alright, now let's change context to App Intents.\n\nApp Intents let you integrate your app with Apple Intelligence and rich system experiences like Siri, Spotlight, Shortcuts, and many more. For the rest of this talk, I will assume you are familiar with the basics of App Intents and App Schemas. To learn more, check out these great sessions linked below. As a quick recap, when an App Intent adopts an intent schema, it becomes available as a tool to the Siri model. For example, here our DeletePhotoIntent adopts the deleteAssets schema from the photos domain. Conceptually, this adds our Delete Photo action into the Siri Toolbox. And this allows Siri to reason over our tool definition and invoke it to service user queries. However, as it is the model which decides which intent to call, a prompt injection attack can let an attacker misuse your app for data exfiltration or other malicious goals.\n\nFor example, here we are running with external context, which may try to run our Delete Photo function without user intent.\n\nIf such an attack succeeds, and we don't have any other deterministic guardrails in spite of Willy's vigorous warnings, then there's a real possibility of data loss.\n\nActions which have externally visible side effects or are destructive are tempting targets for attackers. The App Intents system has a number of guardrails in place to help developers mitigate such attacks. We will look at two of these: confirmations and lock-screen authentication. Let's start with confirmations.\n\nThe system uses a risk-based, contextual confirmation mechanism. This automatically triggers confirmations on high-risk actions from your app. The risk of an action is determined by considering static action metadata and the dynamic system state.\n\nWhen an intent is chosen, before execution, the system invokes a Risk Evaluation system with intent's risk metadata. We will come back to this metadata later.\n\nThe Risk Evaluation component also takes as input the dynamic state of the system. It combines both to determine the overall risk of this intent.\n\nIf the risk is considered high, the user is asked for a confirmation.\n\nIf the user confirms this action, normal control-flow continues and the intent is executed.\n\nOn the other hand, if the user declines, execution is blocked, and the intent is never invoked.\n\nNow let's go back to risk metadata.\n\nRisk metadata is internal risk data that is assigned to all intents. It is based on the intent's side effects. Certain side effects are considered riskier than others. For example, intents that delete device state, like our DeletePhotoIntent, can be considered high-risk. Intents which exfiltrate data may also cause damage if executed in a poisoned context. And update intents that operate on shared content can also be risky.\n\nThe system is more likely to trigger confirmations for high-risk tools.\n\nSo, how is risk metadata associated with your App Intent? The risk metadata is automatically assigned to an intent when it adopts a schema. You don't have to do anything extra. Technically, it is the schemas which have risk metadata associated with them. For example, the deleteAssets schema is used to delete photos, and thus has a destructive side effect. And so our DeletePhotoIntent is assigned this destructive side effect too. But risk is subtle.\n\nLet's define a new intent that sets a brewing timer.\n\nThis intent adopts the createTimer schema.\n\nWhat do we think about the risk of this schema? On the face of it, it seems an attacker can not cause too much damage by creating timers, so perhaps we don't need to confirm this action.\n\nBut if we look further, the schema defines an optional String property for a label for the timer.\n\nNow remember that it is the model which determines the arguments for your intent.\n\nSo a prompt injection can cause this label being set to an attacker controlled value. And a subsequent query to list timers, can then pull this attacker controlled data into that context, thus poisoning the new context too.\n\nSo it is not safe to entirely skip confirmation in cases like createTimer. The system understands these in-between situations. This is where the dynamic system state we discussed earlier plays a role. This information is used to figure out if a confirmation is needed in the current system context, thus capturing the dynamic risk of this action.\n\nTo recap, remember that the confirmation system is contextual and risk-based. Your intents will inherit side effects from the intent schemas they adopt. And actions with risky side effects are more likely to be confirmed.\n\nNow let's look at lock screen authentication.\n\nAs you know, you can interact with Siri on the lock screen, without having to first unlock your device. This is great for accomplishing quick tasks, or when your hands are occupied.\n\nBut this also means that an attacker in physical possession of a locked device can potentially invoke your intent via Siri. So if you are not careful, your app can be used by such an attacker for data exfiltration or running malicious actions.\n\nThe main mitigation against such a threat is to request the user to unlock their device before running risky actions. So let's see how authentication policy is defined on an App Intent.\n\nFor custom App Intents, you can explicitly set authentication behaviour by setting the authenticationPolicy property. For example, here, as our DeletePhotoIntent is destructive, we want to ensure it doesn't run on a locked device. So we explicitly set authenticationPolicy property to .requiresAuthentication. The situation is slightly different when your @AppIntent adopts an intent schema.\n\nSchemas have their own default authenticationPolicy. This policy is set internally, based on the sensitivity of each schema and the data it handles. And similar to side effects, your intent is automatically assigned the schema's default policy.\n\nBut if you want, you can still explicitly override the default. The only constraint is that your policy has to be stricter. For example, let's assume the default policy of the deleteAssets schema is .requiresAuthentication. Then as we don't explicitly set policy here, our @AppIntent is assigned the same, and will require authentication before running.\n\nBut if we try to set a weaker policy, we get a build error which helpfully tells us the minimum allowed policy.\n\nSo to recap, authentication is an important mitigation for lock screen attacks. Schemas have their own default authentication policies, which get assigned to your App Intent. And you can override the schema policy, but only to make it stricter.\n\nSo please go and review your intents with their lock screen behaviour in mind. Now back to Willy! Quality stuff, Akshay! To summarize, the next steps for your agentic application consists of coming up with a threat model, which requires finding sources of untrusted context in your prompt, and then determining the risk level for each action based on its side effects. With Akshay's guidance, you have some stepping stones on how you can implement the best mitigations for your app using the Foundation Models framework and the App Intents framework.\n\nNow let's raise the bar, and promptly inject your defenses!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "12:50",
+ "title": "Tools",
+ "language": "swift",
+ "code": "// Tools\n\nstruct OrderTeaTool: Tool {\n let name = \"orderTeaTool\"\n let description: String = \"Orders a particular quantity of a tea from the store.\"\n // Arguments\n // Implementation\n}\n\nstruct PostAndFetchPublicFeedTool: Tool {\n let name = \"postAndFetchPublicFeedTool\"\n let description: String = \"Posts a message to the public feed.”\n // Arguments\n // Implementation\n}"
+ },
+ {
+ "timestamp": "13:13",
+ "title": "Profile",
+ "language": "swift",
+ "code": "// Profile\n\nclass LooseLeafAgent {\n struct DefaultProfile: LanguageModelSession.DynamicProfile {\n var body: some DynamicProfile {\n Profile {\n Instructions(\"You are a helpful, tea-loving assistant ... \")\n\n OrderTeaTool()\n PostAndFetchPublicFeedTool()\n }\n .model(SystemLanguageModel())\n }\n }\n}"
+ },
+ {
+ "timestamp": "13:28",
+ "title": "Session",
+ "language": "swift",
+ "code": "// Session \n\nclass LooseLeafAgent {\n struct DefaultProfile: LanguageModelSession.DynamicProfile {\n var body: some DynamicProfile {\n Profile {\n Instructions(\"You are a helpful, tea-loving assistant ... \")\n\n OrderTeaTool()\n PostAndFetchPublicFeedTool()\n }\n .model(SystemLanguageModel())\n }\n }\n\n let session: LanguageModelSession\n\n public init() {\n self.session = LanguageModelSession(profile: DefaultProfile())\n }\n}"
+ },
+ {
+ "timestamp": "14:33",
+ "title": "Confirmation via onToolCall",
+ "language": "swift",
+ "code": "// Confirmation via onToolCall\n\nvar body: some DynamicProfile {\n Profile {\n Instructions(\"You are a helpful, tea-loving assistant ... \")\n\n OrderTeaTool() // Financial impact; risky tool.\n // Other Tools\n }\n \n .onToolCall { call in\n guard call.toolName == \"orderTeaTool\" else {\n return\n }\n guard ConfirmationAction.confirmWithUser() else {\n throw LooseLeafError.userConfirmationDenied\n }\n }\n}"
+ },
+ {
+ "timestamp": "15:56",
+ "title": "Spotlighting via historyTransform",
+ "language": "swift",
+ "code": "// Spotlighting via historyTransform\n\nvar body: some DynamicProfile {\n Profile {\n Instructions(\"You are a helpful, tea-loving assistant ... \")\n\n PostAndFetchPublicFeedTool() // Returns untrusted data; requires spotlighting\n // Other Tools\n }\n\n .historyTransform {γentries in\n entries.map { entry in\n guard case .toolOutput(var toolOutput) = entry,\n toolOutput.toolName == \"postAndFetchPublicFeedTool\"\n else {\n return entry\n }\n }\n toolOutput.segments = toolOutput.segments.map { segment in\n delimit(segment: segment,\n startDelimiter: \"<>\",\n endDelimiter: \"<>\")\n }\n return .toolOutput(toolOutput)\n }\n}\n\nfunc delimit(segment: Transcript.Segment,\n startDelimiter: String,\n endDelimiter: String) -> Transcript.Segment"
+ },
+ {
+ "timestamp": "16:48",
+ "title": "Redaction via historyTransform",
+ "language": "swift",
+ "code": "// Redaction via historyTransform\n\nvar body: some DynamicProfile {\n Profile {\n Instructions(\"You are a helpful, tea-loving assistant ... \")\n\n PostAndFetchPublicFeedTool() // Returns untrusted data; requires spotlighting\n // Other Tools\n }\n\n .historyTransform {γentries in\n entries.map { entry in\n guard case .toolOutput(var toolOutput) = entry,\n toolOutput.toolName == \"postAndFetchPublicFeedTool\"\n else {\n return entry\n }\n }\n toolOutput.segments = toolOutput.segments.map { segment in\n redactPII(segment: segment,\n placeHolder: \"[REDACTED]\")\n }\n return .toolOutput(toolOutput)\n }\n}\n\nfunc redactPII(segment: Transcript.Segment,\n placeHolder: String) -> Transcript.Segment"
+ },
+ {
+ "timestamp": "23:08",
+ "title": "Intent authentication policy",
+ "language": "swift",
+ "code": "// Intent authentication policy\n\nstruct DeletePhotoIntent: DeleteIntent {\n var entities: [LooseLeafPhoto]\n\n static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication\n\n func perform() async throws -> some IntentResult {\n // Implementation\n }\n}"
+ },
+ {
+ "timestamp": "23:27",
+ "title": "Schema authentication policy",
+ "language": "swift",
+ "code": "// Schema authentication policy\n\n@AppIntent(schema: .photos.deleteAssets)\nstruct DeletePhotoIntent {\n var entities: [LooseLeafPhoto]\n\n // Example: Schema default authentication policy is .requiresAuthentication\n\n func perform() async throws -> some IntentResult {\n // Implementation\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Security Overview",
+ "url": "https://developer.apple.com/security/"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/347/4/07cdbfeb-280a-49e3-aeba-c18fbb0d32b4/downloads/wwdc2026-347_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/347/4/07cdbfeb-280a-49e3-aeba-c18fbb0d32b4/downloads/wwdc2026-347_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "242",
+ "year": "2026",
+ "title": "Build agentic app experiences with the Foundation Models framework",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/242"
+ },
+ {
+ "id": "240",
+ "year": "2026",
+ "title": "Build intelligent Siri experiences with App Schemas",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/240"
+ },
+ {
+ "id": "343",
+ "year": "2026",
+ "title": "Explore advanced App Intents features for Siri and Apple Intelligence",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/343"
+ },
+ {
+ "id": "248",
+ "year": "2025",
+ "title": "Explore prompt design & safety for on-device foundation models",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/248"
+ },
+ {
+ "id": "10189",
+ "year": "2020",
+ "title": "Secure your app: threat modeling and anti-patterns",
+ "url": "https://developer.apple.com/videos/play/wwdc2020/10189"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:22.852Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-356.json b/data/wwdc/videos/2026-356.json
new file mode 100644
index 0000000..a80bb6b
--- /dev/null
+++ b/data/wwdc/videos/2026-356.json
@@ -0,0 +1,51 @@
+{
+ "id": "356",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/356/",
+ "title": "Bringing Cyberpunk 2077 to Mac",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hello, welcome! My name is Garrett Austin. I'm an engineer on Apple's Game Performance team. Today I'll be joined by a special guest, Paweł Sasko, Associate Game Director at CD PROJEKT RED. Paweł is here to talk about Cyberpunk 2077: Ultimate Edition, an incredible achievement on Mac. It takes advantage of unique platform features and delivers great performance across a wide range of Mac devices. Now, I'll leave it to Paweł to tell us all about how it was brought to life on Mac, and how they made it stand out. Paweł? Thanks, my choom! I'm going to share more with you about the experience of bringing Cyberpunk 2077 to Mac.\n\nI'll start with an overview of Cyberpunk 2077 itself, explore how we approached the Mac development process, and finally, share the finishing touches and flourishes that made Cyberpunk 2077 stand out on Mac. So, what is Cyberpunk 2077? It's an open world RPG where you play as V, a cyberpunk mercenary operating in Night City. Players explore the city and its surroundings, meet characters, build relationships, and make a lot of decisions, some of which lead to unforeseen consequences. This is a beast of a game, a massive world, no loading screens, and a lot of data to constantly process. But, from the get go, the game was designed to be scaleable, to run on a variety of hardware, and this really helped us out when we brought it up on Mac. To this day Cyberpunk 2077 remains a benchmark that is used in the industry whenever new hardware comes out, and there are a few reasons for that. Night City is packed with moving parts, crowds, traffic, AI, physics, animation, quests, and systemic interactions all running in parallel. A lot of this pressure lands on the CPU.\n\nCyberpunk 2077 has mixed lighting everywhere, neon, signs, puddles, reflective surfaces, headlights, emissive lights, volumetrics, often all in the same scene.\n\nIt also supports advanced graphics modes like ray tracing and path tracing, which make the game much more demanding and scale strongly with GPU capability. You probably know that we've been regularly updating Cyberpunk 2077 since its release. We shipped the Ultimate Edition, introduced the Metro system, extended romances, vehicle customization and far more content to the game. So when we work on our game, we always try to make it relevant and fresh. Naturally, we were looking for other platforms where we can ship the game. So that brings us to the next question.\n\nHow did we decide to bring Cyberpunk 2077 to Mac, and why did it make sense to do it at this point? At CD PROJEKT RED, we have a pretty long history shipping on Apple platforms. We wanted to continue that. As Apple silicon developed, the hardware capabilities evolved far enough for us to bring a game on Cyberpunk's scale to the Mac, not only running it, but aiming at a serious quality level. With Apple silicon we were confident that we could deliver something we'd be comfortable putting our name on. And of course bringing the game to Mac is a chance to reach more players, which we are always happy to do.\n\nOnce we aligned on, \"Yes, this is worth doing,\" the next question was, what does \"doing it properly\" mean for Cyberpunk 2077 on Mac? We set the quality bar for ourselves, and there are three main components to it.\n\nMaintaining the visual fidelity and identity of the game was key. The look, the lighting, the materials, all of that is really tied to the very core of what Cyberpunk is. Another component is stable performance. It's really important to look at scenarios when we have heavy CPU and GPU usage, large crowds, screen space reflections, driving through dense areas, combat, and evaluate performance across heavy scenes and locations to make sure it all runs smoothly. And then last but not least, it's the finishing touches that make the experience feel native.\n\nWe always try to use the platform capabilities to their maximum. We wanted an experience that stands apart on macOS, with native features and behaviors that players love.\n\nBefore we started building the native path for Cyberpunk 2077 on Mac, we used Apple's Game Porting Toolkit to evaluate the Windows build in a translated environment on macOS. This let us gain valuable insight before writing any code.\n\nInsights like, \"Is bringing Cyberpunk 2077 to Mac feasible at the quality bar we set for ourselves?\" \"Where is the pressure likely to be, CPU, GPU, or any specific systems?\" And, \"What are the first real focus areas we should plan around?\" The goal wasn't final performance numbers. It was information, where frame time goes, what are the performance challenges in real game play, and what we should build first once we move to native.\n\nTo make the evaluation data useful, we ran a predetermined set of hotspot sequences in the evaluation environment. For each test run, we looked at the data from three angles. Statistical frame time data, from our in-engine profiler so we could compare runs consistently. Metal HUD, to correlate what we're doing in the scene with what we see in the trace, loading, shader translation, or some known events such as saving the game. And, engine internal profiling broken down into threads, so we could see which CPU systems were active and when, to make sense of hotspots and spikes. Later, once we had a native build running, we moved to using Metal HUD as the primary frame time capture tool on Mac. This let us collect comparable measurements across many devices, and most importantly, do it on builds without debug or profiling settings enabled, allowing us to capture the most reliable data possible. Once we had that approach in place, the signals became pretty clear.\n\nOn high-spec hardware, GPU time looked healthy even this early, which actually pleasantly surprised us.\n\nIt was stable enough to serve as a baseline, and it suggested that we had a realistic path for our performance targets once the Metal rendering pipeline was native. In heavy game play, the experience was influenced by CPU pressure. For example, in a city driving scenario, we could see a best case where the GPU was the limiter, but a hotspot case where the CPU time rose sharply when the scene became dense with traffic, crowds, and action.\n\nAt this stage, we saw a couple of patterns that were evaluation environmental artifacts.\n\nOne was oscillation in frame time caused by live shader translation. The other was our audio middleware, which looked heavy in some scenarios. Both of these were resolved once we moved to native binaries, and Game Porting Toolkit identified them for us, so we knew to investigate it early during our native implementation. Once we had the signals, we could turn the evaluation into a production roadmap.\n\nFirst was making Mac a real target: Native builds and libraries, and adjusted data pipelines.\n\nThen we aimed for a playable build by bringing up the rendering and shader paths with the Metal API and Metal shader converter. Once that was in place, the rest was the fun part, making it shippable. Adding platform native features, then optimizing for performance and adding more polish.\n\nThere were three core parts to making Mac a real target in our pipeline. First, native builds on macOS. We needed Apple silicon builds produced with the macOS toolchain, not only the game executable, but also the development tools that we rely on in the process. Then, there's the data pipeline. Internally Cyberpunk 2077 had a full build pipeline set up for all the platforms we support, and we have added macOS as yet another parallel platform, following the same process. The platform-specific outputs, such as archives and shader cache, had to be generated following this already-existing pipeline.\n\nFinally, the architecture bridge. The game and engine had years of assumptions from other CPU architectures, so at the beginning of the process, we validated what works with unit testing, and identified what changes were required to have it run well on Apple silicon. After this step, we could move on to the shader pipeline.\n\nAs soon as we had a basic Metal path that could display frames, Metal shader converter helped us get broad shader coverage quickly, so we could render meaningful scenes. In practice, we treated this process as a loop.\n\nIntegrate Metal shader converter into our shader build, so Metal shader output was generated as a part of normal builds.\n\nValidate repeatable scenes, and look for differences in lighting response, materials, and post effects.\n\nRefine the smaller set of advanced shaders or edge cases where the converted result didn't quite match what we expected. Then, repeat this process as part of the build and test pipeline.\n\nIn parallel to bringing shaders online with Metal shader converter, we were building out the native Metal rendering foundation. We started with unit tests, bringing the Metal backend up piece by piece, and checking the base output carefully before adding more complexity.\n\nOnce that base layer was solid, we moved on to stationary in-game scenes. That's where you can validate the parts unit tests don't cover very well, lighting stacks, post effects, and scene-level behavior. After that, we moved into dynamic scenes, where camera movement, streaming, and game play start to expose the real edge cases. When we implemented ray tracing and path tracing, we optimized the performance while validating that the visual output remained the same as other platforms to preserve the identity and look of the game.\n\nOnce the native Metal foundation was stable, our next challenge was scaling performance across different Macs, and this is where MetalFX Upscaling comes in as a solution. At a high level, MetalFX lets you render at a lower internal resolution and reconstruct a higher resolution output in less time. The practical benefit was quite straightforward. It gave us more performance headroom in the heavy scenes, without lowering the quality across the board.\n\nWe also used Dynamic Resolution Scaling to help maintain stable performance under load across a variety of Mac hardware. As a temporal upscaler, it helped the image hold together in motion, especially during fast traversal and VFX-heavy scenes. At that point, we had the full game on Mac, we had it running with a working build, it was rendering in a predictable way, and it was playable. From that point on, you really need to ask yourself, \"Okay, now what actually makes it ready to ship?\" For us, it's a few other components.\n\nThere's the default settings - how it looks when you launch the game for the first time. Then, how do we differentiate this version of the game with platform native features? And finally, polishing the game for a great overall experience.\n\nIt was time to take what we had built and turn it into a first-launch experience on Mac that really stood out.\n\nWe wanted to provide great performance on the first launch for every Mac. That's where the \"For this Mac\" preset comes in.\n\n\"For this Mac\" is a device-based graphics preset system - it detects the hardware in your Mac and automatically configures the best settings for your device. Regardless of which supported Mac you have, you can launch the game and get a stable, enjoyable starting point right away.\n\nOur process started with picking settings that maintain image fidelity for each supported Mac device. We set our target FPS to either 30 or 60, MetalFX was used with Dynamic Resolution Scaling, and we adjusted minimum and maximum resolution boundaries to hit the target FPS.\n\nUnique to the \"For this Mac\" preset, we also adjusted video settings. This was where we set the final output resolution, which Dynamic Resolution Scaling operates within. We set VSync for proper frame pacing with our FPS target. We also enabled HDR, based on the display's capabilities. We then further tuned every setting in our game one-by-one, for each Mac, to make sure they were not only performant, but also beautiful.\n\nWhen a player downloads the game and launches it for the first time, they can trust that it is optimized for their specific Mac. During our tuning process, what really helped us was using several consistent scenes that stressed the CPU, GPU, and streaming systems in different ways. Then, we collected performance data, refined our settings, and revalidated in a loop across the lineup, until we found the ideal settings for each Mac.\n\nWhat's cool for us is since the release of Cyberpunk 2077 on Mac, we've seen that other developers are starting to adopt \"For this Mac\" settings in their games as well. We think that it's pretty healthy for the ecosystem, and it makes us really happy to see it. Now, let's take a look at \"For this Mac\" in the game.\n\nHere is the \"For this Mac\" preset. For this MacBook Pro with M5 Max chip, I'm using the Ultra preset as a base.\n\nFrom here, I can see the rest of the settings.\n\nStarting with MetalFX Upscaling using Dynamic Resolution Scaling, targeting 60 frames per second, and rendering anywhere from 50 to 80% of the target output resolution.\n\nThen I can go deeper into the settings, which were tweaked even more. We didn't require a lot of tweaks for M5 Max - it's quite a capable chip.\n\nWe also adjust video settings as part of the \"For this Mac\" preset. Here, it's targeting a 60 FPS lock with VSync.\n\nResolution upper bounds are set, and 2336x1460 is chosen for the internal display. Since the MacBook Pro's display is HDR capable, HDR defaults to on.\n\nHDR is automatically calibrated using Apple's EDR APIs, players do not have to visit a calibration screen. It's a really great feature which I will talk about later. Now, I will load into one of the most taxing areas of the game. Yes, so this area is a part of Dogtown.\n\nIf you played Phantom Liberty, you know, I'm not going to spoil the story here, but I am going to load into the Black Market. This is the first moment when the player is actually entering this space.\n\nIt's really dense in geometry, lights, reflections, and characters. It's one of the heaviest areas in the whole game. Again, this is demonstrating some of the quality bar that I spoke about earlier, where we really wanted to stress frame time stability in the hardest scenes in the game. Now of course, in Cyberpunk you have no loading screens. You can traverse through the scenes in any way you want. Everything is loading in and out smoothly, and, as I go around here, there is a variety of neon and light sources. It's really, really dense, and it's all running smoothly at 60 frames per second with out of the box settings.\n\nAh, so these two gentlemen are actually our founders, Marcin Iwiński and Michał Kiciński. Our company started in a van, back when they were selling video games at the end of the 90s in Poland, so we scanned them and put them into the game.\n\nThey're all speaking Polish in all localizations of the game, they are actually doing the voice acting as well. This is just one of the easter eggs that we have hidden here. Alright so, we didn't stop at just settings optimization. We also adopted a number of platform specific features around windowing and app switching, game controllers and input devices, display and audio, and cloud save technologies that let you play anywhere. Modern gaming intersects with multitasking. Players play in different scenarios and switch contexts, and macOS can let you know when these things happen. We wanted to create a smooth system experience for players. macOS broadcasts events as NSNotifications that we can leverage.\n\nResponding to NSNotification events in the game helped us ensure the game feels native to the system, accounts for app switching, and reacts to display configuration changes. We also reduce our game's CPU and GPU activity while not in focus. Next, I'll walk you through some of these behaviors.\n\nFirst, we reduced activity while the game is in the background. When the game is not visible, we don't need to render and can save CPU and GPU resources for the player.\n\nFor this, we listen to the NSWindowDidChangeOcclusionStateNotification to know when we need to check for changes, and then check the NSWindow occlusionState to determine if we should pause rendering or if we should start rendering again.\n\nNext, display settings.\n\nPlayers might change the settings of their attached displays while multitasking. We want to make sure our game window is aware of these changes so it can correctly fill the screen.\n\nWhen we receive an NSApplicationDidChangeScreenParametersNotification, we get the new screen resolution, and update our game window accordingly.\n\nNow, not only can the display settings change, the player might move the game to a completely new display too.\n\nAfter receiving a NSWindowDidChangeScreenNotification, we collect details about the new display. This includes for example the Display ID, resolution, mirror mode, and screen name. Based on these, we know if and how to update our game window.\n\nWe are using our own cursor to match our game style. To create a seamless system experience, we listen to two notifications to manage the cursor when the game window loses focus. For example, when I open the Game Overlay, our game cursor disappears and the system cursor appears to allow for a seamless system experience. When I close the Game Overlay, the system cursor disappears and our game cursor appears, bringing us right back into the game.\n\nThe NSWindowDidResignKeyNotification is the signal for us to show the system cursor and hide our game cursor.\n\nConversely, the NSWindowDidBecomeKeyNotification is the signal for us to hide the system cursor, and show our game cursor.\n\nAside from notifications, another thing to look for is Game Mode. Game Mode is a system feature on Apple that gives games higher priority access to the CPU and GPU, and lowers the impact of background tasks, resulting in a smoother experience.\n\nIt also improves responsiveness for wireless accessories by doubling the Bluetooth sampling rate, and that reduces latency for wireless game controllers, as well as AirPods audio latency.\n\nAnd good news, Game Mode is automatically enabled for apps that are categorized as games and our validation showed the benefits players get from it. Next, let's talk about input.\n\nOn Apple platforms, the Game Controller framework made it easy for us to implement native support for a range of third party controllers. It also supports advanced controller features like touchpad and adaptive triggers, which allowed us to easily bridge our existing implementations of these features in our game.\n\nWe also support Apple's native input devices - the Magic Mouse and trackpad.\n\nFor example, every Mac laptop has a trackpad, so make sure you design your control options to adapt to the current input.\n\nIn our case, we automatically detect and enable toggle aiming and an alternative to the middle mouse button by using a modifier key and the mouse click.\n\nNow, displays deserve their own moment.\n\nApple's high-end displays are some of the best we've seen Cyberpunk run on. Night City is built on contrast: neon highlights, dark alleys, bright signage, and they look incredible in HDR.\n\nWe implemented HDR through Apple's Extended Dynamic Range pipeline, which uniquely allows us to access information about the display so we can calibrate our HDR presentation automatically.\n\nThis is such a great benefit for players - they get the best HDR presentation and never have to manually adjust their HDR settings on a calibration screen with Apple displays.\n\nTo dynamically calibrate our HDR output, we simply poll maximumExtendedDynamicRangeColorComponentValue to get the current maximum EDR value for our display, and then send that to our tone mapper. This value can change dynamically depending on the capabilities of the display hardware and other conditions, so using this to drive your tone mapper's maximum HDR output will always result at the best possible HDR rendering on Apple displays. We automatically enable HDR for displays with sufficient EDR headroom, such as Apple's XDR displays. For this, we check the maximumPotentialExtended- DynamicRangeColorComponentValue, and see if the display's maximum potential EDR value is greater than 2.0.\n\nIf so, we enable HDR by default for a seamless experience. Audio is yet another area where Apple platforms offer something special. Cyberpunk 2077's soundscape is designed for spatial audio. We were able to take advantage of Apple's spatial audio APIs to enable head-tracked spatial audio for players with AirPods. This is a genuinely unique way to further immerse yourself within the game, and it's enabled by default with no additional setup.\n\nOur game has existing support for spatial audio with our audio middleware, and our audio middleware implements Apple's spatial audio APIs via AVAudioEngine. We enabled head tracking for AirPods by setting the AVAudioEnvironmentNode's listenerHeadTrackingEnabled property to true Finally, we wanted to reduce friction for our players when introducing our game to a new platform. On Mac, we support iCloud Drive integration, allowing you to transfer save files between your Apple devices. And because we have our own in-house cross‑progression solution, players can continue their save across platforms.\n\nThey can start anywhere, continue playing on Mac, and vice versa.\n\nSo what's the outcome? A native Cyberpunk experience across Apple silicon Macs, with platform features integrated in a way that supports our quality bar.\n\nWe've sold 35 million copies across all platforms and an additional 10 million copies of Phantom Liberty.\n\nIt has been reviewed very favorably by our players on the App Store, which we are very thankful for.\n\nAnd Cyberpunk 2077: Ultimate Edition was recognized as Mac Game of the Year in Apple's 2025 App Store Awards. We are really, really thankful for this recognition. It was a lot of work, but we truly believe it was worth it.\n\nAt the end of the day, what matters is not how much work you put into something, but what the result is for our players.\n\nAlright, that is it from us right now, choom. Thank you so much! It was a pleasure sharing this with you.\n\nThanks Paweł.\n\nWe hope you've enjoyed this special presentation.\n\nIf you're feeling inspired, try out Game Porting Toolkit's evaluation environment to see how your next game runs on macOS. There's no code required, and setup is fast.\n\nUse Metal HUD to evaluate your game's performance within Game Porting Toolkit.\n\nThen, make your game stand out with a great first launch experience. Provide optimized settings for your players, and enable platform native features like EDR and head-tracked spatial audio with AirPods. Check out \"Speedrun your game port with agentic coding\" to learn about new agentic skills in Game Porting Toolkit 4.\n\nFinally, you can discover powerful analysis tools in \"Find and fix performance issues in your Metal games.\" Alright, thanks for watching. We look forward to playing your games on Apple platforms!",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Performing your own tone mapping",
+ "url": "https://developer.apple.com/documentation/Metal/performing-your-own-tone-mapping"
+ },
+ {
+ "title": "Personalizing spatial audio in your app",
+ "url": "https://developer.apple.com/documentation/PHASE/personalizing-spatial-audio-in-your-app"
+ },
+ {
+ "title": "Download the Game Porting Toolkit",
+ "url": "https://developer.apple.com/games/game-porting-toolkit/"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/356/5/d3ce460b-554d-4760-ae03-072c5acf42aa/downloads/wwdc2026-356_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/356/5/d3ce460b-554d-4760-ae03-072c5acf42aa/downloads/wwdc2026-356_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "388",
+ "year": "2026",
+ "title": "Find and fix performance issues in your Metal games",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/388"
+ },
+ {
+ "id": "357",
+ "year": "2026",
+ "title": "Speedrun your game port with agentic coding",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/357"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:22.956Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-357.json b/data/wwdc/videos/2026-357.json
new file mode 100644
index 0000000..f37b015
--- /dev/null
+++ b/data/wwdc/videos/2026-357.json
@@ -0,0 +1,72 @@
+{
+ "id": "357",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/357/",
+ "title": "Speedrun your game port with agentic coding",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Graphics & Games"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm David Srour, Engineer with the Metal Ecosystem Team. We're at a moment where coding agents are fundamentally changing how software gets built. Game porting is no different. What used to take months of manual platform work can now happen in a fraction of the time with the right agent workflow.\n\nThe lineup of games on Apple platforms keeps growing, and they look and play great, thanks to Apple silicon, beautiful displays, and immersive audio. Many of these games were able to come to the platform much more quickly, with the help of the Game Porting Toolkit.\n\nAnd now, Game Porting Toolkit 4 brings you new agentic skills, that give your coding agent the expertise it needs to port your game to Apple platforms far faster and with better quality than ever before. Today I'll show you exactly how.\n\nIn a typical porting workflow, you've got to scope out the work, get a build going and convert your shaders, bring up the renderer, remap all your inputs, add polish for a native platform feel, and optimize for performance. This takes time. But with Game Porting Toolkit 4, agentic skills take the stage.\n\nAnd can get you there a lot faster by bridging knowledge gaps in porting to the platform, improving the quality of the agent's work, and reducing the guidance it needs from you.\n\nThe toolkit includes: Expert skills that provide technical guidance. And workflow skills that provide a structured approach.\n\nAnd tying it all together is the porting assistant agent. It orchestrates the porting methodology so you can focus on the decisions that matter.\n\nExpert skills increase the odds that the agent will output optimal code.\n\nIt does this by providing platform knowledge, applying best practices, and flagging common porting anti-patterns. That means less time debugging and a solid foundation for everything that comes after.\n\nThe porting assistant ensures the relevant skills are used at the right time. During execution, all planned milestones automatically load the necessary expert skills without relying on whether the model decides to use them or not.\n\nValidation checks each skill's anti-patterns and best practices, and compares against ground truth captures from the evaluation environment.\n\nThe agent stores what it learns across milestones, so nothing gets lost between sessions.\n\nTo show you how this works, I'm going to port Microsoft's MiniEngine, a D3D12 open-source engine, from Windows to macOS. Here's how I'll break this up. First, I'll show you the porting assistant, how it plans and executes. Then I'll dive into the expert skills, seeing them in action during the actual port. I am using Claude Code for everything today. Let's get into it.\n\nThe skills and assistant are provided as a plugin from the Game Porting Toolkit marketplace on GitHub.\n\nYou will first add the marketplace. And then install the plugin. All the skills are now installed and ready to be used.\n\nYou're now ready to work with the porting assistant. If you're unsure what to do, just ask the assistant.\n\nIt'll be able to guide you through its structured workflow.\n\nThe first step the agent asks for, is to run a discovery. Let's look at the workflow in more detail. At a high level, the porting assistant workflow goes through three stages. The discover workflow skill looks at your codebase, grabs reference captures from the evaluation environment, and asks you questions about your preferences.\n\nNext, you and the assistant plan milestone goals, since porting a whole title is usually too big for one session.\n\nFor each milestone, the agent executes the necessary changes and you bring your expertise to guide it along the way.\n\nFollowing execution comes validation — a multi-point checklist the agent uses to check the changes that were made in the session.\n\nThe validation workflow helps keep the port moving forward.\n\nIt checks that the app launches properly, runs Metal validation on API usage and shaders, confirms visual correctness with screen captures, compares against ground truth references, reviews the code against known anti-patterns from the used skills, checks for memory issues, and more. This phase is a great place to work with the agent to address last concerns or issues that come up with each milestone. So that's the porting assistant and its workflow. I went through this same process, discovery and planning for our MiniEngine project, and got a comprehensive porting plan. Goals and milestones with expert skills mapped to each. Every goal breaks down into milestones that I can start working through.\n\nLet's go through some of the main goals. I need a window so I can start drawing content.\n\nThen bring up the renderer with Metal 4 to get the scene on screen. Add game controller support so I can move around.\n\nAnd integrate MetalFX for upscaling. Let's see how the expert skills guide the agent through each goal with domain expertise and best practices.\n\nRight now the app builds and runs, but there's nothing on screen. I need a window to show actual content.\n\nI tell the agent to set up the window. And it loads the skills that it'll need. For the windowing and frame pacing milestone, the agent actually needs several skills working together to create a window, drive the render loop, map D3D12's swap chain model, and get the pacing right.\n\nThe window skill provides expertise in window creation and lifecycle. Translating to Metal maps the swap chain concepts.\n\nPresenting drawables covers frame pacing best practices. And the metal-cpp skill teaches correct Metal object lifetime patterns. These skills cover a lot of ground for this milestone. Usually the difficulty is in ensuring that all the best practices are followed rather than some. For the window itself, the skills set up Metal display link for the render loop, handle lifecycle events like focus changes and fullscreen, and configure the layer's resolution and color space optimally for games. For presentation, they provide guidance to leverage direct-to-display presentation for lower latency, manage drawable lifetimes to avoid stuttering, and keep textures resident so the GPU can properly access them.\n\nThis will help you avoid common pitfalls that can lead to broken window behavior, dropped frames, or blank output.\n\nAnd here's the result from working with the porting agent. A smooth color clear animation.\n\nThe Metal HUD shows steady frame pacing and I can be sure that presentation is solid before starting rendering work. Milestone complete.\n\nTime to bring up the renderer. I'm targeting lighting, shadows, SSAO, and tone mapping for first playable. Metal 4 takes the API to the next level with explicit memory management and a new command structure.\n\nThe skills bridge the gaps in current models, and carry porting expertise from real-world experience.\n\nI planned three goals with the assistant. Scene rendering, which includes the depth, shadow, and color passes. A subset of the post-processing pipeline, porting SSAO for better visual quality and tone mapping to get pixels on screen.\n\nFinally, dynamic lighting to complete the scene.\n\nI'll demonstrate how the skills guide the agent through each goal, starting with rendering the scene. Scene rendering has three milestones: GPU resources, shader pipelines, and command encoding.\n\nThe resources skill covers GPU memory, textures, and render targets.\n\nThe shader pipeline and converter skills wire the HLSL shaders through Metal's binding model. And the synchronization skill provides the synchronization patterns between passes.\n\nTo render the scene with Metal 4, the agent needs to manage GPU resources in ways specific to the API. The skill teaches which storage modes to use, including options specific to Apple silicon's tile architecture.\n\nIn Metal 4, constants are passed through buffers. The skill provides the recommended allocation pattern for this.\n\nAnd it ensures all resources are registered in a residency set for GPU access. I'll demonstrate with an example.\n\nThe skill teaches the agent to register resources in a residency set before use, much earlier in the application. This ensures every resource is accessible when the GPU needs it.\n\nWithout the skill, the agent doesn't know this step is needed and just gets something compiled. The GPU cannot read from the texture as expected, causing incorrect results.\n\nTo connect the existing shaders to Metal, the agent needs the shader pipeline and converter skills. The skill guides the agent in creating pipeline states with Metal 4's compiler.\n\nIt teaches the encoding rules for descriptor tables.\n\nIt handles the translation of D3D12 root signatures through the Metal shader converter runtime. And it provides Metal 4's argument buffer layout model. Let's look at an example. The skill teaches the agent to query argument buffer offsets from the Metal shader converter runtime. This ensures they match what the shader expects.\n\nWithout the skill, the agent copies MiniEngine's pattern of calculating offsets with index times size. But when using Metal shader converter, the layout might be different. So those offsets are wrong. No error, just incorrect rendering.\n\nTo synchronize GPU work correctly, the agent needs the synchronization skill.\n\nMetal 4 uses fully explicit synchronization, giving you precise control over resource dependencies.\n\nMultiple encoders within a command buffer need proper barriers between them.\n\nD3D12 and Metal 4 use different barrier models, and the skill maps between them.\n\nIt also provides stage mapping tables since D3D12 states don't translate directly.\n\nI'll demonstrate. The skill teaches the agent to map D3D12 states to Metal 4's producer-consumer model. The agent implements correct synchronization from the start. Without the skill, the agent resorts to broad blanket barriers at encoder boundaries.\n\nThis may work for simple cases, but can silently break as the rendering pipeline grows more complex.\n\nI have the first GPU workload running. The agent validates the output against the evaluation environment and confirms all three geometry passes match. Can't present yet without post processing, but I know it's on the right track. Time to get pixels on screen. The post-processing pipeline introduces compute dispatches for the SSAO and tone-mapping passes.\n\nPost processing has two parts. The SSAO chain, a series of compute dispatches that need correct resource setup and synchronization. And tone mapping, which transitions from compute back to render. The same rendering skills as before are leveraged for both.\n\nThese same skills now also act as guardrails.\n\nThe synchronization skill catches barrier configurations that don't account for how Apple silicon's tile memory works.\n\nThe resources skill provides safe defaults for resources not yet bound during incremental porting. And the shader converter skill catches data alignment mismatches between the engine and its shaders. I'll demonstrate.\n\nThe skill teaches the agent to leverage Metal shader converter's reflection to query the actual parameter count from the shader. The layout matches and everything aligns.\n\nWithout the skill, the agent carries over the engine's original 5 root parameters. But Metal shader converter determines the layout from the HLSL shader, which only declares 4. That mismatch shifts the sampler table to the wrong offset.\n\nI got first light! This is always a rewarding milestone. But something is off. I can clearly see SSAO working on the drapes, that's encouraging. But the overall lighting is wrong and textures on the wall surfaces are visibly stretched.\n\nLet's debug this! Normally I'd capture a frame in Xcode and diagnose the issue. But until now, an agent couldn't do that on its own. But macOS 27 introduces new command-line tools which support fully autonomous agent workflows: gpucapture for capturing a GPU frame, and gpudebug for analyzing it. Let's see how the agent leverages these new tools with the GPU debugging skill.\n\nI first describe the visual symptoms I observed. The agent loads the debugging rendering issues skill. It provides a structured methodology to go from symptoms to root causes.\n\nWith the application running, the agent leverages the gpucapture tool to capture a GPU trace.\n\nNow it's leveraging gpudebug tool to inspect the capture. It can examine anything you'd normally check in Xcode, resource bindings, constants, resource contents, and data flow through the pipeline. It's tracing where things diverge from the evaluation environment.\n\nThe agent identifies the problem, and implements a fix. The structured debugging approach means it didn't waste time guessing. It narrowed down to the problem systematically.\n\nAnd here's the corrected build of MiniEngine after the agent's fix. The lighting and texture maps now match the expected output.\n\nDuring validation, the agent uses the GPU tools again to carefully check a frame capture. The agent matches the dispatch calls and pipelines against my original trace from the evaluation environment.\n\nIt checks all details, including the dispatch dimensions.\n\nDoing such validations manually for every milestone is usually tedious. It requires loading multiple captures and comparing a large amount of data side-by-side.\n\nWith the new tools provided in macOS 27, the agent can handle this on its own much more quickly. The final goal is dynamic lights. 128 point and spot lights, computed through a light culling pass. Most of the compute infrastructure is already working, but there are new challenges with mixed resource types and new synchronization patterns.\n\nDynamic lights need a compute-based light culling pass that mixes buffers and textures, along with new GPU resources for the light data. The shader converter and synchronization skills handle the culling pass while the resources skill covers the light data.\n\nEven with the compute infrastructure in place, the skills flag issues specific to this milestone.\n\nThe residency pattern from earlier pays off once again and new light resources are made GPU-accessible automatically.\n\nThe sync skill catches invalid render stage flags the agent set on compute encoders. And the shader converter skill ensures correct binding for mixed resource types.\n\nAnd there's our first playable. The Sponza scene is properly lit with directional and dynamic lighting. The app uses all the rendering features that were ported using Metal 4.\n\nThat's first playable done. Let's keep going and add some more features to MiniEngine.\n\nKeyboard and mouse have been fine so far, but let's add gamepad support for better camera control.\n\nI prompt the agent to add controller support, and it loads the game controller skill.\n\nThe controller skill covers GCController discovery, the input model, and porting from Windows APIs. On Windows, XInput provides a fixed controller layout. The skill teaches the agent to check what each controller actually supports instead.\n\nSame idea for inputs. Rather than hardcoding a button map, the skill has the agent query what's actually available on the connected device.\n\nAnd controllers can show up or disappear at any time. The skill provides the right discovery and disconnect patterns so the agent handles that gracefully.\n\nGamepad is working.\n\nDevice discovery, thumbstick mapping, connect/disconnect events, everything is properly handled. This makes navigating the scene a lot smoother.\n\nLet's push further by integrating MetalFX features. I will be adding two distinct features of MetalFX.\n\nUpscaling renders at a lower resolution and reconstructs a higher quality image. Done right, it can match or exceed native quality.\n\nFrame interpolation doubles the effective frame rate by generating every other frame, at the cost of some input latency. Both are about getting more out of less.\n\nTo learn more, check out \"Go further with Metal 4 games.\" I ask the agent to add upscaling and frame interpolation. And it loads both expert MetalFX skills.\n\nMetalFX has two milestones: temporal upscaling and frame interpolation. Each milestone leverages a separate skill that targets that feature. The upscaling skill guides the first. The frame interpolation skill handles the second. The upscaling skill handles a lot of the tricky integration work. It configures jitter correctly in pixel space. Without it, the agent would likely use normalized values that effectively disable temporal accumulation.\n\nIt sets up motion vectors with the right scale and conventions, avoiding the ghosting you'd get from a naive setup.\n\nIt provides a good starting MIP bias for the scale factor, giving you a solid base to tune from. And it provides the history reprojection setup so the scaler can accumulate detail across frames.\n\nThe frame interpolation skill handles the full integration. It sets up a dedicated present thread to keep presentation separate from rendering. Without it, the agent might present from the render thread, leading to uneven frame spacing.\n\nIt configures precise timing so interpolated and rendered frames are evenly spaced.\n\nAnd it gets the presentation order right so the interpolated frames are properly interleaved with the synthesized ones.\n\nHere's both features running together, upscaling plus frame interpolation.\n\nThe Metal HUD shows that the temporal upscaler and the interpolator are both in use.\n\nmacOS 27 extends the Metal HUD features to help debug and validate the integration. Using the Metal HUD, you can now ensure that your MetalFX integration is behaving as expected.\n\nThe upscaler's exposure parameter is displayed so you can ensure it is properly fed to the API.\n\nYou can also look at jitter sequence information to ensure they are properly set up. Out of range jitters are marked in red and a scatter plot helps you visualize their positions.\n\nNow, let's look at the overrides options. These will help you debug your integration to get a better sense of what might be going wrong.\n\nYou're now able to visualize the exposure of your scene as observed by MetalFX. The jitter plot option will display the scatter plot in the HUD.\n\nBoth the jitter multipliers and motion vector scales can be overridden. These overrides take effect while the application is running, helping you tweak jitter multipliers or motion vector scales to debug upscaling issues.\n\nLooking at an incorrect integration, I can observe wobbling artifacts while the camera is in motion. When looking at the motion vectors information in the HUD, I see that the X-scale axis is negated. Let's make it positive in the overrides panel.\n\nAnd the motion wobbles are now fixed.\n\nNow in the rendered output, some blurriness appears on the textured objects.\n\nI'll tune the jitter multipliers to improve visual quality.\n\nHere is a side by side comparison before and after tuning the jitter multipliers.\n\nIf the HUD overrides fix the output, that tells you where the bug is. You trace it back to the logic that calculates your jitter or motion vector values and fix the source.\n\nTo recap, the new agentic skills in Game Porting Toolkit 4 accelerate bringing your game to Apple platforms.\n\nThe porting assistant provides a structured approach to using both expert and workflow skills. The skills combine knowledge and best practices to get the features in your game up and running.\n\nThe assistant also helps create comprehensive porting plans with clear milestones.\n\nI demonstrated the skills by porting Microsoft's MiniEngine as a native Mac app.\n\nNow it uses Metal 4 to render the same features as the original application.\n\nI also showed how the new gpudebug tool enables the agent to analyze frame traces.\n\nAnd the skills help out with a variety of porting tasks, ranging from windowing, game controllers, to MetalFX. I was able to use the skills and workflows in the Game Porting Toolkit 4 to port, troubleshoot, and tune the MiniEngine from PC to Mac in a fraction of the time it would have taken to do this work manually. I let the skills do the heavy lifting, while I focused on other things: making the architectural decisions, reviewing the agent's output, and providing important context about the game. The skills handled all the platform knowledge. Before I wrap up, I'll demonstrate the porting agent's work on one last codebase. I pointed the porting assistant at Godot, a production engine with an existing Metal 3 backend, and asked it to add Metal 4 alongside it. Following the same workflow, using the same skills, and validating against ground truths and best practices. It's still a collaborative effort, but the skills sped things up significantly. It was up and running in a few days. This demonstrates that the skills scale beyond small engines to production-grade projects. To get started, download and install the skills. Try them out on your game's code base. Invoke the porting assistant, and let it guide you through the porting process. You can find the skills at the Game Porting Toolkit GitHub repository.\n\nI can't wait to see how these skills help you bring amazing games to the Apple ecosystem. Thank you for watching.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:31",
+ "title": "Install Game Porting Toolkit skills",
+ "language": "swift",
+ "code": "/plugin marketplace add apple/game-porting-toolkit\n/plugin install game-porting-skills@game-porting-toolkit"
+ },
+ {
+ "timestamp": "10:24",
+ "title": "Register resources for residency",
+ "language": "swift",
+ "code": "// With skill\nresidencySet->addAllocation(texture);\nresidencySet->commit();\n// ...\nargumentTable->setAddress(texture->gpuAddress(), bindPoint);\n\n// Without skill\nargumentTable->setAddress(texture->gpuAddress(), bindPoint);"
+ },
+ {
+ "timestamp": "11:25",
+ "title": "Query argument buffer offsets",
+ "language": "swift",
+ "code": "// With skill\nIRRootSignatureGetResourceLocations(m_MtlCurIRRootSig, locations);\nsize_t offset = locations[i].topLevelOffset;\n\n// Without skill\nsize_t offset = paramIndex * descriptorSize;"
+ },
+ {
+ "timestamp": "12:34",
+ "title": "Map D3D12 states to Metal 4 stages",
+ "language": "swift",
+ "code": "// With skill\nm_MtlPendingProducerStages |= MtlProducerStageFromD3D12(OldState);\nm_MtlPendingConsumerStages |= MtlConsumerStageFromD3D12(NewState);\n// ...\nm_ComputeEncoder->barrierAfterStages(\n m_MtlPendingProducerStages,\n m_MtlPendingConsumerStages,\n MTL4::VisibilityOptionDevice);\n\n// Without skill\nm_ComputeEncoder->barrierAfterStages(\n MTL::StageDispatch,\n MTL::StageAll,\n MTL4::VisibilityOptionDevice);"
+ },
+ {
+ "timestamp": "14:24",
+ "title": "Query shader reflection parameter count",
+ "language": "swift",
+ "code": "// With skill\nIRShaderReflection* refl = IRShaderReflectionCreate();\nIRObjectGetReflection(compiledObj, IRShaderStageCompute, refl);\n// ...\ns_RootSignature.Reset(4, 2); // Reflection reveals: 4 params\n\n// Without skill\ns_RootSignature.Reset(5, 2);"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Game Porting Toolkit on GitHub",
+ "url": "https://github.com/apple/game-porting-toolkit/"
+ },
+ {
+ "title": "Download the Game Porting Toolkit",
+ "url": "https://developer.apple.com/games/game-porting-toolkit/"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/357/5/5cfd0ceb-598f-4535-9abc-12e22a778326/downloads/wwdc2026-357_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/357/5/5cfd0ceb-598f-4535-9abc-12e22a778326/downloads/wwdc2026-357_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "211",
+ "year": "2025",
+ "title": "Go further with Metal 4 games",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/211"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:23.162Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-358.json b/data/wwdc/videos/2026-358.json
new file mode 100644
index 0000000..50a6c02
--- /dev/null
+++ b/data/wwdc/videos/2026-358.json
@@ -0,0 +1,110 @@
+{
+ "id": "358",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/358/",
+ "title": "Make your game great with touch",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Graphics & Games",
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi! I'm Keyi Yu from the Game Technology team.\n\nPorting your game from Mac to iOS is incredibly straightforward, with help from the Game Porting Toolkit 4. Players can already use a wide range of game controllers across Apple devices — Mac, iPad, and iPhone.\n\nPlayers love taking their favorite games with them everywhere. They'll pull out their iPhone and jump into your game anywhere, anytime.\n\nBut in those spontaneous moments, they may not always have a controller with them. So how do you make sure they still get a fantastic, responsive experience? The answer is to give your game great touch controls.\n\nDredge, by Black Salt Games, is a perfect example of how touch controls can elevate an already engaging experience. Players get a seamless blend of gameplay and platform interaction. Everything feels natural and intuitive, letting players focus on the adventure.\n\nI'll walk you through, step by step, how to design and implement great touch controls on iOS and iPadOS, using my game as an example.\n\nHere's the plan: I'll set up touch controls for my game, create flexible layouts, design fluid interactions on screen, and provide rich feedback to the player.\n\nThe first step is to set up the touch controller. If your game already has support for game controllers, or has keyboard and mouse support, chances are you're familiar with the Game Controller framework.\n\nBuilding on top of that, the Touch Controller framework extends that support to touch input.\n\nThe core of the Game Controller framework is straightforward.\n\nIt reacts to notifications when GCController objects connect or disconnect, and then either polls active devices for their input state, or sets up value-changed handlers to be notified when input state changes.\n\nAnd once you've implemented your game logic using GCController, you're ready to add touch controls on top of it, using the Touch Controller framework.\n\nThe Touch Controller framework includes a rich set of button types and behaviors to support the most common game inputs.\n\nOn top of that, each button's appearance can be customized to best fit your game.\n\nAnd the API integrates directly with Metal to ensure the highest possible performance.\n\nWhen Touch Controller is enabled in your game, it shows up as a GCController object.\n\nSo you can poll its state, or set up handlers to listen for input updates, just like any other controller.\n\nHere's my game. It already has game controller support, so now I'll add support for touch controls.\n\nI'll start by creating a touch controller object from a descriptor. Then, I'll enable the touch controller. This automatically enables game controller logic.\n\nWith the touch controller enabled, I'll work on 2 things.\n\nFirst, in UIView, I'll add a handler for touch input. This notifies the touch controller when players start, end, and move the touch.\n\nSecond, in the Metal renderer, I render the touch controls on screen.\n\nIn the code, I'll use a descriptor to create a touch controller object.\n\nI'll enable the touchController using the connect API.\n\nThen, I'll render all the controls using the touchController's render API.\n\nIn UIKit, the touchesBegan function reports when one or more new touches occurred in a view or window. I'll override that, and call handleTouchBegan instead. You'll want to do that for touchesEnd and touchesMove also.\n\nFinally, I'll set up the polling states and valueChangedHandler.\n\nThere's one more thing I still need to do: add all the controls to the touch controller object.\n\nBut where should I place them? And how can I do it in a way that gives players the best experience? The key is setting up a flexible layout.\n\nA flexible layout means your game feels comfortable on any screen size.\n\nWith Apple's unified gaming platform, players can find your game across a wide variety of devices. The key is to plan early, and design an adaptive game interface that scales gracefully across all of them. The Touch Controller framework makes it easy to do that.\n\nIt provides nine anchor points for each layout. After you assign the anchor for a control, you can place it with an offset which is relative to the anchor.\n\nYou can group related controls into a section and assign the same anchor point to every control in that section. Then, as the device shape changes, each section stays at a consistent size and distance from its anchor point.\n\nThis keeps your controls at physically comfortable sizes for mobile players, while making the best use of available screen space across all devices.\n\nYou'll also want to make sure your controls are always visible. The way to do that is to design for the fullscreen.\n\nOn iPad and iPhone, designing a fullscreen gaming experience means keeping the safe areas in mind.\n\nSafe areas are regions of the display where you can safely put UI so that it doesn't overlap with hardware or software features. On iPad and iPhone, safe areas help you avoid placing UI where the device's rounded corners could obscure it. They also help you steer clear of the system home indicator and the Dynamic Island on iPhone, both of which can potentially overlap your controls' tap targets.\n\nOn iOS and iPadOS, you can read the safeAreaInsets from any UIView in UIKit. Then you can add the safeAreaInsets to the offset of the controls you want to place on the screen.\n\nAvoiding the safe areas is a start. But I also need to place my controls carefully so they don't interfere with the game's play area.\n\nI want to avoid placing controls where I expect movement or camera input to happen.\n\nAnd of course, I don't want to cover my character, so no controls will go in the center of the display. That leaves the regions near the thumbs, which are ideal for frequent or important actions, and the region at the top of the screen, which is a great place to put less frequently used controls like menu buttons. With that, I know exactly where to place my touch controls. Now, I'll implement these controls in my game using the touch controller I set up earlier. The Touch Controller framework provides convenient APIs to create controls. And they all follow a similar pattern.\n\nSet up the relevant properties for the control using a descriptor. Some controls have properties in common. Some properties are unique to a specific type of control. Then, using the descriptor, create a control. Add it to the TCTouchController.\n\nFor my game, I'll need to implement these controls to match my controller support. I'll start with creating button B.\n\nHere I'll create a standard circular button B. First, I initialize a TCButtonDescriptor. I set its label to TCControlLabel.buttonB. This maps it to the physical buttonB on a game controller.\n\nSince my game was already handling the input from a physical controller, I don't have to write any more game logic here. It's already handled! I just have to place the button on screen. I anchor it in the bottomRight region, and set a fixed offset.\n\nI also need to set the button's visual contents so it actually appears on screen. Finally, I call addButton on the touchController and pass this descriptor.\n\nAnd since my game is fullscreen, I'll adjust the offset using safeAreaInsets so nothing gets clipped.\n\nButton B is showing up at the bottom right in a circular shape. All the other controls follow a similar pattern. After I add all the controls into the touch controller object, they are appearing in the game.\n\nBecause I applied safe areas to the offsets, the Dynamic Island doesn't overlap any of my controls. The controls also don't cover the main character, keeping the game area clean.\n\nBut these controls don't feel quite right. I've created a direct one-to-one mapping from a physical controller. This makes the screen cluttered with controls that compete for space. But I can improve on that.\n\nIn this section, I'll clean up the clutter and design controls that feel native to touch because when interactions feel fluid and natural, players feel immersed in the gaming experience.\n\nThere are a few choices you can make here that really make a difference. I'll start with using dynamic controls. Unlike a physical game controller button, you can easily change the appearance of on screen controls.\n\nYou can choose a glyph that actually represents what the control's function is. Since I'm using system assets for my controls, I'll just change the name of the system assets from buttonB to instead display an icon showing the actual action.\n\nNow, the button B displays the icon for the strike action.\n\nAfter swapping out all the system assets the layout is much more intuitive. Players immediately know what each control does without having to check the settings or read hints during gameplay.\n\nAnd when a control's behavior changes based on context, you should update its icon to match.\n\nIn my game, besides the default strike power, the single button B can also represent the fireball or water power. So the icon should update with the flame or water drop image when players select the power.\n\nI'll create a helper function that updates the contents of button B. Then, in the cyclePower function, I'll call it with the right symbolName for each power type. This shows the players exactly what action they are playing with.\n\nNow, once a player selects their power, button B on the bottom right automatically shows the correct icon for that power.\n\nAnd importantly, when an action isn't available or relevant, remove it from the screen entirely. Don't leave controls visible that players can not use. In my game, here's how I apply this.\n\nThumbsticks are hidden when they're not being touched. The pick-up button only appears when there's an item nearby to pick up.\n\nAnd quick time event buttons are only shown when a quick time event is actually happening.\n\nThe aim and release power button should only appear when a specific power is selected.\n\nHiding thumbsticks when not in use is easy. When you create the thumbstick, just set hidesWhenNotPressed to true.\n\nFor other controls like buttons, set isEnabled to false to hide them.\n\nThe pickup button is a little different. It should appear right next to the item that needs picking up. So I update its position every time I show it.\n\nWhen it's time to dismiss it, I remove the button from the touchController entirely.\n\nAfter hiding the thumbsticks, pickup button, and quick time event buttons when they're not needed, the screen is much cleaner.\n\nOne of the real advantages of touch controls is that they can serve as both input and output. So instead of showing an overlay and cycling through actions, you can display those actions directly as touch controls.\n\nIn the buttonX press handler, I'll open the power wheel controls directly instead of showing the power wheel overlay.\n\nIn the openPowerWheel function, I add each power control to the touch controller based on what powers are currently available.\n\nThen, I set a value-changed handler for each one.\n\nAnd since these controls are not used all the time, I auto-dismiss them after three seconds if no selection is made.\n\nNow players can select a power directly from the touch controls. No overlay is needed! Smooth character and camera movement are essential for a great-feeling game. So how do you adapt those from a physical controller to touch? I'll use the fullscreen for both character and camera movement. For sprinting, I'll use a single left thumbstick without an extra button.\n\nI'll also replace the right thumbstick with a touchpad.\n\nPhysical thumbsticks have a fixed size. But on touch screen, you're not bound by those constraints at all.\n\nAnd because players can't physically feel where their finger is relative to a visual control, it's important to expand the input area as much as possible.\n\nI'll set the colliderShape to either leftSide or rightSide, which gives the thumbstick access to the entire half of the screen for touch detection.\n\nNow, the entire left half of the screen responds to the player's touch.\n\nLet's look at another issue I want to address: when the character is sprinting.\n\nIn my game, sprinting requires players to hold down the left thumbstick and move it at the same time. On a physical controller that's fine, but on touch, it means using at least two fingers simultaneously. This is really difficult to do.\n\nTo solve this, I'll embed the thumbstick button's functionality directly into the thumbstick itself. And I'll use the tilt magnitude to determine sprint.\n\nA small tilt means the character moves at a normal pace.\n\nIf it's a big tilt, the character will sprint.\n\nIn the pollInput() function, I read the leftThumbstick from GCController and get the tilt value from it.\n\nThen I do a quick magnitude check to decide whether the tilt is large enough to trigger sprinting. Now players can sprint using just the thumbstick. No second finger is required! Another issue I want to solve is the camera control.\n\nDirectly mapping the right thumbstick to camera movement on touch can cause over-rotation, and can feel sluggish. A touchpad gives you both speed and precision. The camera moves immediately. The player doesn't have to wait for it to spin around. It moves exactly as far as their finger moves, with no latency or drift at the start or end of a gesture.\n\nThe Touch Controller framework provides TCTouchpad for this. I initialize the descriptor, set its label to rightThumbstick so it maps to the existing camera logic, set colliderShape to rightSide so it covers the right half of the screen, and set reportsRelativeValues to true so it works no matter where on the screen the player touches. Then I add it to the touchController. Now, when players use the touchpad to control the camera, there's no visible control to clutter the screen. This leaves more room for the game itself. And there's no over-rotation when they move their fingers. Most modern games have some complex control combinations that are fine on a physical controller, but need rethinking for touch.\n\nIt's important to approach those thoughtfully.\n\nIn my game, there are two cases worth working through. A quick time event and an aim-to-release power.\n\nThese events usually require using two or more fingers at once on a physical controller, but on touch there are better ways.\n\nI'll start with the quick time event. One quick time event happens when the big boss freezes your character. Players need to hold L1 and R1 to break free, while also using the left thumbstick to move away from the boss.\n\nThat's a lot to manage with just two fingers! Instead, consider collapsing those two buttons into a single quick time event button. And hide it entirely when the event isn't happening.\n\nIn my game, I add this quick time event button once during setup. Unlike the pickup button, which appears at different positions, the quick time event button always appears in the same spot. So I use isEnabled to show and hide the button instead of adding and removing it each time.\n\nNow players can hold the quick time event button to escape while moving with the left thumbstick at the same time.\n\nThe other challenge is aiming to use a power. This is a common event in modern games. To throw a fireball, players need to use more than two fingers to aim, move and release power at the same time. That's very challenging in a busy game. The fix is to combine the aim and release into a single action button.\n\nIn code, I'll remove the aim and release buttons. Instead, in buttonB's valueChangedHandler, I'll call the releasePower function based on this pressed state. To implement hold and drag, capture the raw touch delta while button B is held. This has to happen in touchesMoved because it's tracked independently from the button's pressed state itself. Now, if a player wants to throw a fireball, they hold button B and drag to aim, while still moving with the left thumbstick. When they release button B, the fireball fires.\n\nThe interaction feels much smoother with this redesign.\n\nNow that players can touch anywhere on screen, it's just as important to give them clear feedback about what they're touching. Every touch control you create should have a visible pressed state. And the Touch Controller framework handles this for you by default. Thumbsticks animate as they move and the buttons highlight when pressed. But in the context of a visually busy game, you may want to go further with custom visual feedback.\n\nIn my game, players don't get much feedback when they're sprinting. I'll fix that with a strong visual indicator. I'll add a glowing halo around the outer ring of the left thumbstick when sprint is active. To do this, I create TCControlContents manually. First, I generate the halo ring TCControlImage from a halo Metal texture. It's sized slightly larger than the thumbstick background.\n\nTCControlContents is essentially an array of layers. I stack the halo control image on top of the standard background images. Then, I swap in the new TCControlContents with the halo when sprint is active, and revert to the normal background when it's not. Now, when a player is sprinting, the glowing halo around the thumbstick makes it immediately clear the character is in sprint mode.\n\nGreat! So far, I've set up a touch controller, redesigned my controls and implemented the design with Touch Controller framework in my game. Let's check how it works in the game overall! This is where I started. Every button from a physical controller mapped directly onto the screen, cluttering the game.\n\nWith all the improvements I went through in today's session, the game view is clear and controls are straightforward to use. The left thumbstick appears when players touch the screen. The pickup button shows up when there's an item nearby to pick up. The right half screen is the touchpad for camera control without over rotation. Simply press the button to pick a power that players just picked up. Hold and drag one single action button to aim and release.\n\nThe sprint indicator enhances the gameplay a lot! Players can jump into my game with only two fingers anywhere, anytime.\n\nIt's your turn to design touch controls. When done well, they can make your game feel brand new to players who pick it up on their phone. And implement those great controls with the Touch Controller framework. For more, watch \"Design great interfaces for handheld games\" and \"Level up with Apple game technologies.\" Thanks so much for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "2:04",
+ "title": "GCController polling vs. change handlers",
+ "language": "swift",
+ "code": "// Polling\nif (button.isPressed) {\n // ...\n}\n\n// Change handlers\npressedInput.pressedDidChangeHandler = { (element: any GCPhysicalInputElement,\n input: any GCPressedStateInput,\n pressed: Bool)\n // ...\n}"
+ },
+ {
+ "timestamp": "3:14",
+ "title": "Set up a TCTouchController",
+ "language": "swift",
+ "code": "// Set up a TCTouchController\nprivate(set) var touchController: TCTouchController?\n\nlet descriptor = TCTouchControllerDescriptor(mtkView: mtkView)\nif TCTouchController.isSupported {\n touchController = TCTouchController(descriptor: descriptor)\n}\ntouchController?.connect()\ntouchController?.render(using: renderEncoder)\n\noverride func touchesBegan(_ touches: Set, with event: UIEvent?) {\n for touch in touches {\n touchControls.handleTouchBegan(at: touch.location(in: view), index: touch.hash)\n }\n}\n\nbuttonA?.valueChangedHandler = { (_ button: GCControllerButtonInput, _ value: Float,\n _ pressed: Bool) in\n // ...\n}"
+ },
+ {
+ "timestamp": "8:33",
+ "title": "Create a standard circular button B",
+ "language": "swift",
+ "code": "// Create a standard circular button B\nlet buttonBDesc = TCButtonDescriptor()\nbuttonBDesc.label = TCControlLabel.buttonB\nbuttonBDesc.anchor = .bottomRight\nbuttonBDesc.offset = adjustedOffset(CGPoint(x: -35, y: -106), for: buttonBDesc.anchor)\nbuttonBDesc.contents = .buttonContents(forSystemImageNamed: \"b.circle\",\n size: buttonBDesc.size, shape: .circle,\n controller: touchController)\n// Set other properties ...\ntouchController.addButton(descriptor: buttonBDesc)\n\nfunc adjustedOffset(_ offset: CGPoint, for anchor: TCControlLayoutAnchor) -> CGPoint {\n // Adjust offset for other anchors ...\n case .bottomRight:\n x -= safeArea.right\n y -= safeArea.bottom\n}"
+ },
+ {
+ "timestamp": "10:48",
+ "title": "Change icon image",
+ "language": "swift",
+ "code": "// Change icon image\nbuttonBDesc.contents = .buttonContents(forSystemImageNamed: \"figure.fencing\",\n size: buttonBDesc.size,\n shape: .circle,\n controller: touchController)"
+ },
+ {
+ "timestamp": "11:51",
+ "title": "Update contents for button B based on context",
+ "language": "swift",
+ "code": "// Update contents for button B based on context\nfunc setButtonBContents(symbolName: String) {\n for button in touchController.buttons {\n if button.label == TCControlLabel.buttonB {\n button.contents = .buttonContents(forSystemImageNamed: symbolName, size: buttonSize,\n shape: .circle, controller: touchController)\n }\n }\n}\n\nfunc cyclePower() {\n // Get the current power type ...\n switch currentPower {\n case .strike: touchControls?.setButtonBContents(symbolName: \"figure.fencing\")\n case .fireball: touchControls?.setButtonBContents(symbolName: \"flame.fill\")\n case .waterBlaster: touchControls?.setButtonBContents(symbolName: \"drop.fill\")\n }\n}"
+ },
+ {
+ "timestamp": "13:01",
+ "title": "Hide left thumbstick when not touched",
+ "language": "swift",
+ "code": "// Hide left thumbstick when it is not touched\nlet leftStickDesc = TCThumbstickDescriptor()\nleftStickDesc.hidesWhenNotPressed = true\n// Set other properties ...\ntouchController.addThumbstick(descriptor: leftStickDesc)"
+ },
+ {
+ "timestamp": "13:19",
+ "title": "Show/hide the pick-up button",
+ "language": "swift",
+ "code": "// Show pickup button when there's an item nearby\nfunc showPickupButton(at projectedPosition: CGPoint) {\n // Calculate the position(ptX, ptY) for pickup button ...\n descriptor.offset = CGPoint(x: ptX, y: ptY)\n // Set other properties ...\n touchController.addButton(descriptor: descriptor)\n}\n\nfunc hidePickupButton() {\n for button in touchController.buttons {\n if button.label == TCControlLabel.buttonY {\n touchController.removeControl(button)\n }\n }\n}"
+ },
+ {
+ "timestamp": "13:56",
+ "title": "Show power options as touch controls",
+ "language": "swift",
+ "code": "// Show power options as touch controls\nbuttonX?.pressedChangedHandler = { (_ button: GCControllerButtonInput, _ value: Float,\n _ pressed: Bool) -> Void in\n if pressed {\n self.openPowerWheel()\n }\n}\n\nfunc openPowerWheel() {\n touchControls?.showPowerWheelButtons(fireballCount: fireballCount, has: hasWaterBlaster)\n wirePowerWheelHandlers()\n DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { [weak self] in\n guard let self = self, self.powerWheelActive else { return }\n self.closePowerWheel()\n }\n}"
+ },
+ {
+ "timestamp": "15:34",
+ "title": "Use the left half of the screen for character movement",
+ "language": "swift",
+ "code": "// Use the left half of the screen for character movement\nlet leftStickDesc = TCThumbstickDescriptor()\nleftStickDesc.colliderShape = .leftSide // Don't set as .circle\n// Set other properties ...\ntouchController.addThumbstick(descriptor: leftStickDesc)"
+ },
+ {
+ "timestamp": "16:39",
+ "title": "Calculate thumbstick tilt magnitude to trigger sprint",
+ "language": "swift",
+ "code": "// Calculate left thumbstick's tilt magnitude to trigger sprint\nfunc pollInput() {\n if let gamePad = gameController.extendedGamepad {\n let gamePadLeft = gamePad.leftThumbstick\n var moveInput = simd_make_float2(gamePadLeft.xAxis.value, -gamePadLeft.yAxis.value)\n let magnitude = simd_length(moveInput)\n if magnitude > 0.8 {\n self.runModifier = 1.3\n }\n self.characterDirection = moveInput\n }\n}"
+ },
+ {
+ "timestamp": "17:36",
+ "title": "Replace right thumbstick with a touchpad",
+ "language": "swift",
+ "code": "// Replace right thumbstick with touchpad\nlet touchpadDesc = TCTouchpadDescriptor()\ntouchpadDesc.label = TCControlLabel.rightThumbstick\ntouchpadDesc.colliderShape = .rightSide\ntouchpadDesc.reportsRelativeValues = true\n// Set other properties ...\ntouchController.addTouchpad(descriptor: touchpadDesc)"
+ },
+ {
+ "timestamp": "19:30",
+ "title": "Collapse two QTE buttons into one",
+ "language": "swift",
+ "code": "// Collapse 2 QTE buttons into 1 single button\nfunc setupControls() {\n let desc = TCButtonDescriptor()\n desc.label = TCControlLabel(name: \"escape_button\", role: .button)\n // Set up other properties ...\n touchController.addButton(descriptor: desc)\n}\n\nfunc showEscapeButton() {\n // Find escape button in touchController ...\n escapeButton.isEnabled = true\n}\n\nfunc hideEscapeButton() {\n // Find escape button in touchController ...\n escapeButton.isEnabled = false\n}"
+ },
+ {
+ "timestamp": "20:28",
+ "title": "Use button B to aim, move, and release power",
+ "language": "swift",
+ "code": "// Use button B to aim, move, and release power\nbuttonB?.valueChangedHandler = { (_ button: GCControllerButtonInput, _ value: Float,\n _ pressed: Bool) -> Void in\n self.releasePower(pressed: pressed)\n}\n\noverride func touchesMoved(_ touches: Set, with event: UIEvent?) {\n for touch in touches {\n let point = touch.location(in: metalView)\n // Handle touch input ...\n if let gc = gameController, gc.isAiming {\n let prev = touch.previousLocation(in: metalView)\n gc.aimTouchDelta += simd_float2(Float(point.x - prev.x), Float(point.y - prev.y))\n }\n }\n}"
+ },
+ {
+ "timestamp": "21:52",
+ "title": "Add a halo effect with custom TCControlContents",
+ "language": "swift",
+ "code": "// Add a halo effect around left thumbstick with customized TCControlContents\nlet haloLayer = TCControlImage(texture: haloTexture, size: haloSize, highlight: nil,\n offset: .zero, tintColor: tint)\nlet normalBgImages = TCControlContents.thumbstickStickBackgroundContents(size: bgSize,\n controller: controller).images\nhaloThumbstickBg = TCControlContents(images: [haloLayer] + normalBgImages)\nthumbstick.backgroundContents = active ? haloThumbstickBg : normalThumbstickBg"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/358/4/fdd21d54-a233-49d4-8d00-4dc51284515d/downloads/wwdc2026-358_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/358/4/fdd21d54-a233-49d4-8d00-4dc51284515d/downloads/wwdc2026-358_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:23.023Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-359.json b/data/wwdc/videos/2026-359.json
new file mode 100644
index 0000000..fcc962e
--- /dev/null
+++ b/data/wwdc/videos/2026-359.json
@@ -0,0 +1,62 @@
+{
+ "id": "359",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/359/",
+ "title": "Build real-time neural rendering pipelines with Metal",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Graphics & Games"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Yulia, a GPU Software Engineer here at Apple.\n\nToday, I'll share how to bring machine learning to your real-time rendering pipeline with Metal 4. You'll learn practical ways to integrate machine learning into your renderer, best practices for building high-performance pipelines, and two techniques to start adopting today.\n\nMachine learning is moving from research into production in real-time rendering. Across the rendering pipeline, many established techniques that have traditionally relied on analytical methods can also be implemented with machine learning.\n\nNeural denoising, neural textures, learned tone mapping and many others are among the techniques that can leverage machine learning. At every stage of the pipeline, these approaches can improve quality, performance, or memory footprint. I'll share just how this works in Metal.\n\nOn Apple platforms, you have a complete machine learning toolset for your rendering needs. At the highest level, MetalFX provides a ready-to-use neural denoising and upscaling API as a fully integrated, black box solution.\n\nThe Metal 4 ML command encoder lets you run pre-trained models directly in your command buffer, giving you more control over integration and scheduling.\n\nAnd at the most flexible level, the TensorOps API provides the building blocks to design and run custom models directly in your shaders, enabling you to fully leverage the neural accelerator introduced in our M5 and A19 Pro Apple silicon GPUs. Today, I'll talk about all of these in turn.\n\nHere is the plan. I'll cover how to adopt and achieve production quality results in your rendering pipeline with MetalFX, using Maxon's Redshift Live as an example of a modern real-time path tracing viewport that adopting MetalFX Denoising using Apple's best practices.\n\nThen, I will describe how you can train a neural tone mapper and deploy it with Metal 4.\n\nFinally, I will explain how to build a small network directly in a shader using the TensorOps API. It starts with MetalFX.\n\nIn your path tracer your frame budget might only allow you one or few samples per pixel to stay interactive. However one sample is naturally noisy.\n\nTo keep the quality bar, use MetalFX Denoising. It is designed specifically for the low latency demands of a live viewport.\n\nMetalFX Denoising is a combined neural upscaler and denoiser, the platform-integrated solution, optimized for Apple silicon.\n\nYou can integrate it easily in your pipeline. You will need to generate a few extra auxiliary inputs like diffuse albedo, depth, and a few others. Depending on your renderer, you might already have produced those. You feed all these inputs to MetalFX, which produces a beautiful denoised image.\n\nFrom there, you complete your pipeline with post processing and displaying the output.\n\nThis is Redshift Live, Maxon's modern real-time path tracer, rendering one of their high-quality 3D assets in Cinema 4D on Apple silicon. You get all the benefits of path tracing directly in the viewport, but during camera movement you can see some noise from the one sample-per-pixel presentation. Enable the MetalFX denoiser, and the image becomes dramatically more stable and noise-free.\n\nRedshift Live can now deliver clean, near-final image quality at interactive frame rates, with real-time ray-traced lighting, shadows, and global illumination. Now artists can see lighting effects take place in real-time in their viewport, like this tree being moved. This becomes possible when you combine hardware-accelerated ray tracing with MetalFX neural denoising.\n\nHere is an example of a one sample-per-pixel frame rendered by Redshift Live. By leveraging both spatial and temporal techniques, MetalFX is able to transform the noisy one sample-per-pixel into an image with near final quality, in real time. To get all the details on the inputs and how to leverage MetalFX in your application, check out \"Go further with Metal 4 games\" session.\n\nI'll outline three key best practices that Maxon used to get the best quality from MetalFX, starting with denoiser inputs and noise.\n\nThe output quality of the denoiser is directly dependent on the quality of your inputs. Normally your auxiliary inputs are noise free, do your best to keep them that way. Among all the inputs, the diffuse albedo is the strongest signal for denoising. When in doubt, make it as close as possible to a noise free version of the final result you want to see on the screen.\n\nConsider building debug views for each input directly in your engine.\n\nUse a GPU capture to inspect textures frame-by-frame. This will allow you to validate your inputs and make sure they look the way the model expects.\n\nYou might have some noise-free layers in your scene, or some parts you don't want to denoise as strongly. You have two tools at your disposal, the transparency overlay, and the denoiser strength mask, using them will help you to maximise the quality in these scenarios.\n\nParticles, fog, volumetrics, and sky are effects that don't have a meaningful surface and might be already noise free based on your rendering pipeline.\n\nMetalFX will denoise and upscale your noisy input.\n\nFor those noise free effects, you can leverage the MetalFX transparency overlay input instead. The overlay input will only be upscaled and composited in the final result for you. For areas that are already noise free, like the sky, you can configure MetalFX to skip denoising for those pixels, using the denoiser strength mask. I'll share an example.\n\nHere, the sky has been marked as not to be denoised. The value can be tuned from zero, meaning no denoising, all the way to one, meaning denoise at max strength depending on your use case.\n\nThis gives you control over the denoising effect in the scene. By now you should already have a great output by MetalFX, but there are a few tricky cases with reflection and transmission. This is what this second best practice will help you with.\n\nA mirror has no color of its own. The viewer sees the reflected surface. As discussed previously, your inputs and especially the diffuse albedo should represent the final desired output as close as possible. Store your reflected geometry properties like albedo, normal, and roughness in the mirror-like objects.\n\nGlass builds on the same foundational concepts and pushes it a bit further. The viewer sees a combination of what is reflected and what is transmitted, which could be noisy. One solution is to blend geometry properties like diffuse albedo, by the Fresnel term reducing substantially the noise of your inputs. The Fresnel is the term telling you at a given intersection point how much light would be reflected versus refracted.\n\nOn the left, you can see the primary surface albedo while on the right, it is replaced by the combined reflected and refracted albedo.\n\nThis is a well known technique called primary surface replacement. Getting this right will keep the reflection beautiful and sharp.\n\nNow that your materials look rich, and your reflection and refraction are sharp, let's dive into the third best practice: get your motion vectors right. Correct motion vectors are essential for temporal stability.\n\nMotion vectors are per-pixel screen-space displacements from the current frame to the previous frame.\n\nFor every pixel, the motion vector should answer the question, where was this pixel in the previous frame? Motion vectors have been a staple of modern rendering technique.\n\nGetting motion vectors right makes the difference between a blurry result and a sharp output under motion. The model uses motion vectors to understand the scene under motion and over time.\n\nMetalFX expects dejittered motion vectors, meaning without the sub-pixel shifts. Without this, MetalFX might receive motion vectors that might be up to one pixel wrong, creating edge shimmering. Here is how you can compute them correctly.\n\nHere is the code to compute camera-only motion vectors for static objects. You start by computing the projected position of the current vertex. Then project the same position through the previous frame's matrix.\n\nYour motion vector is the difference between the two.\n\nHowever since the camera matrices were jittered, subtract the jitter deltas from the current and previous frame.\n\nFinally, get a clean unjittered motion vector for a cleaner motion. For objects that move and deforming geometry, the camera-only path won't see the displacement. Store each vertex's previous-frame world position, or skin twice, and compute the actual motion vector. For objects where motion is genuinely unreliable fast motion, like alpha-blended particles use the reactive mask. For more on the reactive mask, check out \"Go further with Metal 4 games\".\n\nThis is what it looks like in practice. Redshift Live from Maxon ships every best practice I just covered, getting the most of MetalFX Denoising, running on Apple silicon and delivering near-final image quality.\n\nNow, I'll take you beyond platform solutions, and share how you can build your own ML-powered solutions. Neural rendering goes well beyond denoising. More and more techniques across the pipeline are becoming machine learning based, and with Metal 4, you have the tools to build and deploy your own.\n\nMetal 4 gives you two ways to bring your own machine leaning technique into the pipeline. The machine learning command encoder lets you deploy a trained model right in your command buffer in the same pipeline without context switch. The TensorOps API lets you build a small hardware-accelerated network directly in your shader.\n\nFor more details on both APIs, check out \"Combine Metal 4 machine learning and graphics\". Today I'll focus on tone mapping.\n\nMost renderers have extended post-processing pipelines to correctly map the HDR image to something that can be displayed and matches the artistic vision, like tone mapping, color grade or film emulation. The pipeline is composed of multiple stages, each with its own parameters and concatenated outputs.\n\nThe pipeline can grow arbitrarily complex. The best results come from understanding the content of the image, and that's exactly what a neural network can learn.\n\nThe idea is simple. Take your existing whole color pipeline or part of it and replace it with a single neural network. The network will learn the color transformation.\n\nAn example of such a workflow is called HDRNet. A 2017 architecture from Gharbi and colleagues.\n\nHere's the bird's-eye view on how it works. The network works on a small downsampled version of the image. It performs two types of analysis, a global and local one to capture both scene level and small details. This process allows the network to create color transformations for 16x16 tiles of the image. These localized transformations are applied with smart, edge-aware techniques to produce the beautiful tone mapped final result.\n\nTo create this solution you would first develop and train the network in your framework of choice, for example PyTorch. The training data could be deployed from manually tone mapped previous projects, or a lot of tone mapped images generated by your renderer. Once the model is trained, export it to an MTLPackage.\n\nIn order to execute your network in Metal 4, there are a few steps that need to be done on both setup and on the actual execution. First you need to setup the pipeline by loading an MTLPackage, specifying the network function with a function descriptor and creating a machine learning pipeline descriptor. This process is very similar to loading regular pipelines.\n\nThe next step is to dispatch your network execution, to do that, you will create an encoder, create an argument table with the inputs and outputs and finally dispatch the command buffer. That will kick off the execution, where you will have a mix of compute, machine learning, and rendering work happening at the same time.\n\nHere's the updated pipeline. First, your path tracer produces samples, followed by MetalFX denoising and the new neural tone mapper, all encoded in the same command buffer, executing in the same frame.\n\nThe ML encoder replaced your entire multi-stage post-processing chain with a single neural evaluation.\n\nI've shared how you can train and deploy your networks. Now, go one level deeper and build small networks directly in your shaders with the TensorOps API.\n\nSo far you have explored large general-purpose networks trained offline on a very large dataset. Now I will show you the opposite approach: tiny networks for one specific task. A few thousand parameters or less, trained on your scene data, sometimes even trained online every few frames. The network only sees one scenario, it does not need to generalise.\n\nSo far you have learned how to execute ML in the same command buffer as a stand alone step.\n\nHere it is executing alongside compute and render.\n\nHowever a small network can fit inline in your shader, among the rest of your code, ALU and texture sampling instructions.\n\nThe key enabling technology is TensorOps, available in any stage of the rendering pipeline.\n\nAll this combined unlocks new possibilities and workflows that involve online training.\n\nHere's an example, a skybox used for image based lighting. The skybox is casting light on the geometry in the scene, creating a natural soft illumination. The soft illumination is the result of the average light coming from all visible directions at a specific point. Normally, this result is precomputed offline and sampled at runtime.\n\nHowever, a scene is rarely static. You might have a dynamic day-night cycle.\n\nYour offline learned signal may be out of sync.\n\nThis is a learnable function for a neural network, and this is where online training comes into play. Here is how you could recreate this technique.\n\nBased on what you learned about the machine learning encoder so far, a simplified rendering loop might look like this, first, you update your world so that all the information is up to date for rendering.\n\nNext, you dispatch the machine learning encoder to run the inference on the model, and produce the necessary lighting information that you will use later for shading.\n\nOnline training disrupts this paradigm. By creating your own training and inference routines, you can run one or more training iterations per frame to improve the model accuracy.\n\nThis is how the online training loop would look like for the sky illumination model. You start by generating a direction you wish to sample and run inference on your model to get the result.\n\nThen you are able to compute the analytical solution to the sky illumination problem that you can use to compute the error, and finally, run a back propagation pass to progressively improve the model. This is the same exact flow you could use to train offline, but this time, repeating training iteration over frames.\n\nSo, you are now running your own inference and training routines. This enables you to run the inference pass, inline in your shading pass, And TensorOps will allow you to implement this very efficiently. You now have a model that every frame adapts to the new world condition and can use this information for shading right away. This would not be possible with the standard offline training workflow. This concept generalizes to any technique that can learn a signal. Here is how to start building your own solutions. At a high level, a neural network is composed of three main building blocks: the input layer, which processes the network inputs, also known as input features. The output layer, which generates the network's final predictions, and finally the hidden layers, where the magic of learning happens. The sky probe is a small network, the hidden layers group is composed of two hidden layers of four neurons each.\n\nThe network takes as an input value three floats to encode a direction, and produces three floats as an output that represents the average illumination coming from that direction, as a color.\n\nThis is called a fully connected multilayer perceptron, or in short an MLP, a 3 - 4 - 4 -3 network. You can experiment with the input sizes, amount and size of layers to get the best result for your application. To be able to evaluate your network you need to prepare your input tensor. It's best to batch multiple inputs at the same time making it a 2D matrix. For the sky probe example, this will be a 2D matrix of a batch of input directions you wish to evaluate. But the input can contain whatever data might be useful to the network, like positional or material data. Same principle applies to the output tensor. For sky probe, make it a 2D matrix of a batch of colors.\n\nNow that you know the structure of an MLP, here is how you can implement it in your shader and evaluate it in a forward pass.\n\nNow you are ready to begin the evaluation. You have your input tensor and the first hidden layer weights tensor. You can multiply the two together using a matmul 2D tensor operation.\n\nYou will obtain a pre-activation result on which you want to apply your activation function. Before doing that, you will need to store your matrix multiplication result. I'll explain how to do that efficiently. You may be familiar with the thread execution scope, where a single thread will be in charge of executing the whole tensor operation. This works great for executing divergent work or in pipeline stages where you don't have full control of a thread group.\n\nHowever, when you do have full control, new possibilities arise.\n\nIn a compute stage, you can use SIMD group execution scope, where all participating threads will work on the same matrix multiplicaiton.\n\nThis execution mode, will also give you access to cooperative tensors. Cooperative tensors storage is distributed among multiple threads in the thread group, avoiding an expensive round trip to main memory.\n\nBy using a cooperative tensor as an output of your first multiplication, the result will stay in fast thread storage memory. Then you can apply your activation function in place.\n\nYou can now repeat the same operation of matrix multiplication and activation for the next layer.\n\nAnd all the subsequent layers, all the way to the output layer, where you can store the resulting tensor and leverage the result in your compute shader immediately, or at a later stage.\n\nOn the left, there is the ground truth render computed using raytracing. On the right the neural rendering version. The small neural network was capable of learning the signal efficiently.\n\nThis was a high level overview of how you can construct an MLP and evaluate it in your shader using TensorOps. The same exact building blocks can be used to create an efficient back propagation pass needed for the online training step. For all the code details, please check the \"Metal Performance Primitives (MPP) Programming Guide\".\n\nTo recap, today, I have covered three levels of ML in your rendering pipeline. First, MetalFX gives you platform-integrated neural denoising, with three best practices: keep your inputs clean, store what the viewer sees, get motion vectors right. Next, the MTLPackage lets you export your offline trained models and deploy at runtime, You learned how to replace an entire post-processing pipeline with one neural evaluation. Finally, I covered the TensorOps API, it lets you build tiny networks directly in your shaders, running on the neural accelerator. Each level gives you more control. Pick the one that's right for your app.\n\nDownload Xcode and explore the Metal 4 sample code. If your app has realtime requirements, like viewports in pro-apps or games, adopt MetalFX Denoising and Upscaling.\n\nTry training a neural tone mapper with your own post-processing pipeline.\n\nAnd experiment with small specialized networks using the tensor API.\n\nCheck out our sessions from previous years for more details.\n\nI can't wait to see what you build.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "8:46",
+ "title": "Compute camera-only motion vectors",
+ "language": "swift",
+ "code": "#include \nusing namespace metal;\n\n// Compute camera-only motion vectors\nfloat4 clipCurrent = viewProjCurrent * float4(worldPos, 1.0);\nfloat2 ndcCurrent = clipCurrent.xy / clipCurrent.w;\n\nfloat4 clipPrevious = viewProjPrevious * float4(worldPos, 1.0);\nfloat2 ndcPrevious = clipPrevious.xy / clipPrevious.w;\n\nfloat2 motion = ndcPrevious - ndcCurrent;\n\n// Get subpixel offset for current and previous frames\nfloat2 jitterCurrent = getJitter(frameIndex);\nfloat2 jitterPrevious = getJitter(frameIndexPrevious);\nmotion -= jitterPrevious - jitterCurrent;"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Training a neural network to render irradiance in real time",
+ "url": "https://developer.apple.com/documentation/Metal/training-a-neural-network-to-render-irradiance-in-real-time"
+ },
+ {
+ "title": "Metal sample code library",
+ "url": "https://developer.apple.com/documentation/Metal/metal-sample-code-library"
+ },
+ {
+ "title": "Download the Metal Performance Primitives (MPP) Programming Guide",
+ "url": "https://developer.apple.com/download/files/Metal-Performance-Primitives-Programming-Guide.pdf"
+ },
+ {
+ "title": "Understanding the Metal 4 core API",
+ "url": "https://developer.apple.com/documentation/Metal/understanding-the-metal-4-core-api"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/359/5/9da4a720-0dcb-4b8e-b61b-ba8310a61f29/downloads/wwdc2026-359_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/359/5/9da4a720-0dcb-4b8e-b61b-ba8310a61f29/downloads/wwdc2026-359_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "262",
+ "year": "2025",
+ "title": "Combine Metal 4 machine learning and graphics",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/262"
+ },
+ {
+ "id": "211",
+ "year": "2025",
+ "title": "Go further with Metal 4 games",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/211"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:23.083Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-369.json b/data/wwdc/videos/2026-369.json
new file mode 100644
index 0000000..5013f93
--- /dev/null
+++ b/data/wwdc/videos/2026-369.json
@@ -0,0 +1,80 @@
+{
+ "id": "369",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/369/",
+ "title": "Find your accessory with Bluetooth Channel Sounding",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "System Services"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi! I'm Gretchen and I work on Core Bluetooth. Today, I'm excited to introduce a way to find nearby Bluetooth accessories, using Channel Sounding. First, I will give you some ideas for how you could use Channel Sounding, and how it works. Then I will explain how to implement it in your app, using Core Bluetooth to get distance, or with Nearby Interaction to get distance and direction to your accessory.\n\nNext, I will give you some tips for building hardware that supports Channel Sounding. And I will end with some next steps.\n\nLet's start with an overview.\n\nImagine I am hosting a party.\n\nI am cooking inside using my oven, and I have a smoker going in the backyard. I have Bluetooth thermometers in both places, to help me cook everything perfectly. I get a notification, that one of my thermometers has reached the temperature I set.\n\nWhen I open the app, it says the probe is 8 meters to my right. That must be the smoker! With Channel Sounding, I won't mix up my temperature probes. I can measure distance to each one! There are a couple ways to measure distance and direction to third-party accessories on iOS. For the best accuracy, you can add an Ultra Wideband chipset to your accessory and use the Nearby Interaction framework in your app. For more information on this, watch the video \"Explore Nearby Interaction with third-party accessories\". But if your accessory only has a Bluetooth chipset, then Bluetooth Channel Sounding is your best option. You might have used RSSI in the past to estimate distance, but with Channel Sounding you can actually measure distance. We encourage you to try Channel Sounding, where your app could benefit from better accuracy. So, how exactly does Channel Sounding work? Let's say we have an iPhone that is paired and connected to a Bluetooth accessory. In this scenario, the iPhone is called an initiator and the accessory is called a reflector. The iPhone sends a signal, or tone, to the accessory, and the accessory reflects that tone back. The iPhone measures how the signal changes in transit, from one side, to the other, and back.\n\nBy repeating this process across the channels of the 2.4GHz band, the iPhone observes the rate of change in these reflected tones, from one channel to the next, and uses that to estimate the distance between the initiator and reflector. This process of measuring distance is called a procedure. So, how do you perform Channel Sounding in your app? If you just need distance, you can use Core Bluetooth. Before you begin, you'll want to ensure your accessory is paired and set up, using AccessorySetupKit, and connected through Core Bluetooth. You can refer to the documentation for more on how this is done. Now let's look at the code to measure distances. First, check that Channel Sounding is supported on the local iOS device, using CBCentralManager.supportsFeatures method.\n\nOnce you have a connected CBPeripheral, call startChannelSoundingSession on the CBPeripheral object.\n\niOS will repeatedly perform Channel Sounding procedures. When each procedure is completed, the delegate method peripheral didReceive results will be called with the measured distance in meters.\n\nWhen you are ready to complete your Channel Sounding session, call cancelChannelSoundingSession.\n\nWhen the session ends, the delegate method peripheral didCompleteChannelSoundingSession will be called.\n\nNext, I'll tell you how to use Nearby Interaction to measure distance and direction. Again, ensure your accessory has been paired and set up through AccessorySetupKit, and connected via CoreBluetooth. Before creating a Channel Sounding session, check whether the local iOS device supports it, with the supportsBluetoothChannelSounding method.\n\nThen create a configuration object, passing in the peripheral.identifier from CoreBluetooth as the bluetoothChannelSoundingIdentifier.\n\nIn order to get direction, CameraAssistance is required. Be sure to enable this if you need it.\n\nFinally, create your NISession, set the delegate, and run it with the new accessory configuration you just created. If your app knows whether the accessory is moving or stationary, you can tell Nearby Interaction, and it will use that information to produce better direction estimates. Call updateMotionState on your session with your accessory object.discoveryToken.\n\nFor example, if your accessory is a mounted tag on a wall, pass .stationary. If it's attached to a moving object, pass .moving. The delegate callback is identical to what you'd get with UWB. You will receive NINearbyObjects updates with distance and direction.\n\nBoth the distance and the direction results benefit from the fusion of raw Bluetooth Channel Sounding measurements and camera inputs.\n\nRemember that both, distance and direction, are optional. Distance may be nil, if the Channel Sounding measurement failed.\n\niOS automatically filters outliers and smooths your results for a better user experience. In iOS 27, use Channel Sounding when your app is in the foreground. When your app moves to the background, your Channel Sounding session will be paused.\n\nAlso be aware, that iOS may reduce the frequency of Channel Sounding measurements, if other Bluetooth or Wi-Fi activity increases. Channel Sounding is available on iPhones with the N1 chip.\n\nNow I will explain how to make your accessories work well, with Channel Sounding on iOS.\n\nYour accessory must support Bluetooth 6.3, and the inline PCT feature is required.\n\niOS uses phase based ranging, so your chipset must also support mode-0 and mode-2, as defined by the Bluetooth spec.\n\nT_FCS is the interspace timing between tones. Make sure your accessory supports T_FCS of at least 100µs.\n\nAlright, you've heard all about Channel Sounding, but what's next? Try out the APIs with a compatible accessory. Imagine how measuring distance would improve how people interact with your app. Ask your questions on the Developer Forums and send us feedback using Feedback Assistant. Thank you!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:43",
+ "title": "Start a Core Bluetooth Channel Sounding session",
+ "language": "swift",
+ "code": "import CoreBluetooth\n\nfunc isChannelSoundingSupported() -> BOOL {\n guard centralManager.state == .poweredOn else { return }\n if #available(iOS 27.0, *) {\n // Check current device supports Bluetooth Channel Sounding\n return CBCentralManager.supportsFeatures(.channelSounding)\n }\n}\n\nfunc startChannelSounding(_ peripheral: CBPeripheral) {\n guard peripheral.isConnected else { return }\n if #available(iOS 27.0, *) { \n // Step 1: Create a CBChannelSoundingSessionConfiguration\n let config = CBChannelSoundingSessionConfiguration(role: .initiator)\n\n // Step 2: Start the channel sounding session\n peripheral.startChannelSoundingSession(config)\n }\n}"
+ },
+ {
+ "timestamp": "4:09",
+ "title": "Receive distance results and cancel a session",
+ "language": "swift",
+ "code": "import CoreBluetooth\n\n// Receive distance results\nfunc peripheral(_ peripheral: CBPeripheral,\n didReceive results: CBChannelSoundingProcedureResults?,\n error: Error?) {\n guard let results = results else { return }\n\n let distance = results.distance\n \n // Do something with distance\n}\n\n// Cancel a Channel Sounding session\nfunc cancelChannelSounding(_ peripheral: CBPeripheral) {\n guard peripheral.isConnected else { return }\n if #available(iOS 27.0, *) {\n // Cancel the channel sounding session\n peripheral.cancelChannelSoundingSession(config)\n }\n}\n\nfunc peripheral(_ peripheral: CBPeripheral,\n didCompleteChannelSoundingSession error: Error?) { \n // Session is complete\n}"
+ },
+ {
+ "timestamp": "4:41",
+ "title": "Start a Nearby Interaction Channel Sounding session",
+ "language": "swift",
+ "code": "import CoreBluetooth\nimport NearbyInteraction\n\n// Configure a Nearby Interaction Channel Sounding session\nfunc startChannelSoundingThroughNearbyInteraction(_ peripheral: CBPeripheral) {\n if #available(iOS 27.0, *) { \n // Step 1: Check current device supports Bluetooth Channel Sounding\n guard NISession.deviceCapabilities.supportsBluetoothChannelSounding else { return }\n\n // Step 2: Create an NINearbyAccessoryConfiguration\n let config = NINearbyAccessoryConfiguration(\n bluetoothChannelSoundingIdentifier: peripheral.identifier, \n previousChannelSoundingIdentifier: nil)\n\n // Step 3: Enable camera assistance for direction support\n if NISession.deviceCapabilities.supportsCameraAssistance { \n config.isCameraAssistanceEnabled = true\n }\n }\n}"
+ },
+ {
+ "timestamp": "5:19",
+ "title": "Run a Nearby Interaction Channel Sounding session",
+ "language": "swift",
+ "code": "import CoreBluetooth\nimport NearbyInteraction\n\n// Run a Nearby Interaction Channel Sounding session\nfunc runChannelSoundingThroughNearbyInteraction(_ config: NINearbyAccessoryConfiguration) {\n // Create an NISession\n let session = NISession()\n session.delegate = self\n // Run the NISession with the accessory configuration\n session.run(config)\n}\n\n// Improve Nearby Interaction direction outputs\nfunc updateAccessoryMotionState(_ isMoving: Bool) {\n NIMotionActivityState motionState = isMoving ? .moving : .stationary\n \n // Tell NISession about.the accessory's motion state\n session.updateMotionState(motionState, forObjectWithToken: object.discoveryToken)\n}\n\n// Receive NISession updates\nfunc session(_ session: NISession, didUpdate nearbyObjects: [NINearbyObjects]) { \n guard let object = nearbyObjects.first else { return }\n\n if let distance = object.distance {\n // Do something with distance\n }\n\n if let direction = object.horizontalAngle {\n // Do something with horizontal angle\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Measuring distance between devices using Channel Sounding",
+ "url": "https://developer.apple.com/documentation/CoreBluetooth/measuring-distance-between-devices-using-channel-sounding"
+ },
+ {
+ "title": "AccessorySetupKit",
+ "url": "https://developer.apple.com/documentation/AccessorySetupKit"
+ },
+ {
+ "title": "Nearby Interaction",
+ "url": "https://developer.apple.com/documentation/NearbyInteraction"
+ },
+ {
+ "title": "Core Bluetooth",
+ "url": "https://developer.apple.com/documentation/CoreBluetooth"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/369/4/fea90204-fd38-4da4-b9e7-5dce37bc87d8/downloads/wwdc2026-369_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/369/4/fea90204-fd38-4da4-b9e7-5dce37bc87d8/downloads/wwdc2026-369_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "10203",
+ "year": "2024",
+ "title": "Meet AccessorySetupKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10203"
+ },
+ {
+ "id": "10165",
+ "year": "2021",
+ "title": "Explore Nearby Interaction with third-party accessories",
+ "url": "https://developer.apple.com/videos/play/wwdc2021/10165"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:23.417Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-370.json b/data/wwdc/videos/2026-370.json
new file mode 100644
index 0000000..6d43861
--- /dev/null
+++ b/data/wwdc/videos/2026-370.json
@@ -0,0 +1,115 @@
+{
+ "id": "370",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/370/",
+ "title": "Elevate your app’s text experience with TextKit",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hello, and welcome to \"Elevate your app's text experience with TextKit.\" I'm Tarun Uday, an engineer on the TextKit team.\n\nTextKit is Apple's next generation text engine, and the foundation of text layout and rendering across all of Apple's platforms.\n\nText controls in SwiftUI, UIKit and AppKit all use TextKit to lay out and render their text content. In this video, I want to talk about something we've been hearing from developers for a while, a tension between convenience and control, and the new APIs we've built to resolve it.\n\nIf you're building a text editing experience on Apple platforms, you have two paths. The first path is to use the framework text view. That's NSTextView in AppKit, UITextView in UIKit and TextEditor in SwiftUI.\n\nWith these, you get an incredible amount for free.\n\nText input, selection, accessibility, undo and redo, dictation, inline predictions, and more. These text views use TextKit internally, but that internal implementation is mostly hidden. You have limited ability to customize how the text is drawn or how the viewport manages its visual elements.\n\nThe second path is to use TextKit as the text engine, and render the text in a view or a layer directly. We call this a custom text view to differentiate them from the prepackaged framework text views. You set up an NSTextLayoutManager, implement viewport layout on your own view or layer, and handle all the rendering yourself.\n\nWhen you build custom text views, you get total control over the storage, layout, and the viewport layout process, but you give up everything that the framework text views provide. And building a production-quality text editing experience from scratch is a lot of work. For some scenarios though, choosing between the convenience of a framework text view, and the control of a custom text view has been difficult. Today we'll look at how we can get the best of both worlds.\n\nFor an in-depth introduction to the TextKit architecture and custom text views, watch, \"Meet TextKit 2,\" from WWDC21. And for details on how framework text views adopted TextKit, watch, \"What's new in TextKit and text views,\" from WWDC22. While this talk is self-contained, these two sessions will give you a deeper foundation for everything we cover today.\n\nI'm going to start by giving you a recap on TextKit's architecture. Later on, I'll talk about some new API we've introduced in TextKit.\n\nAt the very end, I'll show you new ways of extending text views, using some examples.\n\nUnderstanding the TextKit architecture is pivotal to making a great custom text experience. Let's start there.\n\nTextKit uses a four-layer architecture for text rendering.\n\nAt the base is the text storage layer. This encapsulates all the text data to be rendered.\n\nThe layout layer sits on top of the text storage. It's responsible for breaking the text into chunks for rendering.\n\nNext, is the viewport layer. This keeps track of which of the chunks from the layout are visible.\n\nAt the top is the view layer. This is where the text appears in your app.\n\nThe storage, layout, and viewport layers are shared across all of Apple's UI Frameworks. You can use these shared layers to render text on any view or view-like drawable visual element provided by a UI Framework.\n\nNext, I'll cover how each layer works. By understanding the pieces that make up each layer, you can customize TextKit to create unique experiences in your apps.\n\nTo do that, I'll use the example of rendering a long NSAttributedString in a custom text view. Text content storage is responsible for breaking this attributed string into paragraphs. For this example, the text content storage creates NSTextParagraph objects for each paragraph of the underlying attributed string. NSTextContentStorage and NSTextParagraph are concrete types that work with NSAttributedStrings.\n\nIf you have a different backing storage type, you can write your own subclasses of the corresponding abstract classes: NSTextContentManager and NSTextElement.\n\nOk, that was the text storage layer. Continuing the example, I'll look at layout next.\n\nAfter the text content storage breaks the attributed string into paragraphs, the NSTextLayoutManager does the work to prepare the paragraphs for rendering. The text layout manager performantly measures the metrics of the glyphs that make up the represented text, and dynamically creates an NSTextLayoutFragment that stores the calculated layout information of the paragraph.\n\nThese objects are immutable. Which means, if a paragraph is edited, the NSTextParagraph and NSTextLayoutFragment are recreated. For example, if I replace the word sandwich with slider a new NSTextParagraph is created for that paragraph, and a corresponding new NSTextLayoutFragment is created with new layout information.\n\nNext I'll show you how the top two layers, the viewport and the view, work together to efficiently render huge amounts of text.\n\nThe text view is a dynamically sized view that can grow as text is laid out and drawn into it, and shrink as text is removed. The viewport is the part of the text view that is visible to the user. TextKit organizes all of its work around the viewport only rendering text that the user can see. This means that one of your core tasks when working with TextKit is enhancing the user's interaction based on the layout information that the viewport provides. To facilitate the rendering of layout fragments onto the text view, TextKit provides a dedicated class: NSTextViewportLayoutController.\n\nThe NSTextViewportLayoutController, I'll just call it the viewport controller.\n\nThe viewport controller coordinates with the text layout manager, and the text view to efficiently layout and render paragraphs of text. Let me show you how.\n\nThe text view knows the scroll position and size of the viewport with respect to the whole document, and provides this to the viewport controller.\n\nThe viewport controller then requests the text layout manager to provide all of the layout fragments that intersect with the viewport, and sends them to the text view for rendering. This coordination, facilitated by the viewport controller, repeats on any change of viewport state. That is, any scroll, edit, or selection event, and is called the viewport layout process.\n\nThe viewport layout process is central to TextKit's performant layout and rendering. And that's it! To build your own custom text view, instantiate an NSTextContentStorage, NSTextLayoutManager, and render the text using an NSTextViewportLayoutController into it's delegate, a view provided by the UI Framework.\n\nEven though I refer to the text view as a view, this could be any drawable visual element that the UI Framework provides. For example, in UIKit, you can choose a UIView or a CALayer to render text into a custom text view. UI Frameworks also package its own type of text view for your convenience with these TextKit layers.\n\nIn UIKit, you can use UITextView to implement an off-the-shelf text editing experience. AppKit and SwiftUI have similar views.\n\nOccasionally, a framework text view might not meet the needs of your app. Perhaps you're building an app that has multiple presentations of the same text.\n\nConnect multiple text layout managers to the same text content storage, and edits in one view will propagate through the shared content storage to the other.\n\nThis means you can present the same document in two different views and they stay in sync automatically.\n\nWith the flexibility that TextKit provides, you can build custom text views with a layering configuration that's right for your scenario. Now, let's take a look at some of the new APIs that we are introducing in TextKit. In the previous section, we talked about rendering text from a layout fragment onto the viewport. That's the viewport layout process. And before the 2027 releases, we did not have a way of referring to the destination views where the text is rendered across TextKit. This meant that while TextKit helped you keep track of layout fragments, it did not help you keep track of the views that they were drawn in.\n\nFirst, meet NSTextViewportRenderingSurface.\n\nThis is a new protocol that represents a visual element inside the viewport that you can draw into. The view that actually renders a layout fragment's text and provides a common abstraction to work with. You can conform your UIView, NSView or CALayer to this protocol, and use it in the viewport controller's delegate methods to keep track of what views are visible in the viewport.\n\nThe rendering surface comes with a companion key protocol NSTextViewportRenderingSurfaceKey. A rendering surface key is any class that can uniquely identify a rendering surface across viewport layout process cycles, like NSTextLayoutFragment.\n\nThis means you can use NSTextLayoutFragment as a key to cache rendering surfaces in map tables or dictionaries.\n\nThe viewport layout process extensively uses the rendering surface key to rendering surface mapping internally.\n\nYou can assign a rendering surface to a key during the viewport layout process by using the renderingSurfaceFor delegate method.\n\nThese are cleared at the beginning of the viewport layout process.\n\nYou can query the rendering surface for a particular key within the didLayout process using the viewport controller's renderingSurfaceFor method.\n\nThese new APIs empower you to use and customize your own rendering surfaces when building custom text views using TextKit.\n\nNow that we've seen how TextKit works in our 2027 releases, let's look at how the text views that power apple's default text experiences work. UIKit's UITextView and AppKit's NSTextView power thousands of long-form text experiences on Apple's platforms, including Messages, TextEdit, Notes, and Journal.\n\nIf you have a SwiftUI app, the most convenient way to implement a long-form text experience is using TextEditor. But you could also include a UITextView or NSTextView in your app by using a ViewRepresentable.\n\nLet me show you.\n\nTo start, I'll create a view called MyTextView.\n\nI will populate MyTextView's body with a ViewRepresentable, that I'll call, TextViewRepresentable.\n\nTextViewRepresentable will conditionally be an NSViewRepresentable on macOS and a UIViewRepresentable otherwise.\n\nInside the NSViewRepresentable, you simply call the initializer for your NSTextView, or your NSTextView subclass in the makeNSView method.\n\nAnd do the same for UITextView inside the UIViewRepresentable. You can see specific examples of this in the accompanying sample app.\n\nIn order to show you how you can extend UITextView using its Textkit hooks, I'll be creating a few different example apps.\n\nIn my first example, I want to build a code editor for my iPad so that I can write some quick code while I'm away from my Mac.\n\nI'll start with a UITextView subclass, initialize it and set it's font to the monospaced system font.\n\nOk, that's a start! But, this isn't really a great code editor experience if I can't see line numbers. Let's start building that.\n\nFirst, let's create a view that can hold a TextView and a lineNumberView. We'll call this ContainerView.\n\nThe ContainerView will hold on to our UITextView subclass, and a UIView to display the line numbers.\n\nI have a basic setup, so what I want now is to recompute and show the NSTextParagraph index for the layout fragments in the viewport, whenever there is a change in the viewport. And in order to do that, I need the text view to be notified whenever its viewport controller has gone through a viewport layout process.\n\nAnd that's possible now! Starting with our 2027 releases, UITextView and NSTextView now conform to NSTextViewportLayoutControllerDelegate.\n\nThis means you can subclass UITextView or NSTextView and override the delegate methods to add your own behavior. I'll do that next! In my TextView subclass, I'll override the delegate methods.\n\nFirst, I'll override the WillLayout method to do some setup work. I'll show the details in a bit. I'll override the configureRenderingSurface method to capture the bounds of the paragraphs to be rendered. Finally, I'll override the DidLayout method to share the accumulated info back to the ContainerView so it can render the line numbers.\n\nBefore showing these methods, I'll add some state to my subclass. I'll start with an array to accumulate the bounds of each paragraph the text view lays out, an integer to track the starting line number, and a closure that I'll use to send the accumulated info up to my ContainerView, so it can render the line numbers.\n\nThe viewport controller delegate methods help the text view know when scrolling or editing has happened, so that we can redraw the line numbers.\n\nI'll implement the methods next, starting with WillLayout. I'll start by calling super. Remember to do that in all of these delegate methods.\n\nI'll clear out the lines variable so that we can get ready to store the bounds of the layout fragments. We also need the starting LineNumber, that's basically a count of all the paragraphs before the viewport starts.\n\nLet me do that in its own function and call it from within the WillLayout method.\n\nI'll start with some simple nil checks and variable naming. I'll use the enumerateTextElements from text location method to enumerate the elements and increment my count until we reach the viewportRange. And that's it! The sample code improves this with caching, so you don't pay this cost on every layout pass. Let's go back to our delegate methods. and see how we can get the bounds for each paragraph.\n\nWe'll do that using the next delegate method, configureRenderingSurfaceFor: textLayoutFragment.\n\nI'll start again by calling super, so that I get the default text view behavior and then append the lines array with the layout fragment's layoutFragmentFrame variable. That's it for the configureRenderingSurfaceFor textLayoutFragment method. This method will be triggered for every paragraph in the viewport.\n\nLet's look at the DidLayout method. At this point, I have the bounds information of every paragraph in the viewport, and I want to pass it to the ContainerView.\n\nBefore firing the closure, I need to convert the fragment frames from text container coordinates to viewport coordinates.\n\nI do that by subtracting the viewport origin. Then, I pass the starting line number and the adjusted frames to the ContainerView. Back in the ContainerView, I set the closure. For each frame, I calculate the actual line number by adding the index to the starting line number, and draw it at the right position in the LineNumber view. And that's it. We set up the variables, collect the bounds for each paragraph in the text view, and pass it to the ContainerView to display it. Let's run the app and see how we did.\n\nPerfect, we added line numbers to a UITextView with just a few lines of code.\n\nI have more work to do but this is a great first step to building a code editor.\n\nUsing the framework text view's viewport layout process is a powerful way to access and display individual paragraph information.\n\nLet me show you one more example. This time involving modifying layout for multiple paragraphs.\n\nHere I've set up a UITextView to show some of my favorite recipes. But I really want to see them one recipe at a time. That is, I want to collapse each multi-paragraph recipe into just its heading.\n\nTo do this, I'll start with the same three viewport delegate methods from the last example. But on top of this, if a paragraph is collapsed, I want to avoid doing layout on it.\n\nTo do that, I'll conform the TextView to NSTextContentStorageDelegate.\n\nThrough this conformance, I'll get access to textContentManager: shouldEnumerate, which will help me mark textElements as collapsed or not. Remember, NSTextContentManager is just the abstract version of NSTextContentStorage, and NSTextElement is the abstract version of NSTextParagraph.\n\nWe want some state to hold on to which sections are collapsed.\n\nWe'll use a set of ints to keep track of the paragraph offset to uniquely identify each paragraph.\n\nAdditionally, we add a method to handle when the user taps on a toggle button.\n\nThese are all the pieces you need! Skip layout using the text content storage delegate method, process every paragraph that does layout in the viewport using the viewport controller delegate methods, and handle the user interaction for when the user taps on a section's disclosure button.\n\nYou can take a look at the sample code for the details. Let's look at what that accomplished.\n\nI can collapse any recipe into just the heading by tapping on the triangle next to it, and I did it right in UITextView. Ok, let's take a step back.\n\nSo far, our examples have been about text, paragraphs, line numbers, and section headings. But text views display much more than just text. Think about Messages with inline photos and stickers. Or Notes, with drawings and document scans.\n\nAll of that non-text content lives inside the text view, managed by TextKit. These are called text attachments. Text attachments follow the same architecture as regular text. Let me focus on one paragraph, and represent an attachment using the paperclip symbol to make things simple. A text attachment is stored in the text storage just like any other character, and is done using a NSTextAttachment object.\n\nWhen the layout manager encounters a text attachment, it asks for an NSTextAttachmentViewProvider, that's the corresponding object in the layout layer. The view provider provides the necessary information to render the attachment onto the text view. This brings us to a challenge. Since these objects are immutable, if we were to edit the text in the paragraph all instances would have to be discarded and recreated. Let me show you a concrete example.\n\nSay I'm building a messaging app with inline animations. Watch carefully as I edit. The animation restarts on every edit for the corresponding paragraph. My view provider is recreated on every edit and that restarts the animation.\n\nTo solve this, we've added a new API on UITextView.\n\nOnce I initialize my text view, I use the register forTextAttachmentViewProviderType method to register a view provider reuse policy for a particular subclass of NSTextAttachmentViewProvider. For the first argument, I add the onEditingInlineParagraphs reuse policy.\n\nThis preserves the view provider across paragraph edits, so keystrokes don't tear down my view provider.\n\nFor the second argument, I provide the view provider subclass type, and the text view will take care of all objects of that particular class. In the sample code, you can see a second type of reuse policy: onScrollingOutOfViewport. This caches the attachment's rendering surface when it scrolls off screen and restores it when it comes back. You can combine both reuse policies depending on your scenario.\n\nNow, on editing, UITextView reuses the view provider, maintaining state, and avoiding any animation glitches.\n\nSo there you go! Three examples of using TextKit in UITextView, line numbers for a text editor, collapsible sections in a recipe app, and inline text attachment reuse in a simple text view. You can download the sample app to look at the details.\n\nTo recap, to create a convenient but powerful rich text editor experience, kickstart your app with UITextView on UIKit and NSTextVIew on AppKit. If you have a SwiftUI app, use a ViewRepresentable to include these text views in your app. For those of you who want much more control over your text rendering, create custom text views using TextKit and use the new Rendering Surface APIs.\n\nCheck out the sample code to see collapsible sections, line numbers, and inline attachment reuse in action. Thanks for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "9:47",
+ "title": "NSTextViewportRenderingSurface conformance",
+ "language": "swift",
+ "code": "class MyView: UIView, NSTextViewportRenderingSurface {}"
+ },
+ {
+ "timestamp": "10:25",
+ "title": "NSTextViewportRenderingSurfaceKey and NSMapTable",
+ "language": "swift",
+ "code": "class MyView: UIView, NSTextViewportRenderingSurface {}\n\nvar cache: NSMapTable"
+ },
+ {
+ "timestamp": "12:39",
+ "title": "UITextView/NSTextView in SwiftUI via ViewRepresentable",
+ "language": "swift",
+ "code": "// Using a TextView in SwiftUI\n\nimport SwiftUI\n\nstruct MyTextView: View {\n var body: some View { TextViewRepresentable() }\n}\n\n#if os(macOS)\nstruct TextViewRepresentable: NSViewRepresentable {\n func makeNSView(context: Context) -> NSTextView { \n NSTextView() \n }\n func updateNSView(_ nsView: NSTextView, context: Context) {\n }\n}\n#else\nstruct TextViewRepresentable: UIViewRepresentable {\n func makeUIView(context: Context) -> UITextView {\n UITextView() \n }\n func updateUIView(_ uiView: UITextView, context: Context) {\n }\n}\n#endif"
+ },
+ {
+ "timestamp": "13:33",
+ "title": "ContainerView with TextView and line number view",
+ "language": "swift",
+ "code": "// Create a text view subclass for a code editor\n\nimport UIKit\n\nclass TextView: UITextView {}\n\nclass ContainerView: UIView {\n let textView = TextView()\n let lineNumberView = UIView()\n \n textView.font = UIFont.monospacedSystemFont\n}"
+ },
+ {
+ "timestamp": "14:42",
+ "title": "Three NSTextViewportLayoutControllerDelegate overrides",
+ "language": "swift",
+ "code": "// Override viewport controller delegate methods\n\nclass TextView: UITextView {\n // Set up\n\t\toverride func textViewportLayoutControllerWillLayout(_ textViewportLayoutController: NSTextViewportLayoutController) {\n \tsuper.textViewportLayoutControllerWillLayout(textViewportLayoutController)\n //...\n }\n\n // Get paragraph bounds\n override func textViewportLayoutController (_ textViewportLayoutController: NSTextViewportLayoutController, configureRenderingSurfaceFor textLayoutFragment: NSTextLayoutFragment) {\n\t\t\tsuper.textViewportLayoutController(textViewportLayoutController, configureRenderingSurfaceFor: textLayoutFragment)\n //...\n }\n\n // Share accumulated info back to ContainerView\n\t\toverride func textViewportLayoutControllerDidLayout (_ textViewportLayoutController: NSTextViewportLayoutController) {\n\t\t super.textViewportLayoutControllerDidLayout(textViewportLayoutController)\n //...\n }\n}"
+ },
+ {
+ "timestamp": "15:59",
+ "title": "startingLineNumber(for:) using enumerateTextElements",
+ "language": "swift",
+ "code": "func startingLineNumber(for viewportRange: NSTextRange?) -> Int {\n guard let viewportRange,\n let storage = textLayoutManager?.textContentManager\n as? NSTextContentStorage else { return 0 }\n let startLocation = storage.documentRange.location\n var count = 1\n storage.enumerateTextElements(from: startLocation) { element in\n guard let range = element.elementRange else { return true }\n if range.location.compare(viewportRange.location)\n != .orderedAscending { return false }\n count += 1\n return true\n }\n return count\n}"
+ },
+ {
+ "timestamp": "17:02",
+ "title": "DidLayout: convert frames to viewport coordinates",
+ "language": "swift",
+ "code": "// Override viewport controller delegate methods\n\nclass TextView: UITextView {\n private var lines: [CGRect] = []\n private var startingLineNumber = 0\n var onDidLayout: ((Int, [CGRect]) -> Void)?\n\n // Share accumulated info back to ContainerView\n\t\toverride func textViewportLayoutControllerDidLayout (_ textViewportLayoutController: NSTextViewportLayoutController) {\n super.textViewportLayoutControllerDidLayout(controller)\n let origin = controller.viewportBounds.origin\n onDidLayout?(startingLineNumber, lines.map {$0.offsetBy(dx: 0, dy: -origin.y) })\n }\n}"
+ },
+ {
+ "timestamp": "17:16",
+ "title": "Draw line numbers in ContainerView closure",
+ "language": "swift",
+ "code": "// Draw line numbers in the ContainerView\n\nclass ContainerView: UIView {\n let textView = TextView()\n let lineNumberView = UIView()\n func setup() {\n textView.onDidLayout = {startingLineNumber, lines in\n let attributes: [NSAttributedString.Key: Any] = [\n .font: UIFont.monospacedSystemFont(ofSize: 11, weight: .regular),\n .foregroundColor: UIColor.secondaryLabel\n ]\n for (i, frame) in lines.enumerated() {\n let number = \"\\(startingLineNumber + i)\" as NSString\n number.draw(at: CGPoint(x: 8, y: frame.minY),\n withAttributes: attributes)\n }\n }\n }\n}"
+ },
+ {
+ "timestamp": "19:22",
+ "title": "Collapsible sections: full TextView class",
+ "language": "swift",
+ "code": "// Add collapsible sections to your text view\n\nclass TextView: UITextView, NSTextContentStorageDelegate {\n var collapsedSections: Set = []\n\n // Set up\n\t\toverride func textViewportLayoutControllerWillLayout(_ textViewportLayoutController: NSTextViewportLayoutController) {\n \tsuper.textViewportLayoutControllerWillLayout(textViewportLayoutController)\n //...\n }\n\n // Get paragraph bounds\n override func textViewportLayoutController (_ textViewportLayoutController: NSTextViewportLayoutController, configureRenderingSurfaceFor textLayoutFragment: NSTextLayoutFragment) {\n\t\t\tsuper.textViewportLayoutController(textViewportLayoutController, configureRenderingSurfaceFor: textLayoutFragment)\n //...\n }\n\n // Share accumulated info back to ContainerView\n\t\toverride func textViewportLayoutControllerDidLayout (_ textViewportLayoutController: NSTextViewportLayoutController) {\n\t\t super.textViewportLayoutControllerDidLayout(textViewportLayoutController)\n //...\n }\n \n // Skip layout for paragraphs marked as collapsed\n func textContentManager(shouldEnumerate textElement: NSTextElement, options: NSTextContentManager.EnumerationOptions) -> Bool {\n //...\n }\n\n // Handle section collapse toggling\n func toggleSection(headerOffset: Int) {\n if collapsedSections.contains(headerOffset) {\n collapsedSections.remove(headerOffset)\n } else {\n collapsedSections.insert(headerOffset)\n }\n guard let textLayoutManager = textLayoutManager else { return }\n\n let textViewportLayoutController = textLayoutManager.textViewportLayoutController\n textViewportLayoutController.delegate?.textViewportLayoutControllerReceivedSetNeedsLayout?(textViewportLayoutController)\n }\n}"
+ },
+ {
+ "timestamp": "22:06",
+ "title": "Text attachment view provider reuse policy",
+ "language": "swift",
+ "code": "// Cache text attachment view providers\n\nimport UIKit\n\nclass ViewController: UIViewController {\n\n var textView: UITextView\n \n func setupTextView() {\n textView = UITextView()\n textView.register(\n [.onEditingInlineParagraphs],\n forTextAttachmentViewProviderType: AnimatedAttachmentViewProvider.self\n )\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Enriching your text in text views",
+ "url": "https://developer.apple.com/documentation/UIKit/enriching-your-text-in-text-views"
+ },
+ {
+ "title": "TextKit",
+ "url": "https://developer.apple.com/documentation/AppKit/textkit"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/370/5/f61dbe38-7302-451a-b3ab-9851d5746315/downloads/wwdc2026-370_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/370/5/f61dbe38-7302-451a-b3ab-9851d5746315/downloads/wwdc2026-370_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "219",
+ "year": "2026",
+ "title": "Enhance the accessibility of your reading app",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/219"
+ },
+ {
+ "id": "10090",
+ "year": "2022",
+ "title": "What's new in TextKit and text views",
+ "url": "https://developer.apple.com/videos/play/wwdc2022/10090"
+ },
+ {
+ "id": "10061",
+ "year": "2021",
+ "title": "Meet TextKit 2",
+ "url": "https://developer.apple.com/videos/play/wwdc2021/10061"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:23.480Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-372.json b/data/wwdc/videos/2026-372.json
new file mode 100644
index 0000000..028f906
--- /dev/null
+++ b/data/wwdc/videos/2026-372.json
@@ -0,0 +1,87 @@
+{
+ "id": "372",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/372/",
+ "title": "Unwrap PaperKit",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Matt, an engineer on the Pencil and Paper team. Apps that give users a canvas to create whatever they want, have been some of the most iconic and empowering experiences on Apple platforms. And PaperKit is what powers the canvas experience across many of Apple's own applications. When you sketch an idea, drop in an image, or mark up a document in Notes, that's PaperKit. It's the full canvas experience, pencil, shapes, text, images, all working together. When you open a PDF in Preview and add a signature, highlight a passage, or circle something important, that's PaperKit too.\n\nAnd when you're ideating in Freeform on macOS, that's also PaperKit.\n\nAnd in iOS, macOS and visionOS 27, PaperKit opens up. Today I'll show you how to unwrap PaperKit, so you can take full control over your canvas experience. I'll start with the data model, which gives you access to everything on the canvas.\n\nThen, I'll show you how to work with elements like shapes and images.\n\nAnd I'll finish with adornments, which let you add interactive overlays and controls.\n\nLet's get going with the data model. I've been building a comic book editor powered by PaperKit. I've already set up some basic templates. But that's as far as I got. Now I need to turn those templates into PaperMarkup. PaperMarkup has a new subelements property. It gives you access to every element on the canvas as a MarkupOrderedSet, which is an ordered collection you can read from and write to. This code snippet creates a shape element for each panel.\n\nAnd then update's the markup.\n\nThat's it. Let's try this on the iPad.\n\nI'll add a three-page panel to my comic. Great. It's showing up exactly how I wanted. The canvas is fully interactive, which is a problem for my comic book editor. I can select the panels, drag them around and even delete them.\n\nThat's not what I want. The template elements should not be editable. To fix this, I need to change how those shape elements behave. Every element on the canvas conforms to the Markup protocol.\n\nThis gives you common properties like frame and rotation. There is also a new allowedInteractions property, which is a MarkupInteractions option set. It gives you fine-grained control over what can be modified on each element. Markup interactions lets you control moving, resizing and rotating, deleting, styling, and selecting, individually or in any combination. And if you want to lock down everything at once, read-only combines them all into a single flag, which is perfect for the comic template.\n\nTo limit interactions with panels in the comic book editor, I need to set .allowedInteractions to .readOnly. I'll give it a try. Now when I tap a panel border, nothing happens. The template shapes are read-only. I can add a speech bubble, move that around, and stylize it, but the panels stay fixed. Perfect. The app is starting to take shape, but the panels need to really pop, so I've added a color picker in the toolbar for styling our template.\n\nTo implement styling, I'm going to dive into elements. Every element in PaperMarkup has a concrete type.\n\nShapes, images, links, loupes, and pencil strokes. They are all part of the same Markup ordered set, and conform to the Markup protocol. But each of these types have their own custom properties. Let's take a deeper look at shapes. PaperKit supports many shape types and each type has its own properties, like corner radius for rounded rectangles, or control points for curved lines.\n\nI used rectangles for the comic panels. They have a stroke color and that's what we're looking for. To apply a color to our panels, I need to iterate over the subelements.\n\nThen set their stroke and fill colors.\n\nTo give it that extra pop, I'm going to use the same color for the markup background. And lastly I update the markup on the paperMarkupViewController. Let me check the result on the iPad.\n\nAnd just like that, the canvas transforms. The page is styled with the color I chose, and it's starting to unwrap into something more personal. PaperKit is built on top of PencilKit, so I can use the Apple Pencil to draw.\n\nEach stroke becomes a markup element and I can use all of the PencilKit model APIs. Those APIs now support character recognition and Bézier path conversion. For all the details, check out \"Reading Between the strokes with PencilKit\".\n\nNow let's look at how to add custom controls with adornments. I want to add a button to each panel that lets users create artwork. But I don't want those controls to become part of the document. They shouldn't be saved, printed, or exported. I want them to exist on top of the canvas, only when I'm editing. That's exactly what Markup adornments are, a visual overlay anchored to canvas coordinates. This makes adornments ideal for buttons, annotations, and collaboration UI. They automatically track zoom and scroll, and they're completely separate from the persisted markup. For each panel, I create a MarkupAdornment. I anchor it to the center of the panel, and I give it an SF Symbol icon through the imageConfiguration.\n\nThen I assign the array to the controller's adornments property.\n\nTo handle taps, I implement the delegate method didTapAdornmentWithID.\n\nWhen the user taps an adornment, I present the ImagePlaygroundViewController.\n\nWhen an image comes back from Image Playground, I create an ImageMarkup.\n\nThen, I insert it into the subelements and update the view controller's markup. Let's give it another try.\n\nI'll tap one of the panels to create some artwork.\n\nMy comic is going to be about a super hero dog fighting crime in the city.\n\nAnd the generated image fills the panel.\n\nTo learn more about generating images in your app, watch \"Create high-quality images using Image Playground\". And now with just a couple more images, some text and fonts, I've got the first page of my comic.\n\nOur super hero dog is going to save the day. Now you can build a fully interactive, canvas-based experience in your app with PaperKit. Use the data model to programmatically read and modify what's on the canvas. And add adornments to create interactive overlays tailored to your app. I can't wait to see how you will unwrap PaperKit. Thanks for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "1:36",
+ "title": "Creating markup subelements",
+ "language": "swift",
+ "code": "import PaperKit\n\nfunc generateMarkup(pageSize: CGSize, panelFrames: [CGRect], configuration: ShapeConfiguration) -> PaperMarkup {\n var markup = PaperMarkup(bounds: CGRect(origin: .zero, size: pageSize))\n var subelements: MarkupOrderedSet = markup.subelements\n for panelFrame: CGRect in panelFrames {\n let shape = ShapeMarkup(frame: panelFrame, configuration: configuration)\n subelements.append(shape)\n }\n markup.subelements = subelements\n return markup\n}"
+ },
+ {
+ "timestamp": "3:03",
+ "title": "Making template elements read-only",
+ "language": "swift",
+ "code": "import PaperKit\n\nfunc generateMarkup(pageSize: CGSize, panelFrames: [CGRect], configuration: ShapeConfiguration) -> PaperMarkup {\n var markup = PaperMarkup(bounds: CGRect(origin: .zero, size: pageSize))\n var subelements: MarkupOrderedSet = markup.subelements\n for panelFrame: CGRect in panelFrames {\n var shape = ShapeMarkup(frame: panelFrame, configuration: configuration)\n shape.allowedInteractions = .readOnly\n subelements.append(shape)\n }\n markup.subelements = subelements\n return markup\n}"
+ },
+ {
+ "timestamp": "4:22",
+ "title": "Apply style to template elements",
+ "language": "swift",
+ "code": "import PaperKit\n\nfunc updatePanelColor(_ selectedColor: CGColor) {\n guard var markup: PaperMarkup = paperMarkupViewController.markup else { return }\n var subelements: MarkupOrderedSet = markup.subelements\n for element in subelements {\n guard var shape = element as? ShapeMarkup else { continue }\n shape.strokeColor = selectedColor\n shape.fillColor = selectedColor.copy(alpha: 0.15)\n subelements.updateOrAppend(shape)\n }\n markup.subelements = subelements\n markup.backgroundColor = selectedColor.copy(alpha: 0.15)\n paperMarkupViewController.markup = markup\n}"
+ },
+ {
+ "timestamp": "5:53",
+ "title": "Add adornments to each panel",
+ "language": "swift",
+ "code": "import PaperKit\n\nfunc addPanelAdornments(for page: Page) {\n var adornments: [MarkupAdornment] = []\n for (panelIndex, panel) in page.panels.enumerated() {\n let adornmentID = UUID()\n adornmentPanelMapping[adornmentID] = panelIndex\n let center = CGPoint(x: panel.midX, y: panel.midY)\n let adornment = MarkupAdornment(\n id: adornmentID,\n anchor: .canvas(location: center),\n imageConfiguration: .systemImage(\"photo.badge.plus\"),\n dragRegion: .fixed,\n scalesWithZoom: false\n )\n adornments.append(adornment)\n }\n paperMarkupViewController.adornments = adornments\n}"
+ },
+ {
+ "timestamp": "6:08",
+ "title": "Handle adornment taps",
+ "language": "swift",
+ "code": "import ImagePlayground\nimport PaperKit\n\nfunc paperMarkupViewController(_ paperMarkupViewController: PaperMarkupViewController, didTapAdornmentWithID id: UUID) {\n guard let panelIndex = adornmentPanelMapping[id] else { return }\n activeImageGenerationPanelIndex = panelIndex\n\n let imagePlaygroundViewController = ImagePlaygroundViewController()\n imagePlaygroundViewController.delegate = self\n present(imagePlaygroundViewController, animated: true)\n}"
+ },
+ {
+ "timestamp": "6:20",
+ "title": "Place the generated image",
+ "language": "swift",
+ "code": "import ImagePlayground\nimport PaperKit\n\nfunc imageViewController(_ imageViewController: ImagePlaygroundViewController, didCreateImageAt imageURL: URL) {\n guard let panelFrame = activeGenerationPanelFrame,\n let paperMarkupViewController = pageViewController.paperViewController,\n var markup = paperMarkupViewController.markup,\n let image = UIImage(contentsOfFile: imageURL.path) else { return }\n\n let imageMarkup = ImageMarkup(frame: panelFrame, image: image)\n markup.subelements.append(imageMarkup)\n paperMarkupViewController.markup = markup\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "PaperKit",
+ "url": "https://developer.apple.com/documentation/PaperKit"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/372/4/012a7de6-cf54-420f-aaf7-02ea568485bf/downloads/wwdc2026-372_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/372/4/012a7de6-cf54-420f-aaf7-02ea568485bf/downloads/wwdc2026-372_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "375",
+ "year": "2026",
+ "title": "Create high-quality images using Image Playground",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/375"
+ },
+ {
+ "id": "203",
+ "year": "2026",
+ "title": "Read between the strokes with PencilKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/203"
+ },
+ {
+ "id": "285",
+ "year": "2025",
+ "title": "Meet PaperKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/285"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:23.530Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-375.json b/data/wwdc/videos/2026-375.json
new file mode 100644
index 0000000..6070cef
--- /dev/null
+++ b/data/wwdc/videos/2026-375.json
@@ -0,0 +1,117 @@
+{
+ "id": "375",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/375/",
+ "title": "Create high-quality images using Image Playground",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Antonio, an Engineer on the Image Playground team.\n\nToday, I'll show you how to bring Image Playground into your app.\n\nPeople have been creating images with Image Playground in Messages, Freeform, and in your apps. They have been combining scenes, experimenting with different looks, and making things feel personal.\n\nWe are reimagining the experience with powerful image models at the core.\n\nAnd now, it makes a real leap, with the ability to make high quality images in virtually any style, even photorealistic ones, that now look genuinely true to life.\n\nThe ImagePlayground framework brings that full experience, the same powerful models, the same styles, the same quality, directly into your app.\n\nThe Image Playground app is available on iOS, iPadOS, macOS, and visionOS. It runs on devices with Apple Intelligence support, bringing the full power of generative models to your app.\n\nJust like the Image Playground app, you can leverage ImagePlayground.framework APIs on the same platforms. Let's walk through how you can add Image Playground to your app.\n\nI'll start with capabilities, a look at what the image creation model can create.\n\nThen I'll show you how to adopt Image Playground, how to present the sheet and seed it with context from your app.\n\nAfter that, options, configuring size, aspect ratio, and style.\n\nAnd finally, availability, making sure your app handles both supported and unsupported devices gracefully.\n\nBefore writing any code, let's look at what Image Playground can enable in your app. Give the model a text description, and it creates a matching image. You can be as specific or as open-ended as you like; \"a birthday celebration with dogs, balloons and confetti\" or just \"celebration.\" The model handles the rest.\n\nThe model can create images with people, including multiple people in a single scene. With personalization enabled, users can bring in someone from their Photos library or simply describe an appearance using text.\n\nThe result is an image that feels personal. You have a range of styles to work with. You can ask for an image without specifying a style.\n\nYou can specify your desired style in text, such as by asking for an oil painting style.\n\nOr use one of our presets.\n\nAnimation adds playful character.\n\nIllustration gives a polished, editorial look.\n\nSketch creates a hand-drawn feel.\n\nGenmoji is a style tuned for expressive, emoji-scale characters you can embed directly in text.\n\nImage Playground supports multiple sizes and aspect ratios. You can request a landscape image for a banner.\n\nA portrait image for full screen on iPhone. Or square for a thumbnail.\n\nThe model picks the closest supported resolution to the size you ask for. All of this runs on Private Cloud Compute, Apple's privacy-preserving cloud infrastructure. Your data is never stored or shared, even with Apple.\n\nFor a deeper look at how it works, check out \"Build with the new Apple Foundation Model on Private Cloud Compute\".\n\nImage Playground has a usage limit because it relies on powerful server models. Increased access is available with most iCloud+ subscription plans. And as the developer, none of this is your problem to solve. There's no server to provision, no infrastructure to maintain.\n\nThe system manages usage limits on behalf of your users, you never need to build any usage-related UI.\n\nYou call the framework. Apple handles the rest. Moving the models to Private Cloud Compute also meant rethinking the API. ImageCreator, the non-UI API for generating images directly in your code, is deprecated.\n\nEverything is now available through a new API with greater image quality, built-in privacy, and a full experience people already know how to use.\n\nIf your app uses ImageCreator today, keep watching. So now let us look at how to add Image Playground to your app.\n\nTo show you how this comes together in code, I've been building Postcards, a greeting card creator.\n\nWith Postcards, you can design custom cards, write a personal message, and choose a layout.\n\nThe last piece is custom artwork for the front of each card. That's where Image Playground comes in. Adopting Image Playground starts with one view modifier. No SDK initialization, no API keys, no server endpoints, just the modifier. I add .imagePlaygroundSheet to my button with a binding to a @State boolean.\n\nWhen the binding flips to true, the sheet appears. Image Playground handles everything, the UI, the model interaction, the style picker.\n\nWhen someone accepts an image, the completion closure receives a URL to the generated file. That URL points to a temporary location inside your app container, save it elsewhere before the session ends.\n\nThe sheet drops into your app as a fully-formed, consistent experience. The user can type a description, browse style options, include people from their library, and preview results before confirming. Your app gets the final URL, ready to display.\n\nThe sheet can open with an empty prompt or can be seeded with context from your app for a richer initial experience.\n\nImagePlaygroundConcept has two factory methods here: text wraps a direct description, I'm passing the card's theme, like cherry blossoms; extracted takes longer text and lets the system pull out the most relevant ideas, I'm passing the card's message, so the model picks up on what the card is about.\n\nWith concepts in place, the sheet opens already primed for this specific card. The user doesn't have to start from scratch. You can also seed the sheet with an image. Pass any SwiftUI Image to the sourceImage parameter, a photo the person picked from their library, or an image the card already has.\n\nImage Playground uses it as visual inspiration alongside the concepts. The user can replace or refine it inside the sheet, it's a starting point, not a constraint. On iPad, Postcards shows a small canvas below the card front. ImagePlaygroundConcept.drawing takes a PKDrawing from PencilKit and adds it as a concept alongside the text. The model treats the strokes as a visual suggestion, they guide the composition without locking it in. To know more about drawings in PencilKit, check out \"Read between the strokes with PencilKit\".\n\nIf you're building a UIKit or AppKit app, ImagePlaygroundViewController gives you the same experience as a view controller. Set concepts and options as properties before presentation, then implement imagePlaygroundViewController, didCreateImageAt on the delegate to receive the result. The API mirrors SwiftUI.\n\nNow, let's configure the sheet to fit Postcards, configuring options like size, style, and personalization.\n\nImagePlaygroundOptions and ImagePlaygroundStyle allow you to manage the configuration of the playground, size and aspect ratio, available and preselected styles, and personalization. Whether you're generating card artwork, a lock screen wallpaper, a banner, or a Genmoji icon, the same API adapts to fit. Postcards supports three card formats: landscape, portrait, and square. Each format stores a CGSize I pass it directly to .closest(to: , and the system maps it to the closest supported aspect ratio and resolution.\n\nBecause format is a property of the card, the size request adapts automatically. A landscape card requests a wide image, a portrait card requests a tall one.\n\nI pass the options to the sheet using .imagePlaygroundOptions.\n\nImagePlaygroundStyle has several values, like: illustration, sketch, animation, and emoji.\n\nimagePlaygroundGenerationStyle takes two arguments: a default style that the picker opens on, and an allowed list that limits which styles appear. If you pass a single style in the allowed list, the picker locks to that style.\n\nIn Postcards, each card carries a StylePreset that maps directly to these values. A classic card defaults to illustration, and allows only illustration and sketch. While an expressive card defaults to animation and also allows illustration and emoji. The style picker automatically reflects whichever card is open.\n\nexternalProvider is an opt-in style that surfaces whatever third-party provider the person has configured in Settings, ChatGPT, for example. To offer it, append it to your .allowedStyles list.\n\nIf the user has a provider configured, the tab appears in the picker.\n\nIf they haven't, the system handles its setup, no check required on your side. You can also pass it as the default style if your app has a context where that makes sense.\n\nEither way, the picker adapts to what's actually available. ImagePlaygroundStyle.emoji is tuned for expressive, emoji-scale characters. When it's active, the sheet fires a separate completion, onAdaptiveImageGlyphCreation, and hands you an NSAdaptiveImageGlyph instead of a URL. An adaptive image glyph is special - it can be embedded directly inline with text, just like an emoji, which is exactly what you want for a card thumbnail that appears next to the recipient's name.\n\nFor everything you can do with adaptive image glyphs in text, rendering, storing, custom text engines, check out \"Bring expression to your app with Genmoji\".\n\nPersonalization is enabled by default. It lets people include someone from their Photos library, a powerful way to make a greeting card feel genuinely personal.\n\nIf your app context doesn't call for it, say, you're building a product image generator, you can set options.personalization to disabled.\n\nThe people picker and name detection disappear from the sheet entirely. Last up: making sure Postcards handles every device gracefully. Image Playground is available on devices that support Apple Intelligence, across a growing set of languages and regions, and when the user has image generation enabled in Settings. The supportsImageGeneration environment value is all you need. It returns true when image generation is fully available, the device has the capability, the current language and region are supported, and the user has it enabled.\n\nWhen it's true, I navigate to CardEditorView, the full Image Playground experience. When it's false, I navigate to CardPickerView, a simple Photos picker fallback.\n\nNo entitlement, no extra capability check, no setup step, just the environment value and a conditional. That's all it takes to support both paths cleanly.\n\nWith Image Playground, it's easy to provide a high-quality image creation experience in your app.\n\nConsider where images live in your design. A card someone sends to a friend.\n\nA profile that represents them.\n\nA message they've been trying to put into words.\n\nThe right shape and feel changes everything.\n\nAnd then think about what only your app knows. What relationships, what memories, what context can you bring into the picture to make the result feel like it was made for that specific person? That's the real opportunity. Image Playground brings the models, you bring the story. Thanks for watching, now go build something that earns a spot on someone's refrigerator.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "5:28",
+ "title": "Adopt Image Playground in SwiftUI",
+ "language": "swift",
+ "code": "// Adopt Image Playground in SwiftUI\n\nfunc imagePlaygroundSheet(\n isPresented: Binding,\n concepts: [ImagePlaygroundConcept] = [],\n sourceImage: Image? = nil,\n onCompletion: @escaping (URL) -> Void,\n onCancellation: (() -> Void)? = nil\n) -> some View"
+ },
+ {
+ "timestamp": "5:39",
+ "title": "Add Image Playground sheet with binding to @State",
+ "language": "swift",
+ "code": "// Adopt Image Playground\n\n@State private var showingPlayground = false\n\nvar body: some View {\n Button(\"Create image\") {\n showingPlayground = true\n }\n .imagePlaygroundSheet(\n isPresented: $showingPlayground,\n onCompletion: { url in\n var updated = currentCard\n store.saveImage(url, for: &updated)\n }\n )\n}"
+ },
+ {
+ "timestamp": "6:29",
+ "title": "Seeding the sheet with context from your card",
+ "language": "swift",
+ "code": "// Seeding the sheet with context from your card\n\nvar concepts: [ImagePlaygroundConcept] {\n [\n .text(card.theme),\n .extracted(from: card.message, title: card.theme),\n ]\n}\n\nvar body: some View {\n Button(\"Create image\") {\n showingPlayground = true\n }\n .imagePlaygroundSheet(\n isPresented: $showingPlayground,\n concepts: concepts,\n onCompletion: { url in\n var updated = card\n store.saveImage(url, for: &updated)\n }\n )\n}"
+ },
+ {
+ "timestamp": "7:11",
+ "title": "Starting from a reference photo",
+ "language": "swift",
+ "code": "// Starting from a reference photo\n\n@State private var sourceImage: Image?\n\nvar body: some View {\n Button(\"Create image\") {\n showingPlayground = true\n }\n .imagePlaygroundSheet(\n isPresented: $showingPlayground,\n concepts: concepts,\n sourceImage: sourceImage,\n onCompletion: { url in\n var updated = card\n store.saveImage(url, for: &updated)\n }\n )\n}"
+ },
+ {
+ "timestamp": "7:42",
+ "title": "Providing a visual suggestion using a drawing",
+ "language": "swift",
+ "code": "// Providing a visual suggestion using a drawing\n\n@State private var drawing = PKDrawing()\n\nvar concepts: [ImagePlaygroundConcept] {\n var result: [ImagePlaygroundConcept] = [\n .text(card.theme),\n .extracted(from: card.message)\n ]\n if !drawing.strokes.isEmpty {\n result.append(.drawing(drawing))\n }\n return result\n}"
+ },
+ {
+ "timestamp": "8:06",
+ "title": "Adopt Image Playground in UIKit or AppKit",
+ "language": "swift",
+ "code": "// Adopt Image Playground in UIKit or AppKit\n\nfunc presentViewController() {\n let viewController = ImagePlaygroundViewController()\n viewController.concepts = [\n .text(card.theme),\n .extracted(from: card.message)\n ]\n viewController.delegate = self\n present(viewController, animated: true)\n}\n\nfunc imagePlaygroundViewController(\n _ viewController: ImagePlaygroundViewController,\n didCreateImageAt url: URL\n) {\n var updated = card\n store.saveImage(url, for: &updated)\n dismiss(animated: true)\n}"
+ },
+ {
+ "timestamp": "9:02",
+ "title": "Size Specification",
+ "language": "swift",
+ "code": "// Size Specification\n\nvar options: ImagePlaygroundOptions {\n var options = ImagePlaygroundOptions()\n options.sizeSpecification = .closest(to: card.format.size)\n return options\n}\n\nvar body: some View {\n Button(\"Create image\") { showingPlayground = true }\n .imagePlaygroundSheet(\n isPresented: $showingPlayground,\n concepts: concepts,\n onCompletion: { url in\n var updated = card\n store.saveImage(url, for: &updated)\n }\n )\n .imagePlaygroundOptions(options)\n}"
+ },
+ {
+ "timestamp": "9:39",
+ "title": "Styles",
+ "language": "swift",
+ "code": "// Styles\n\nvar options: ImagePlaygroundOptions {\n var options = ImagePlaygroundOptions()\n options.sizeSpecification = .closest(to: card.format.size)\n return options\n}\n\nvar body: some View {\n Button(\"Create image\") { showingPlayground = true }\n .imagePlaygroundSheet(\n isPresented: $showingPlayground,\n concepts: concepts,\n onCompletion: { url in\n var updated = card\n store.saveImage(url, for: &updated)\n }\n )\n .imagePlaygroundOptions(options)\n .imagePlaygroundGenerationStyle(\n pendingStylePreset.defaultStyle,\n in: pendingStylePreset.allowedStyles\n )\n}"
+ },
+ {
+ "timestamp": "10:27",
+ "title": "External Provider Style",
+ "language": "swift",
+ "code": "// External Provider Style\n\nvar options: ImagePlaygroundOptions {\n var options = ImagePlaygroundOptions()\n options.sizeSpecification = .closest(to: card.format.size)\n return options\n}\n\nvar body: some View {\n Button(\"Create image\") { showingPlayground = true }\n .imagePlaygroundSheet(\n isPresented: $showingPlayground,\n concepts: concepts,\n onCompletion: { url in\n var updated = card\n store.saveImage(url, for: &updated)\n }\n )\n .imagePlaygroundOptions(options)\n .imagePlaygroundGenerationStyle(\n pendingStylePreset.defaultStyle,\n in: pendingStylePreset.allowedStyles + [.externalProvider]\n )\n}"
+ },
+ {
+ "timestamp": "11:02",
+ "title": "Generating an expressive icon for the card thumbnail",
+ "language": "swift",
+ "code": "// Generating an expressive icon for the card thumbnail\n\n@State private var showingIconPlayground = false\n\nvar body: some View {\n Button(\"Create icon\") {\n showingIconPlayground = true\n }\n Color.clear\n .imagePlaygroundSheet(\n isPresented: $showingIconPlayground,\n concepts: concepts,\n onCompletion: { _ in\n } ,\n onAdaptiveImageGlyphCreation: { glyph in\n var updatedCard = card\n store.saveIcon(glyph, for: &updatedCard)\n }\n )\n .imagePlaygroundGenerationStyle(.emoji, in: [.emoji])\n}"
+ },
+ {
+ "timestamp": "12:01",
+ "title": "Disabling personalization when it doesn't fit your context",
+ "language": "swift",
+ "code": "// Disabling personalization when it doesn't fit your context\n\nvar options: ImagePlaygroundOptions {\n var options = ImagePlaygroundOptions()\n options.sizeSpecification = .closest(to: card.format.size)\n options.personalization = .disabled\n return options\n}"
+ },
+ {
+ "timestamp": "12:32",
+ "title": "Supports image generation",
+ "language": "swift",
+ "code": "// Supports image generation\n\n@Environment(\\.supportsImageGeneration)\nprivate var supportsImageGeneration\n\nvar body: some View {\n NavigationLink(card.recipient) {\n if supportsImageGeneration {\n CardEditorView(card: card)\n }γelse {\n CardPickerView(card: card)\n }\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/375/4/01104285-3253-4b2d-80c3-0d5cdf95c97e/downloads/wwdc2026-375_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/375/4/01104285-3253-4b2d-80c3-0d5cdf95c97e/downloads/wwdc2026-375_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "319",
+ "year": "2026",
+ "title": "Build with the new Apple Foundation Model on Private Cloud Compute",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/319"
+ },
+ {
+ "id": "203",
+ "year": "2026",
+ "title": "Read between the strokes with PencilKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/203"
+ },
+ {
+ "id": "10220",
+ "year": "2024",
+ "title": "Bring expression to your app with Genmoji",
+ "url": "https://developer.apple.com/videos/play/wwdc2024/10220"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:24.131Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-378.json b/data/wwdc/videos/2026-378.json
new file mode 100644
index 0000000..273f5ef
--- /dev/null
+++ b/data/wwdc/videos/2026-378.json
@@ -0,0 +1,101 @@
+{
+ "id": "378",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/378/",
+ "title": "Unlock in-game content with StoreKit and Background Assets",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "App Store, Distribution & Marketing"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hello, I'm Sam, and I'm an engineer on the StoreKit team.\n\nIn this session, I'll dive into new tools to help you create the best games on Apple platforms. First, I'll cover updates to Background Assets. Then, I'll introduce new Unity plug-ins to help deliver a great In-App Purchase experience. Finally, I'll review how to enhance your presence on the App Store and Apple Games app. I'll start with updates to Background Assets.\n\nHere's an app I've been working on called The Coast. It's a jam-packed game with many different levels to play. In addition to the game content, the app includes lots of audio, video, textures, and machine learning models, but these assets are only really needed at specific moments. Rather than downloading all of the assets upfront and taking up storage space, Managed Background Assets saves your players time and storage. The system automatically downloads your asset packs when they're needed to provide a great gaming experience for players. And for apps on the App Store, Apple can host up to 200 GB of assets per app, included in the Developer Program membership.\n\nApple-Hosted Background Assets is available starting with iOS, iPadOS, macOS, tvOS, and visionOS 26. For a deep dive on setup and API integration, check out the session \"Discover Apple-Hosted Background Assets\" from WWDC25.\n\nIn iOS 27, Managed Background Assets becomes even more powerful with localized asset packs. Localized asset packs greatly reduce the size of assets that players need to download by allowing the system to identify the player's preferred language selected in Settings, and only deliver assets for your game in that language. If asset packs for the user-selected language are not provided, the system automatically falls back to the closest language match.\n\nIn The Coast, I set English as the primary language, with French and German also available. Without localized asset packs, the system installs all of these assets to the player's device. After localizing my asset packs, if a player sets their system-wide language preference as German, the system only installs the German asset packs on their device. Localizing asset packs significantly decreases the storage that my game requires.\n\nIf I select English-UK as my system language preference, the system understands that an English-UK asset pack was not provided. As a fallback, the base language asset pack of the preferred user language is used, which in this case defaults to English-US. In another scenario, if I select Spanish as my system language preference, The Coast did not provide Spanish asset packs, and there is no similar regional variant available, so here the system falls back to the primary app language which is English.\n\nTo support localized asset packs in your game, update your asset pack manifest JSON files with a language tag.\n\nIf you're building a game on Steam, you may already manage your game's assets with depots. You can now more easily convert your Steam depots to asset packs to distribute with your game on Apple platforms.\n\nTo use the Steam asset converter on macOS, install Xcode 27, and run xcrun ba-package convert on the command line.\n\nThree arguments are passed in: the ID of the asset pack, the language ID, if applicable, and the desired download policy.\n\nPass your Steam manifest build script as input, and an asset pack manifest with the specified name will be output. The same tool is coming soon to Linux and Windows.\n\nOnce the asset pack manifest is created, you can generate an asset pack archive by running ba-package again using the new manifest file as input, and an asset pack archive will be output.\n\nThe packaged asset pack archive is now ready to be used in your game! If you are developing with Unity, you can also utilize the updates to Background Assets. I'm excited to share two new plug-ins are joining the Apple Unity plug-in portfolio: Background Assets and StoreKit.\n\nApple Unity plug-ins enable your game to tap into the latest features and game services across Apple platforms.\n\nThe Background Assets and StoreKit plug-ins are available now for download, alongside Apple's existing plug-ins on GitHub.\n\nThe repository can be found in the resources of this session, and it provides instructions for building and installing the plug-ins.\n\nOnce you download the new Unity plug-ins, you can build them with the same Python script used to build the other Apple Unity plug-ins.\n\nBoth new plug-ins expose a C#-based Unity API which acts as a bridge to the underlying native framework.\n\nTo build, package, and test the plug-ins, use Xcode 27, Python 3, and Unity 2022 LTS or later. Check out the Meet with Apple session: \"Chart your game's course to Apple platforms\" to learn more about setting up and configuring your project with other Unity plug-ins from Apple. And for best practices when building your game, check out \"Plug-in and play: Add Apple frameworks to your Unity game projects\" from WWDC22.\n\nOnce the plug-ins are installed in your project, open your game in the Unity editor.\n\nBy adopting StoreKit in your project, you can reach players around the world, across all Apple platforms, and offer In-App Purchases through the App Store's safe and trusted commerce platform.\n\nThe C# version of the StoreKit APIs provides access to common flows like fetching and merchandising your In-App Purchase products with the Product API. And, you can initiate a purchase for products in your game using the Purchase API to display the system payment sheet.\n\nAfter purchasing, use the PurchaseResult to check that the purchase was successful and the transaction IsVerified.\n\nThen, deliver the purchased content to your players and call Finish() to complete the transaction.\n\nThroughout your game's lifecycle, listen for new transactions with the Transaction.Updates sequence. A transaction is emitted through this sequence any time the system creates or updates transactions that occur outside your app or on other devices.\n\nHere, I have an OnUpdate handler that is called at app startup inside a listener for transaction updates. For consumables, I first check that the transaction was not revoked and then grant access to the customer. For non-consumables and subscriptions, currentEntitlements is the source of truth for what a customer is entitled to. It already filters refunded, revoked, and expired states for nonconsumables and subscriptions, so we can grant access to customers once we know the transaction is verified. Finally, call Finish() on your verifiedTransaction.\n\nWhen content is purchased by your players, you can use the Background Assets plug-in to ensure an asset pack is locally available and then start serving the content to players.\n\nIf a download is necessary, then you can monitor download progress, and update your game's UI by iterating over the status updates that the DownloadStatusUpdatesAsync method yields.\n\nAfter configuring your game with the new plug-ins, export your project to Xcode, and leverage the powerful testing capabilities included with StoreKit Testing in Xcode and the Background Assets mock server. To set up StoreKit Testing in Xcode, create a StoreKit configuration file and add your test products.\n\nThen, edit your target's scheme, select Run, and choose your StoreKit Configuration file in the drop-down.\n\nHere is where you can also select the folder where your packaged asset packs are stored for the mock server to send to your game.\n\nNow you're ready to build and run! When you run your project in Xcode 27, the Background Assets mock server automatically starts and attaches to your debug session to serve assets in your game. Sandbox testing is also available to help test the user experience with products you set up in App Store Connect.\n\nWhen you're ready to submit your game to the App Store you can add new visuals to your product page header and search results to make your game stand out. The images and videos you add to your App Store search results will also appear in the Apple Games app, providing more ways to visually highlight your game.\n\nCheck out \"Enhance your presence on the App Store\" from WWDC26 to learn more about configuring these assets.\n\nFinally, when your game is published and players purchase new content, they'll use the redesigned system payment sheet in iOS 27. It works great in landscape mode, so people can unlock content, and continue gaming. Before we conclude, let's review the next steps to improve your development workflow and create an even better gaming experience for players.\n\nFurther reduce your app size and efficiently deliver content to your players by uploading localized versions of your asset packs in App Store Connect.\n\nBring the power of the native Managed Background Assets and StoreKit APIs to your Unity game by installing new Apple Unity plug-ins.\n\nWhen you finish updating your game, plan for how you can highlight your features using new image and video assets in the App Store and Apple Games app. Whether you're starting fresh or building on top of years of work, these new features will help you reach millions of players worldwide on Apple platforms. Thank you for joining me, and now, go take your game to the next level!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:06",
+ "title": "Asset pack manifest for a localized asset pack",
+ "language": "swift",
+ "code": "// Asset pack manifest\n\n{\n \"assetPackID\": \"voice-english\",\n \"downloadPolicy\": { /* … */ },\n \"language\": \"en-US\",\n \"sourceRoot\": \".\",\n \"fileSelectors\": [ /* … */ ],\n \"platforms\": [ /* … */ ]\n //… \n}"
+ },
+ {
+ "timestamp": "3:27",
+ "title": "Convert a Steam depot to an asset pack manifest",
+ "language": "swift",
+ "code": "# Convert a Steam depot to an asset pack manifest\nxcrun ba-package convert --asset-pack-id voice-english -l en-US --on-demand voice-english.vdf -o voice-english.json"
+ },
+ {
+ "timestamp": "3:28",
+ "title": "Convert an asset pack manifest to an asset pack archive",
+ "language": "swift",
+ "code": "# Convert an asset pack manifest to an asset pack archive\nxcrun ba-package voice-english.json -o voice-english.aar"
+ },
+ {
+ "timestamp": "5:52",
+ "title": "Fetch and purchase products with the StoreKit plug-in",
+ "language": "swift",
+ "code": "// Fetch and purchase products with the StoreKit plug-in\n\nusing UnityEngine;\nusing Apple.StoreKit;\n\nasync void Start() {\n var products = await Product.FetchProducts(new[] {\n \"com.thecoast.capecod\"\n });\n}"
+ },
+ {
+ "timestamp": "6:01",
+ "title": "Fetch and purchase products with the StoreKit plug-in",
+ "language": "swift",
+ "code": "// Fetch and purchase products with the StoreKit plug-in\n\nusing UnityEngine;\nusing Apple.StoreKit;\n\nasync void Purchase(Product product) {\n var result = await product.Purchase();\n if (result.Result == PurchaseResult.ResultEnum.Success\n && result.TransactionVerification.IsVerified)\n {\n // Unlock access to purchased content\n\n result.TransactionVerification.SafePayload.Finish();\n }\n}"
+ },
+ {
+ "timestamp": "6:23",
+ "title": "Listen for Transaction updates with the StoreKit plug-in",
+ "language": "swift",
+ "code": "// Listen for Transaction updates with the StoreKit plug-in\n\nusing UnityEngine;\nusing Apple.StoreKit;\n\npublic static class TransactionListener {\n public static void Initialize() => Transaction.Updates += OnUpdate;\n\n\n async void OnUpdate(VerificationResult result) {\n if (!result.IsVerified) return;\n var verifiedTransaction = result.SafePayload;\n\n // Consumables are not in CurrentEntitlements, so handle them inline\n if (verifiedTransaction.ProductType == ProductType.ProductTypeEnum.Consumable) {\n if (verifiedTransaction.RevocationDate != null) {\n // Revoke the consumable identified by verifiedTransaction.ProductId\n } else {\n // Grant access to the consumable\n }\n }else {\n // Non-consumables and subscriptions: re-read CurrentEntitlements as source of truth\n await foreach (var verificationResult in Transaction.GetCurrentEntitlements()) {\n if (!verificationResult.IsVerified) continue;\n // Grant access to the product\n }\n }\n verifiedTransaction.Finish();\n }\n}"
+ },
+ {
+ "timestamp": "7:13",
+ "title": "Download asset packs with the Background Assets plug-in",
+ "language": "swift",
+ "code": "// Download asset packs with the Background Assets plug-in\n\nusing Apple.BackgroundAssets;\nusing UnityEngine;\n\nasync void LoadTutorial(string language) {\n try {\n string assetPackId = $\"tutorial-{language}\";\n AssetPackManifest manifest = await AssetPackManager.GetManifestAsync();\n AssetPack assetPack = manifest.GetAssetPack(assetPackId);\n CancellationTokenSource tokenSource = new CancellationTokenSource();\n _ = Task.Run(async () => {\n await foreach (AssetPackManager.DownloadStatusUpdate statusUpdate in AssetPackManager.DownloadStatusUpdatesAsync(assetPackId)) { \n \t\t// Update download progress in UI\n }\n }, tokenSource.Token);\n await AssetPackManager.EnsureLocalAvailabilityOfAssetPackAsync(assetPack);\n tokenSource.Cancel();\n // Start tutorial with the locally available assets\n } catch (Exception exception) {\n // Handle the exception\n }\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Apple Unity Plug-Ins on GitHub",
+ "url": "https://github.com/apple/unityplugins"
+ },
+ {
+ "title": "Background Assets",
+ "url": "https://developer.apple.com/documentation/BackgroundAssets"
+ },
+ {
+ "title": "StoreKit",
+ "url": "https://developer.apple.com/documentation/StoreKit"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/378/6/16c93f95-21e8-4f7f-bb96-2b3c682fa6c7/downloads/wwdc2026-378_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/378/6/16c93f95-21e8-4f7f-bb96-2b3c682fa6c7/downloads/wwdc2026-378_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "205",
+ "year": "2026",
+ "title": "Enhance your presence on the App Store",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/205"
+ },
+ {
+ "id": "325",
+ "year": "2025",
+ "title": "Discover Apple-Hosted Background Assets",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/325"
+ },
+ {
+ "id": "10065",
+ "year": "2022",
+ "title": "Plug-in and play: Add Apple frameworks to your Unity game projects",
+ "url": "https://developer.apple.com/videos/play/wwdc2022/10065"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:24.078Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-379.json b/data/wwdc/videos/2026-379.json
new file mode 100644
index 0000000..b05359e
--- /dev/null
+++ b/data/wwdc/videos/2026-379.json
@@ -0,0 +1,51 @@
+{
+ "id": "379",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/379/",
+ "title": "Meet Trust Insights",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Business & Education",
+ "Privacy & Security"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, I'm Mike Armstrong, Engineering Manager at Apple. In this video, you'll learn how to use Trust Insights to detect and respond to social engineering threats in your app. Social scams are a growing challenge. Attacks that target people not systems.\n\nSocial engineering exploits human psychology rather than technical vulnerabilities. Your users may be pressured, frightened, or deceived into performing legitimate actions.\n\nAnd your app can't tell the difference between a genuine and coerced intent.\n\nAcross the industry and from partner feedback, several recurring coercion patterns stand out. Tech support scams, where fake alerts prompt remote access, deceiving the user into handing over control.\n\nAuthority impersonation, posing as banks, government agencies, or law enforcement to collect sensitive information.\n\nAnd family emergency fraud, with urgent requests for money that exploit emotional bonds increasingly using AI-generated deepfakes.\n\nReal-time coaching makes detection especially difficult. Attackers guide victims through actions via voice calls, chat, or messaging. The user then performs the actions themselves authenticated and legitimately.\n\nExisting protections like multi-factor-authentication and biometrics don't help in this scenario, because the user is the one acting.\n\nA new kind of signal is needed.\n\nAuthentication confirms who, but not whether they're acting freely.\n\nBehavioral context can help distinguish genuine intent from a coerced action.\n\nCritically, this signal must preserve privacy while still protecting users.\n\nTrust Insights, a framework introduced in iOS 27, provides a new approach to help your app understand this behavioral context. I'll take you through how to integrate Trust Insights into your app, cover the requirements for using the API, explain its privacy architecture, and wrap up with an example of how your app could respond to a trust signal.\n\nStarting with generating insights. Trust Insights is a framework that combines device and cloud infrastructure, but your integration is entirely client-side, using a Swift API.\n\nThe first step is configuration. Generating Trust Insights requires an entitlement. Configure this in Xcode by declaring the capability on your app target.\n\nAfter importing Trust Insights, the next step is to create a parameter pack containing the insights to request. A schema is required, but modelVersion is optional. Specifying both a current and prior version of the same insight can support model governance and validation.\n\nThe InsightEvaluator accepts multiple insights. In its context, specify the operationCategory and the evaluations.\n\nThe operation category tells the system what kind of action the user is performing and determines which model logic is applied.\n\nFive categories are available.\n\npayment: any exchange of assets, content, or money, including in-game purchases.\n\naccount: updating account details or security information.\n\nresourceUse: requests to costly or constrained infrastructure, such as AI inference.\n\ncommunication: sending messages, submitting forms, or signing documents. And other: a fallback for operations that don't fit the above. If your use case lands here, please file feedback through Feedback Assistant. Next create your InsightEvaluator, passing in your InsightContext. As your user has full control over the usage of Trust Insights, you should check if your app is authorized. If not, you may want to notify the user.\n\nYou're ready to go. Asynchronously call requestEvaluation. Take note. This can take a couple of seconds to provide a result and requires Internet reachability. It's important to consider where in your user experience or flow you have this code. You may want to take advantage of existing animations or interstitial screens at the right moment.\n\nWhen in development, your requests will hit a sandbox environment. Once distributed on the App Store your requests will be evaluated by production models and servers. To test decision logic and UX variations, you can override insight values and errors by customizing the build schemes for a project in Xcode.\n\nFor more information about which launch arguments are available, check the developer documentation for Trust Insights.\n\nThe response contains one result per InsightEvaluation requested. For IsLikelyBeingCoachedInsight, there are three possible values: unknown: the system has no evidence of scam risk, but this should not be interpreted as low risk.\n\nmedium: some evidence of coaching risk. Depending on the use case, consider introducing friction, additional verification, or adjusting risk scoring.\n\nhigh: significant evidence of coaching risk. The user should be informed of the determined risks before proceeding.\n\nYou should handle both evaluation-level and insight-level errors independently. Details are in the developer documentation. Behind those three values is a sophisticated ML model. Device-sourced data is processed locally. Inputs are immediately discarded after evaluation, with only a single output value leaving a user's device. The final output may incorporate Apple Account signals and velocity checks for additional context.\n\nTwo types of feedback complete the integration: real-time consumption feedback, which reports how your app responded to an insight and offline feedback, for cases where a transaction later turned out to be fraudulent. To submit real-time feedback, call reportConsumption on the evaluation result. This call is mandatory for each insight evaluation request.\n\nIf omitted, your app may be rate-limited.\n\nSix consumption values are available: usedReducedFriction the insight contributed to making the operation easier. usedUnchangedFriction the insight was evaluated but didn't change the experience.\n\nusedIncreasedFriction the insight led to additional checks or friction, though outright blocking based solely on a trust insight is not recommended.\n\nnotUsedNotNeeded the user cancelled, so no decision was required. notUsedError a technical failure prevented use, such as the result arriving too late. usedEvaluationOnly the insight was used for example in internal evaluations and benchmarking, without affecting the user experience.\n\nOffline labels are vital for model improvement. When a trust insight evaluation ultimately results in confirmed fraud, that signal helps the model understand its real-world performance.\n\nThese reports may come days, weeks, or months later.\n\nSubmit them through Apple Business Register using a server-to-server API with a defined schema that includes the insight identifier from the original evaluation.\n\nDon't include any surplus information such as PII in your submission and apply privacy preserving techniques against any remaining values that could be used for fingerprinting.\n\nOffline label submission is not required to benefit from Trust Insights, but it strengthens the ecosystem for everyone. More details are available in the developer documentation. That covers the full integration: configuration, evaluation, results and feedback. Next: privacy. Data minimization is central to how Trust Insights works. The framework processes only what's needed, discards inputs immediately, and keeps all device-sourced data on the device. Privacy is foundational to every Apple product and service. It's considered from the start, throughout development, and continuously as each service operates. Trust Insights analyzes interaction patterns, timing, context, and basic sensor data. Never content within Photos, Messages, or Mail.\n\nNone of these device-derived signals are shared with Apple or third parties.\n\nUsers have full control and can disable Trust Insights within Settings.\n\nA cooldown period may apply after disabling, to protect users who may have themselves been coached into turning it off.\n\nYou should query authorization status to check whether the user has Trust Insights enabled for your app. Trust Insights has been in use within Apple's own services, and there are practical recommendations to share. Here's an example. A user is setting up a large money transfer to someone claiming to be a doctor treating a family member. Behind the scenes, the app has requested a trust insight.\n\nThe .medium result prompts the app to adjust its flow. In this case, displaying a warning and adding a delay to the transaction. Depending on your use case, you may instead handle the result server-side, add a manual review step, or adjust for risk without disrupting the user.\n\nThe right approach depends on your app, your users, and your product.\n\nChoosing when to call Trust Insights is just as important as how. Consider the moments where it adds the most value.\n\nHigh-value financial transactions, such as peer-to-peer payments. Irreversible actions, like account deletion or personal data export. Permission grants, such as remote access or new device authorization. And sensitive data sharing, like credentials or personal documents. Beyond identifying critical moments, there are other best practices to consider: Integrate Trust Insights into your existing risk and decision logic. It should not be the sole factor or determinant in any decision.\n\nUtilize the ability to sample different model versions over time to understand how newer models impact your decisioning logic before taking action. Handle errors at every level, as evaluation errors and insight errors carry different meaning. Never treat unknown or a missing value as low risk.\n\nYour feedback helps to enrich the ecosystem and protect everyone from being the victims of coercion. To avoid rate limits you must submit real-time insight feedback directly in your app and if possible, contribute offline fraud labels via Apple Business Register.\n\nIdentify the moments in your app where Trust Insights can work along your existing logic to protect users.\n\nFrom there, adopt the framework following the best practices and developer documentation.\n\nRegister your business on Apple Business Register to learn about Partner Data Services. You may also be interested in App Attest a framework for verifying that server requests come from legitimate instances of your app.\n\nNow is the best time to submit feedback through Feedback Assistant on any aspect of Trust Insights, including the framework, its capabilities, or any high-volume use cases.\n\nTrust Insights brings behavioral context to your app, helping detect coercion while preserving privacy. Integrate it at the moments that matter most, handle results thoughtfully, and close the feedback loop to strengthen the ecosystem. Thank you for watching.",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "3:01",
+ "title": "Generating insights",
+ "language": "swift",
+ "code": "import TrustInsights\n\nlet request = IsLikelyBeingCoachedInsight.request(schema: .version1, modelVersion: .current)\nlet context = InsightEvaluator.InsightContext(operationCategory: .resourceUse,\n requestedEvaluations: request)\n\nlet evaluator = InsightEvaluator()\nguard try await evaluator.requestAuthorization(for: context) == .authorized else { return }\n\nlet assessment = try await evaluator.requestEvaluation(context: context)\ndo {\n try handleAssessment(assessment)\n} catch {\n // Handle error\n}\n\nassessment.reportConsumption(.usedIncreasedFriction)"
+ },
+ {
+ "timestamp": "5:37",
+ "title": "Handling results for IsLikelyBeingCoachedInsight",
+ "language": "swift",
+ "code": "func handleAssessment(_ assessment: InsightEvaluation) throws {\n\tswitch try assessment.insight.outcome.get() {\n\t\tcase .unknown:\n\t\t\n\t\tcase .medium:\n\n\t\tcase .high:\n\n\t\t@unknown default:\n\n\t}\n}"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Trust Insights",
+ "url": "https://developer.apple.com/documentation/TrustInsights"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/379/4/e12c4703-5c00-44f7-a5f8-80f6e5b7ebd5/downloads/wwdc2026-379_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/379/4/e12c4703-5c00-44f7-a5f8-80f6e5b7ebd5/downloads/wwdc2026-379_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "201",
+ "year": "2026",
+ "title": "Secure your apps with App Attest",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/201"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:23.637Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-388.json b/data/wwdc/videos/2026-388.json
new file mode 100644
index 0000000..9729693
--- /dev/null
+++ b/data/wwdc/videos/2026-388.json
@@ -0,0 +1,73 @@
+{
+ "id": "388",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/388/",
+ "title": "Find and fix performance issues in your Metal games",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Developer Tools",
+ "Graphics & Games"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hello, and welcome. I'm Ruiwei, an engineer with the Metal Tools Team. A smooth, consistent frame rate is one of the most important things you can deliver to your players. It is what makes your game feel responsive and polished - and maintaining that smoothness across an entire play session is key. I'll demonstrate new tools and workflows that give you everything you need to quickly collect and identify issues in long game sessions.\n\nAchieving a great, smooth game experience is a cycle, you need to play test your game on many different devices and form factors, collect performance data, analyze the results, identify performance issues, and fix them. Then you play test again and repeat the cycle until you hit your performance targets. As players play your game for hours, many conditions could change. The device may heat up and thermal state shifts. The player adjusts graphics settings. They move between different levels and fights. To truly achieve a smooth experience, you need to measure across many different scenarios over long periods of time. To show you how, I will start off with a brief introduction to the Metal performance metrics that are available to you. Then, I will show you new tools and workflows that collect and analyze traces over long periods of time. Next, I will demonstrate how to contextualize the traces, so you understand the state of your game at any point in time. And finally, how to collect data in the field after your game ships. I will start with Metal performance metrics.\n\nMetal tools provide a wide range of different metrics. These metrics give you a baseline to compare between different sessions and identify potential issues. For example, timing related metrics such as frames per second, GPU time, and Frame Interval tell you the pacing of your frames and how utilized the GPU is.\n\nOn the other hand, metrics such as layer sizes, composition mode, and MetalFX related metrics help you make sure the display related settings are set up correctly. There’s one place where you can take a quick glance at all of these metrics.\n\nThe Metal Performance HUD displays performance metrics such as FPS, memory usage, and frame interval in an overlay on top of the game's content. It gives you a broad sense of how your game is performing as you play, and it has a number of customization features. For example, the configuration panel allows you to customize the list of metrics to display.\n\nYou can enable or disable each individual metric, or choose from a preset.\n\nYou can learn more about each metric and the HUD in the Metal tools documentation. \"Monitoring your Metal app's graphics performance\" and \"Understanding the Metal Performance HUD metrics\". While the HUD is great for getting a quick read during development, it doesn't give you the ability to save the metrics for long durations. It’s easy to forget exactly where you encountered frame drops, especially when playing for minutes or hours.\n\nTo get that level of detail, you need to collect performance traces over long periods of time. For testing your game at desk, you can use the Game Performance Overview template in Instruments.\n\nTo start, choose the Game Performance Overview template in the Instruments template selector.\n\nIt captures aggregated Metal performance metrics as well as Time Profiler CPU samples.\n\nYou can either start your game using Instruments, or attach to an already running game session on connected devices.\n\nOnce ready, simply click the record button.\n\nThis is ideal for data collection while testing your game at your desk - sessions that last minutes or longer. But here's something really powerful.\n\nWhile your game runs, the system is always recording and saving Metal performance and resource usage metrics.\n\nAggregated and optional per-frame metrics, such as CPU, GPU, FPS, and Memory are efficiently stored, and saved for days. This means for a long game session that lasts hours, and includes multiple state changes, you can look back in time and collect data after the game session wraps up - locally, on both macOS and iOS devices. I'll demonstrate how this works.\n\nOn macOS, you can collect the data through metalperftrace, a new command line tool in macOS 27.\n\nNo extra configuration needed. Simply run metalperftace collect and pass --last for the time duration you want, from hours to days. For example, this command collects a trace containing performance metrics for the last 5 hours.\n\nAlternatively, you can pass an explicit start and end date to collect the trace for a specific time range.\n\nThat's how you can look-back and collect traces on macOS. On iOS, look-back collection requires a one time device setup.\n\nWith developer mode enabled, go to developer settings and Enable Performance Trace. Select Lookback Collection, and choose for how far back the trace will cover.\n\nAs a last step, go to control center, and add the Performance Trace button.\n\nNow the device is ready for trace collection.\n\nYou can start play test the game as usual. After it wraps up, bring up the control center. At the tap of the performance trace button, the device will start collecting and processing the trace for the configured time duration.\n\nThis might take some time, and you will receive a notification once processing is done. The collected traces will appear in the Available Trace Files list. And from there you can easily transfer the trace to your Mac for further analysis.\n\nNow that I have demonstrated how to collect traces - both with Instruments and on iOS and macOS devices, it's time for the next step: analyzing them. On macOS, you can either print or export an overview of the metrics through metalperftrace. Or open the traces in Instruments and explore the data visually. Let's check both in action. To take a look at the overall performance of a recorded trace, simply run metalperftrace overview with the trace file. metalperftrace will aggregate and print the overall performance data of the game sessions contained in the trace. The report is divided into 2 sections for each process. The first section contains the resource usage statistics, such as memory usage, CPU time, and disk activity. The second section contains Metal performance metrics for each layer. For example, During this session, the average memory usage is around 2.1GB.\n\nThe average FPS is about 60.\n\nAnd the report also includes statistical values for metrics such as frame time and on-GPU time. This gives me a quick idea of the overall performance of the session.\n\nIf there were multiple game sessions or Metal graphics apps in the trace, an optional predicate can be used to print data for the exact process you want.\n\nAdditionally, you can also pass --json to make metalperftrace output in JSON format.\n\nNow you get structured output that can be easily fed into scripts for regression testing, or AI agents to automatically triage potential performance issues.\n\nInstruments can also open and visualize the traces for deeper analysis with more detailed metrics.\n\nThe data is plotted on a timeline, and Instruments automatically evaluates the values so that stats that deviate too much are highlighted in a different color.\n\nFor example, in this trace, the FPS dropped for a period of time and is marked in yellow. The detail view shows statistical values: min, max, average, and standard deviation for each metric. These stats are aggregated for the whole trace or the selected time range.\n\nYou can select a time range and Instruments will automatically aggregate and update the stats for the selected duration. Here the average FPS dropped to 26 for the duration, and GPU usage is really low.\n\nThis is where I need to dive deeper and try to reproduce an issue, but there is a big challenge. I know the frame rate drop happened at around the 12-minute mark, but I do not know what my game was doing at that point.\n\nWhen looking at a trace like this there is not enough actionable context to investigate further with just an average FPS.\n\nWas the drop due to a specific area of the level? Or did graphics settings change? This is why context matters when trying to identify potential performance issues and analyze traces. To investigate individual problems, you need granular data that tells you what the game was doing at each moment.\n\nSo next, I will introduce a new API that helps you to add context to the traces. StateReporting is a new API that lets you describe your game's behavior and state over time. And it is based on four core concepts. It all starts with a domain. Domains are finite state machines that represent specific areas of functionality.\n\nFor example, you can define a level domain that tracks the player progress.\n\nEach domain can be in one state at a time.\n\nEach state starts with a label, such as \"Level 1\".\n\nIt can also include optional stable metadata. This is a immutable dictionary that includes any kind of serializable info about the current state, in addition to the label.\n\nEach state can also include volatile metadata. As the name suggests, you use it for values that can change within a state, like the health of the player. As the player progress through the game, you can transition between states with different label and metadata.\n\nFor example, transition from \"Level 1\" to \"Level 2\" when the player advances to the next level.\n\nPutting this on a timeline, you can define multiple domains to describe the state of specific areas of the game.\n\nFor example, a level domain that tracks level changes, a graphics domain that tracks current graphics settings, and a network domain for the network status.\n\nIf we go back to the previous FPS Graph, now we can contextualize the trace and I can immediately notice areas that needs improvement, and focus my optimization efforts.\n\nThe StateReporting API is available for both Swift and Objective-C, and it is easy to adopt.\n\nI'll show you some sample code that reports the current level of the game. First, create a domain, typically a reverse DNS string, and ask for a reporter for the domain. Reporters are instances of the state machine that can be used to report state transitions. To report a state, use reportTransition with a label to describe the current state.\n\nOptionally, to include additional structured info about the state, simply pass a dictionary in the stableMetadata argument. For volatile metadata, you can update values without transitioning to another state by calling reportVolatileMetadataUpdate, with a dictionary representing the info.\n\nStateReporting is also tightly integrated with all of the tools I've discussed: Metal Performance HUD, metalperftrace, and Instruments.\n\nIn the Metal Performance HUD, the list of state domains appears in the metrics configuration tab. Enable them and the overlay will display the label, stable and volatile metadata. This is the most direct way of checking your StateReporting adoption.\n\nFor example, here I'm reporting the level state with a biome and id as the stable metadata, and player position as the volatile metadata.\n\nThe volatile player position is reported once per second, and the overlay allows me to check the position as the player moves around. In metalperftrace, when printing the overview for traces containing state transitions, the report will contain a list of domains, the number of transitions, and the last known state.\n\nTo know the full state transition details, simply pass --include-state-transitions. This will print the full list of states with start and end timestamps.\n\nmetalperftrace can also automatically aggregate and report metrics as a function of these states.\n\nYou can ask metalperftrace to aggregate for all domains and state transitions, or aggregate for all state transitions for a specific domain, or aggregate for a specific state label within a domain.\n\nFor example, to see the average FPS when the graphics is set to high, I can simply ask metalperftrace to aggregate the \"High\" state label for the graphics domain.\n\nThe report will show detailed state info, how long it was active, and a list of overlapping states in other domains.\n\nFor example, here the average FPS is only about 24 when the graphics is set to High.\n\nTo get a more visual look at the state transitions over time, you can open the trace in Instruments.\n\nWhen opening a trace with state transitions, Instruments will create a track for each domain as part of the Points of Interest instrument.\n\nEach track graphs state transitions and volatile updates, making it easy to understand the context.\n\nYou can select individual states, and inspect the details of that state in the sidebar, such as the stable and volatile metadata.\n\nCombined with the Metal performance metrics, I confirmed that the frame rate started dropping as graphics setting changed to high. That's where I'm going to focus my investigation next. Once there is enough context to reproduce the issue, the next step is taking a deeper look at specific points of the game.\n\nYou can capture detailed CPU and GPU scheduling data by using the Metal system trace template in Instruments.\n\nYou can also capture and profile frames by using the Metal debugger in Xcode.\n\nTo learn more about those tools, check \"Metal Developer Tools\" documentation, and \"Discover new Metal profiling tools for M3 and A17 Pro\".\n\nThere are a few best practices to keep in mind when you adopt StateReporting in your game.\n\nFirst, carefully design your domains and states before instrumenting. Each domain should be conceptually orthogonal to the others. Don't try to represent too many dimensions in a single domain - that makes it hard to keep track of. Second, don't have too many state transitions. StateReporting is designed to provide extra context for analyzing performance over long periods of time. It is not designed for high-frequency state changes. Try to limit transitions to the cadence of user actions or slower. The system will throttle if the transition rate is too high, and you will loose important information until rate is back under control.\n\nAnd lastly, confirm the correctness of your states using tools such as the Metal Performance HUD and Instruments.\n\nCheck that transitions make sense and occur when you expect. It is easy to miss edge cases that make the data wrong and hard to understand.\n\nLooking back, you now have the full picture of collecting, analyzing, and contextualizing performance data for long game sessions before shipping the game. But these tools don't stop at launch.\n\nOnce the game lands on player devices, its important to keep monitoring the performance, and catch unexpected performance drops that impact player experience.\n\nNext, I will cover how you can collect key Metal performance metrics after the game is released. MetricKit is a framework that provides two types of data: metrics and diagnostics. It gives your game direct in-process access to power and performance data in the form of reports.\n\nAs players play your game, MetricKit continuously collects data in the background and delivers a daily report to your game.\n\nThe report includes metrics that measures the smoothness of your game, and how hard it's using the device resources. In macOS and iOS 27, MetricKit exposes Metal frame rate information, along with a ton of other interesting performance and power metrics.\n\nIt also provides Metal frame rate as a function of your StateReporting states. The frame rate will be aggregated and grouped by state in your Metric reports.\n\nIn this sample report, MetricKit reports the overall average frame rate along with time and number of frames.\n\nIt also breaks down the frame rate information by the states in the level domain. These metrics and many more will be delivered to your game process where you can do local analysis. And also package up the data for off-device processing and aggregation.\n\nThis allows you to monitor performance of your game in the field, and identify potential issues that you can investigate and fix.\n\nIn addition to metrics, MetricKit also provides diagnostics that help you identify which code path caused a performance problem, such as memory exceptions.\n\nWhen your game is terminated for exceeding its memory limit, you get more insight on what happened.\n\nTo recap, in iOS and macOS 27, Metal performance metrics are always being recorded by the system.\n\nI demonstrated how to look-back and collect performance metrics spanning hours and days. And how to use metalperftrace and Instruments to analyze those traces.\n\nI also showed a new StateReporting API that can help you add context to the traces.\n\nPost shipping, MetricKit delivers metrics and diagnostics from players devices.\n\nTo get started, check out StateReporting. Design the domains that matter most in your game levels, graphics, network states, and start reporting them.\n\nPlay test the game, collect long game session traces and make sure there is a smooth player experience on supported devices.\n\nAlso remember to check out MetricKit, and collect daily metric reports from player devices. To learn more about the available data and how to integrate MetricKit, please check out \"Meet the new MetricKit\".\n\nTry the new tools and workflows. And I cannot wait to play your game on Apple Platforms. Thank you for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "5:00",
+ "title": "Collect a trace with metalperftrace",
+ "language": "swift",
+ "code": "# Collect the last 5 hours\nmetalperftrace collect /tmp --last 5h\n\n# Output\n# Metal performance traces collected to: /tmp\n# /tmp/MetalPerfTrace_20260401_094100_to_144100.atrc\n\n# Or collect an explicit time range\nmetalperftrace collect /tmp \\\n --start 2026-04-01T09:41:00 \\\n --end 2026-04-01T12:41:00"
+ },
+ {
+ "timestamp": "7:02",
+ "title": "Print a trace overview",
+ "language": "swift",
+ "code": "metalperftrace overview /Data/MyGameTrace.atrc\n\n# [Modern Renderer pid:13833]\n# Mem: 2146.1 MiB (2343.9 Peak, 1199.4 Metal)\n# Total CPU Time: 2417.601s (33.17% Sys, 66.83% User)\n# Instructions: 9944836683668 (5.75% P, 94.25% E)\n# Cycles: 5176430469224 (4.45% P, 95.55% E)\n# Disk Reads / Writes: 317.37 / 0.04 MiB (Logical Write 0.04)\n# Layer 0x729293000 (3456x2104) Interval 300.065s Active 300.065s\n# 59.7 FPS 17735 Frames 188 Skipped\n# Frame Time avg: 16.74ms min: 8.33 max: 125.00 stddev: 3.70\n# CPU Begin-to-Present avg: 3.99ms min: 1.40 max: 94.37 stddev: 1.80\n# On-GPU Time avg: 13.39ms min: 5.24 max: 37.57 stddev: 1.43\n# Next Drawable Wait avg: 0.26ms min: 0.00 max: 91.08 stddev: 1.75\n# Shader Compilation Time: 0.000s (Total: 0, Cached: 18)"
+ },
+ {
+ "timestamp": "7:58",
+ "title": "Filter by process and emit JSON",
+ "language": "swift",
+ "code": "// Report state transitions\n#import \n\nNSString *domain = @\"com.mygame.level\";\nSRStateReporter *reporter = [SRStateReporter reporterForDomain:domain];\n\n[reporter reportTransitionToStateLabel:@\"Level 1\"\n stableMetadata:nil\n volatileMetadata:nil];\n\n[reporter reportTransitionToStateLabel:@\"Level 1\"\n stableMetadata:@{ @\"id\": @1001 }\n volatileMetadata:nil];\n\n[reporter reportVolatileMetadataUpdate:@{ @\"health\": @100 }];"
+ },
+ {
+ "timestamp": "13:55",
+ "title": "Include full state transitions in overview",
+ "language": "swift",
+ "code": "metalperftrace overview /Data/MyGameTrace.atrc --include-state-transitions\n\n# [States]\n# com.mygame.graphics\n# High (30.59%, 14.996s) raytracing: 1 shadow: ultra\n# Medium (69.38%, 34.012s) raytracing: 0 shadow: medium\n# com.mygame.level\n# Level 1 (20.47%, 10.033s) biome: forest id: 1001\n# Level 2 (79.53%, 38.991s) biome: volcano id: 1002"
+ },
+ {
+ "timestamp": "14:15",
+ "title": "Aggregate metrics by state",
+ "language": "swift",
+ "code": "# Aggregate across all domains / transitions\nmetalperftrace overview /Data/MyGameTrace.atrc --aggregate\n\n# Aggregate one domain\nmetalperftrace overview /Data/MyGameTrace.atrc --aggregate \\\n --domain com.mygame.graphics\n\n# Aggregate a specific state label within a domain\nmetalperftrace overview /Data/MyGameTrace.atrc --aggregate \\\n --domain com.mygame.graphics \\\n --state-label \"High\""
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Understanding the Metal Performance HUD metrics",
+ "url": "https://developer.apple.com/documentation/Xcode/Understanding-metal-performance-hud-metrics"
+ },
+ {
+ "title": "Monitoring your Metal app’s graphics performance",
+ "url": "https://developer.apple.com/documentation/Xcode/Monitoring-your-Metal-apps-graphics-performance"
+ },
+ {
+ "title": "Getting started with StateReporting",
+ "url": "https://developer.apple.com/documentation/StateReporting/getting-started-with-statereporting"
+ },
+ {
+ "title": "Metal debugger",
+ "url": "https://developer.apple.com/documentation/Xcode/Metal-debugger"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/388/4/682e727f-75f9-441f-81d9-2d6f38bde4b0/downloads/wwdc2026-388_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/388/4/682e727f-75f9-441f-81d9-2d6f38bde4b0/downloads/wwdc2026-388_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:23.980Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-389.json b/data/wwdc/videos/2026-389.json
new file mode 100644
index 0000000..6b233d8
--- /dev/null
+++ b/data/wwdc/videos/2026-389.json
@@ -0,0 +1,80 @@
+{
+ "id": "389",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/389/",
+ "title": "Discover container machines",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Developer Tools",
+ "Swift",
+ "System Services"
+ ],
+ "hasTranscript": true,
+ "hasCode": true,
+ "transcript": {
+ "fullText": "Hi, my name is Michael, and today I want to introduce a new feature built on top of Containerization. Containerization is a swift framework for running Linux containers, with a focus on security, privacy, and performance. Linux containers are a great way to build, test, and deploy server-side applications. Building on this foundation, Container machine, is a new feature that provides a highly integrated Linux environment, that works seamlessly on your Mac. A Container machine is fast and lightweight, like a container, and persistent like a virtual machine. Along with host integrations, a Container machine feels native to macOS. Before learning more, about Container machine, lets take some time to review the Containerization framework. Then, we will look the design principles that shaped Container machine. Finally, we will explore how Container machine provides a seamless workflow for cross-platform development.\n\nAt WWDC 25 we open sourced Containerization. Containerization is a Swift framework for running Linux containers on macOS. It provides APIs for storage, networking, execution, and a Linux init system. It is designed to provide each container with Virtual machine based isolation. These lightweight Virtual machines are performant, and provide sub-second start times. Along with Containerization, the container tool was open sourced. It provides CLI commands for image creation, distribution, and lifecycle management for Linux containers. If you want to learn more about the overall architecture of Containerization and the container tool, watch \"Meet Containerization\" from WWDC 25. Now, let's take a look at the design principles for container machine. These environments should be fast and lightweight, to integrate into existing workflows. Switching between macOS and Linux should be easy. Users should be able to customize and create new environments quickly. Quick creation allows multiple projects to have their own, dedicated environment, without the worry of conflicting dependencies or toolchains.\n\nDifferent tools and dependencies are often required during the development lifecycle. Having a persistent environment, allows additional tools to be added and used over time. Finally, these Linux environments should integrate into your existing work-flow. Developing for multiple platforms shouldn't involve a large context switch. There shouldn't be the need to learn new tools, when targeting a different environment. We kept these design principles in mind when building Container machine. A Container machine must be fast and lightweight. They must be simple to manage. They must provide persistence, allowing users to revisit over time. And a Container machine must feel like an extension of macOS. With these design principles in mind, lets look at how Container machine improves cross platform workflows. Building on top of Containerization, each Container machine runs inside of its own, lightweight virtual machine, and use the same image format as containers. It is a first class feature of the container tool and has a familiar UX. Images built with the container tool, can to be used as the starting point for a new Container machine. A Container machine is stateful and persists modifications made while you are working in it. Start and stop projects as needed, Container machine ensures your environment can continue where you left it.\n\nAnd with automatic user mapping, shared filesystem support, and the ability to enter your Linux environment, no matter where you are in a terminal, Container machine provides a smooth transition, from macOS into Linux, and back again. Now, lets see Container machine in action. Lets start with container machine.\n\nThis shows an overview of the actions we can perform, including ones like create, run, and stop. To create a new Container machine, I'll use container machine create.\n\nI'll provide it a name and set it as the default machine on my Mac. This way, we don't have to provide the name for every command. Container machine uses the same OCI images that containers use. A common container image is alpine.\n\nGreat, our Container machine is created. Next, I want to execute commands inside of this Container machine. I'll use container machine run to execute the echo command.\n\nGreat, let try this with uname.\n\nOn macOS uname prints Darwin. Container machine run uname, prints Linux, reflecting the runtime environment.\n\nContainer machine automatically mirrors your username and current working directory from your Mac. If I run whoami on my Mac, it returns Michael. Running pwd shows that I'm located in my user's home directory on macOS. Lets run an interactive shell inside of the Container machine. Container machine run, without additional arguments, will start an interactive session.\n\nFrom inside my container machine, running whoami and pwd, returns the same username and path from my Mac.\n\nGreat! Automatic user creation, filesystem sharing, and having a consistent working directory results in a seamless experience. Lets explore more, by looking at an application I'm building. I have a Vapor-based web server that I'd like to run and deploy on Linux. For my work-flow, I edit the project using Xcode on my Mac. I use macOS tools, to edit images for the application. I'll build and run this in Linux, then, test my changes on macOS by accessing the web server from Safari. Let's work on this application. In the terminal, running ls shows my project files.\n\nI have a Package.swift, my Source code, and a Public directory holding assets. I have a Container machine with the swift toolchain installed. container machine list will display the name, IP address, and resource information of all my Container machines.\n\nI will copy the IP address for later.\n\nI'm ready to test my application in Linux. I'll start with an interactive shell, inside of my Container machine, by running container machine run.\n\nWith automatic directory sharing, all my project files are available.\n\nMy Container machine has an isolated network. For Safari on my Mac to access the web server, running inside the Container machine, I need to ensure that Vapor listens on the external interface. Let's update the server's configuration in Xcode.\n\nI will set the configuration's hostname to the IP address of my Container machine. We copied this value earlier. I edited this file in Xcode on my Mac, but these changes are already available to my Container machine. Moving back to my terminal, I'm ready to compile and run my application. Great, the server is running so lets view our site in Safari. I will open Safari and paste the IP for my container machine, into the address bar. I will also add port 8080.\n\nGreat, access works and I could stare at this all day! But, let's make one last change. I used Icon Composer, to create the storage icon on screen. I want to change the icon's background to a gradient. I will open my existing icon file, in Icon Composer to make this change.\n\nNow, I will export this icon to my project and overwrite the existing file.\n\nWithout copying files into my Container machine, I expect refreshing the page in Safari, to automatically display my updated icon. Lets go back to Safari.\n\nGreat! my update is working.\n\nContainer machine builds on the usability and speed of containers, with the persistence of a Virtual machine. The seamless integrations provide a Linux environment that feels like an extension of your Mac. We're excited for you to try out Container machine. Download the latest release of the container tool on Github. We look forward to your feedback. Thanks for watching!",
+ "segments": []
+ },
+ "codeExamples": [
+ {
+ "timestamp": "4:41",
+ "title": "Viewing container machine commands",
+ "language": "swift",
+ "code": "container machine"
+ },
+ {
+ "timestamp": "5:00",
+ "title": "Creating a new container machine",
+ "language": "swift",
+ "code": "container machine create --name demo --set-default alpine"
+ },
+ {
+ "timestamp": "5:39",
+ "title": "Echo hi",
+ "language": "swift",
+ "code": "container machine run echo hi"
+ },
+ {
+ "timestamp": "5:57",
+ "title": "Running uname",
+ "language": "swift",
+ "code": "container machine run uname"
+ },
+ {
+ "timestamp": "6:28",
+ "title": "Start interactive shell",
+ "language": "swift",
+ "code": "container machine run"
+ },
+ {
+ "timestamp": "8:01",
+ "title": "List container machines",
+ "language": "swift",
+ "code": "container machine list"
+ }
+ ],
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Container",
+ "url": "https://github.com/apple/container"
+ },
+ {
+ "title": "Containerization",
+ "url": "https://github.com/apple/containerization"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/389/4/8dd035e7-0481-4028-b4bd-e91ba3634198/downloads/wwdc2026-389_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/389/4/8dd035e7-0481-4028-b4bd-e91ba3634198/downloads/wwdc2026-389_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "346",
+ "year": "2025",
+ "title": "Meet Containerization",
+ "url": "https://developer.apple.com/videos/play/wwdc2025/346"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:24.033Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-391.json b/data/wwdc/videos/2026-391.json
new file mode 100644
index 0000000..ebef6ad
--- /dev/null
+++ b/data/wwdc/videos/2026-391.json
@@ -0,0 +1,32 @@
+{
+ "id": "391",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/391/",
+ "title": "Offer subscriptions to groups and organizations",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Store, Distribution & Marketing",
+ "Business & Education"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hi, I'm Andrew, and I work on the commerce team at Apple. In this session, I'm excited to explain how you can offer subscriptions to groups and organizations. Subscriptions have become increasingly important for developers, serving as an excellent way to offer continuous value to customers. However, there are times when a subscriber wants to get their social group, team, or company on your app. For example, a company wants to give their team of editors, video editing software, or members of a run club want to keep each other motivated and accountable through an exercise app. To address those needs, you will be able to sell subscriptions to groups and organizations, with two ways for your customers to purchase and manage them.\n\nWhen you list your subscription on the App Store, you can now offer them to groups and organizations through two new paths.\n\nCustomers can make group purchases through your app, just like they make in-app purchases today… And, through a brand new channel for subscriptions through volume purchasing in Apple Business and Apple School Manager.\n\nWith volume purchasing an organization, like a business or school, can purchase subscriptions through the App Store, inside Apple Business and Apple School Manager. They can use a device management service to assign seats through the same workflows they already use for distributing apps at scale.\n\nThis is a perfect solution for organizations with larger scale and requirements for management and identity.\n\nWith group purchases, Customers purchase subscriptions from your app just like they do today. But instead of only purchasing for themselves, they purchase multiple seats for your plan. Then, all they need to do is share an invite link with anyone they want to give access to your plan. When they accept, they'll automatically get a seat assigned.\n\nThis is perfect for small teams or social groups to collaborate on apps.\n\nIn this session, I will cover what you need to consider to offer subscriptions to groups and organizations. I'll start by covering availability, for subscriptions for groups and organizations. Next, I'll discuss how you can set up pricing.\n\nAfter that, I'll explain how purchasing works. And I'll wrap up with seat management.\n\nThese new options are available for all auto-renewable subscriptions using StoreKit 2.\n\nFor most new and existing subscriptions using StoreKit 2, the ability to sell to groups and organizations is on by default. If your subscription has Family Sharing enabled, you can still sell to groups and organizations, but it is opted-out by default so you can control how the two options work for you.\n\nSubscriptions are available for both group purchases within your app, and for volume purchasing in Apple Business and Apple School Manager. In App Store Connect, you can make changes to this.\n\nYou can choose to make a subscription available only on Apple School Manager, allowing you to create plans with specific pricing for verified educational institutions.\n\nAnd you also have the option to turn off selling to groups and organizations entirely. If you do so, your subscription won't be available for volume purchasing in Apple Business and Apple School Manager, or group purchases within your app, but you will still be able to sell your subscription to individuals on the App Store.\n\nNext, I'll discus how you can set up pricing for these subscriptions. By default, every seat of your subscription is sold at the current price in App Store Connect. If you want to offer bulk discounts, you will be able to use a new pricing configuration, volume pricing.\n\nWith volume pricing, you can offer reduced pricing for larger purchases. You can set up to 5 price bands, with full control over the quantities required for each band, and the price.\n\nNext, I'll share an example of how you could use volume pricing, to offer reduced pricing at purchases of over 20 seats and over 40 seats.\n\nTo accomplish that, you will need to set up 3 bands. The first band, would be set at your standard price, in this example the subscription is $19.99 per month per seat until 20 seats.\n\nFor the next band of seats, between 21 and 40, each seat is discounted to $13.99.\n\nAnd for seat number 41 or greater, it's $10.99 per seat.\n\nIn this example of a purchase of 50 seats, the average cost per seat for the subscriber, comes down about 20% from the base price. Volume pricing gives buyers an incentive, to cover larger groups and consolidate purchasing. It's configured directly in App Store Connect. Next, I will cover how groups and organizations purchase your subscriptions.\n\nWith volume purchasing, Apple Business and Apple School Manager, display your subscriptions and handle the purchase process. All you need to do is make sure your subscription is available to organizations.\n\nFor group purchases, you make your own in-app UI to trigger the StoreKit 2 purchase flow. Consider how you can highlight the value, of a group purchase during your app's merchandising flows, to encourage customers to use a subscription with their social group, or team.\n\nAfter merchandising, you'll need to get the number of seats requested from your customer and pass that into the StoreKit 2 purchase request.\n\nTo wrap up, I'll discuss how seats are assigned and managed after the purchase is complete.\n\nYour customers purchase the number of seats they need, either through volume purchasing or group purchases.\n\nWith volume purchasing, the organization assigns seats to their members, the same way they assign apps today, through a device management service. This makes it easy for them to assign seats at large scale, and ensure they are owned and managed by the organization.\n\nWith group purchases, an invitation link will be generated for the initial purchaser to share with members.\n\nWhen assignments are completed from either purchase type, the App Store assigns a transaction for each member, and you can give them access.\n\nIf you want group purchases without building the infrastructure, by default, group purchases will use the included seat management system, which covers, generating the invitation link, tracking member acceptance and assignment and Seat life-cycle management for your application, like cancellations. All you need to do is start a purchase request and Apple will take it from there. And, if you already implement an invitation and member management system for your app, you can leverage it. Integrating custom invitation flows, will be powered via new App Store Server API endpoints.\n\nIf your app offers collaborative features or access to shared resources for members under the same subscription, you can use the App Store Server API Group management endpoints, to access information about a group. You'll be able to access all the groups that a single customer is in, and all of the members in a group. These endpoints are supported for volume purchasing and group purchases using the included seat management flows.\n\nAs I wrap up, start thinking now about how your application can take advantage of group purchases and volume purchasing to extend the reach of your app.\n\nMake sure you are using StoreKit 2 if you aren't using it already. StoreKit 2 is required to offer subscriptions to groups and organizations. Next, consider how group purchases, volume purchasing, and volume pricing, might impact your availability and pricing strategies, for both new and existing subscriptions. Finally, consider new or existing collaborative experiences you could add or improve in your app for groups or organizations.\n\nI'm so excited to see how offering subscriptions to groups and organizations, enables you to reach more customers. Thank you for being a part of the Apple developer community!",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/391/4/84af4bfe-b42d-4350-91d0-5581899a3e9d/downloads/wwdc2026-391_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/391/4/84af4bfe-b42d-4350-91d0-5581899a3e9d/downloads/wwdc2026-391_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "206",
+ "year": "2026",
+ "title": "What’s new in managing Apple devices",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/206"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:24.169Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-393.json b/data/wwdc/videos/2026-393.json
new file mode 100644
index 0000000..e1a1557
--- /dev/null
+++ b/data/wwdc/videos/2026-393.json
@@ -0,0 +1,56 @@
+{
+ "id": "393",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/393/",
+ "title": "Supercharge your spatial workflows with Reality Composer Pro 3",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Graphics & Games",
+ "Spatial Computing"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hello, my name is Vincent. I'm a Reality Composer Pro engineer. Today I'll show you how visual, node-based tools in Reality Composer Pro can supercharge your spatial workflow. These tools let you author functionality, animation, particle effects, and customize the look of your scene all within the editor, making it fast to prototype and iterate on your ideas. Reality Composer Pro is a visual editor for composing 3D scenes, creating materials, adding visual effects, setting up lighting, building interactivity, and so much more. There's a lot you can do with Reality Composer Pro, and this year it gets even more capable. For a deeper dive into some of these capabilities, check out the session \"Iterate Your Spatial Scenes Faster with Reality Composer Pro 3\". This is the Chaparral Village game that was built entirely using Reality Composer Pro 3 and Swift, and utilizes many of the latest capabilities.\n\nToday, I'll take you through the alchemy area scene from this game and bring it to life with an interactive character and a particle effect for the cauldron.\n\nI would like the alchemist character to perform a routine where it walks to the table, prepares the ingredients, and waits. And when someone taps the alchemist, it would turn and walk to the cauldron to start brewing the potion. To build this, I'll begin with an Animation Graph to blend between idle and walk animations as the character moves.\n\nI'll setup a Behavior Tree to define how the character moves around the alchemy area autonomously.\n\nI'll wire up a Script Graph to add interactivity, so the scene responds when someone taps. After that, I'll show you how the Navigation Mesh component lets characters pathfind around obstacles and navigate through complex environments. I'll show you how I built the particle effect for the cauldron with Compute Graph.\n\nAnd finally, I will share some enhancements to Shader Graph in Reality Composer Pro 3.\n\nLet's start with the Animation Graph. Animation Graph in Reality Composer Pro 3 is a visual, node-based editor for controlling how a character animates at runtime. It supports a wide array of capabilities, from motion warping, to blend spaces, to inverse kinematics, and a lot more.\n\nTo make the alchemist move naturally, I'll use a State Machine, some Animation clips, Transition conditions, and Runtime parameters to blend the alchemist's idle and walk animations. Let's take a look.\n\nThis is the Animation Graph workspace where I can define my animations. The Animation Graph starts with a Final Pose node by default. Whatever pose flows into this node is what the character displays. I'll drag from the Final Pose node's input and connect a State Machine node.\n\nThe State Machine decides which animation is active at any given moment. Next, I'll double click the State Machine node to step inside. This opens the State Machine editor, where states and transitions are defined. The canvas starts empty, and the character needs two states. I'll add two Animation State nodes and name them Idle and Walk.\n\nWith the states in place, I'll add the transitions. The two states are independent right now, with no way to switch between them. I'll drag from Idle to Walk to create a transition.\n\nThen I'll do the same from Walk back to Idle, so the character can return to Idle when it's done walking. The transitions are in place, but they don't have any conditions yet. Without conditions, the State Machine has no way of knowing when to switch between states. Before I can add conditions, I need to define the inputs they'll reference. I'll go to the Inputs Inspector and add a boolean called isWalking.\n\nThis is the parameter that I will set at runtime to tell the character when to walk.\n\nNow I can add the conditions. I'll select the transition from Idle to Walk.\n\nIn the Inspector, I'll click Add in the condition section, choose Bool Condition, and set it to check whether isWalking is true.\n\nFor the Walk to Idle transition, the condition is the same but inverted. I'll select that transition, add a Bool Condition, and set it to check whether isWalking is false.\n\nI'll step back out to the Main Graph by clicking the back arrow.\n\nThe State Machine node now shows two state inputs: one for Idle and one for Walk.\n\nEach input needs an animation source. I'll drag from the Idle input and add an Animation Clip node.\n\nIn the Inspector, I'll set the timeline to the idle animation, and set the node name to \"Idle\".\n\nThen I'll do the same for the Walk input, dragging out to create another Animation Clip node and setting it to the walk animation and rename it to \"Walk\".\n\nLet me test this. I'll press the Play button at the top of the workspace.\n\nThe character in the viewport begins playing the idle animation. In the Inspector, I can manually toggle the isWalking boolean. Setting it to true blends the character into the walk animation.\n\nSetting it back to false, the character finishes its current Walk cycle and then blends back to Idle.\n\nNotice the Graph Editor as I toggle the isWalking boolean. The active state node gets highlighted as the character transitions between states.\n\nThis makes it easy to follow what the State Machine is doing in real time, which becomes especially useful when debugging more complex with many states.\n\nThe character can now blend smoothly between idle and walk animations. But it doesn't know where to go or when to start moving. To give the alchemist a full routine around the alchemy area, I'll use a Behavior Tree to define each step of its movement.\n\nBehavior Trees in Reality Composer Pro let you author autonomous, multi-step behavior directly in the editor. For example, a character that patrols an area, reacts to events, or follows a routine.\n\nAs shown here, Behavior Trees typically consist of a structured hierarchy of nodes. These are steps that can be rearranged in any order, and everything can be tested without writing code. A Behavior Tree describes an entity's behavior as a hierarchy of nodes. The tree is evaluated from top to bottom, and when nodes share the same level, they are evaluated from left to right. Higher nodes and leftmost nodes are always evaluated first. There are two kinds of nodes: Composite nodes and Action nodes. Composite nodes control the flow. Action nodes do the actual work. Action nodes are bound to Composite nodes as children, and the Composite node determines the order and conditions under which they run. Behavior Tree in Reality Composer Pro provides three Composite nodes for controlling the flow.\n\nA Sequence runs its children one by one in order. If any action fails, the entire sequence stops immediately.\n\nA Selector evaluates its children until one succeeds, then stops.\n\nA Parallel runs all of its children at the same time.\n\nBehavior Trees provide a range of built-in action nodes. For composing the alchemist's routine, I'll use the Move To action node to walk the character to a destination, Rotate To Face node to turn towards a target, the Wait node to pause between steps, and the Parameter Setter node to update entity parameters along the way. In my app I am just going to use a few built-in nodes, but there are a number of other action nodes that you will find in the editor.\n\nLet's head back to the editor and build the Behavior Tree that will help the alchemist character through its routine around the alchemy area. This is the Behavior Tree workspace where I will define the alchemist's routine. I want the alchemist character to turn toward the table, walk to it, and wait for a second to prepare the ingredients. These actions need to happen in order, so I'll use the Sequence Composite node. Inside the Sequence, I'll add a Rotate To Face node, a Move To node, and a Wait node. I'll set the wait time to 1 second.\n\nThe Rotate and Move nodes both need a target position and a speed. I'll add inputs for tablePosition, rotationRate, and movementRate, then plug them into the nodes.\n\nBefore the character starts moving, the Animation Graph needs to know about it. I'll add an isWalking input for the Animation Graph. Then I'll add a Parameter Setter before the Rotate To Face, setting isWalking to true.\n\nAnd another Parameter Setter after Move To, setting isWalking back to false.\n\nNow the walk animation plays only while the character is in motion. The alchemist also needs to walk to the cauldron using the same pattern. The steps are identical, just with a different destination. I've gone ahead and built out the full graph with both sub-sequences already wired up. Here's what I built. The cauldron sub-sequence follows the same structure, with its own Rotate To Face, Move To, and Parameter Setter nodes, pointing to a cauldronPosition input. Let me create a Parent Sequence node and connect both of the subsequences to the parent sequence, so the alchemist visits the table first, then moves on to the cauldron. The Behavior Tree defines the character's routine, but it isn't functional yet. In order to make my character perform its routine, where it turns and walks to the cauldron and starts brewing I will need to use Script Graph. Script Graph allows you to define interactivity and functionality for the entities in your scene. It is a visual scripting system that lets you define how entities in the scene behave and interact with each other. It is event-driven, a Script Graph runs in response to events, either at the scene level or on specific entities. Because all of this is visual, prototyping and iteration are fast. Anyone on the team can build and test behaviors directly in the editor, without a build cycle. To see how the Script Graph can power your workflow, checkout the session \"Design no-code games with Reality Composer Pro 3\". Let's jump back to the editor to setup the table and cauldron position, and add an interaction with the tap gesture using Script Graph. This is Script Graph. Here I will define some logic to interact with the alchemist character. I'll start with an On Initialize node.\n\nThis sends an event the first time the Scripting component is initialized, making it a good place for setup logic.\n\nThe Behavior Tree needs the table and cauldron positions, and I'll use a subgraph to provide them. A subgraph is a reusable piece of logic that works like a function.\n\nMy team member has already built a Setup Behavior Tree Position subgraph in the Project Browser. It finds the table and cauldron entities in the scene and sets their world positions on the entity parameter. I'll drag the subgraph from the Project Browser onto the canvas, then connect its event input to the On Initialize node's event output.\n\nAlright… let's give it a go! I'll click the play button on top. The alchemist turns to the table, prepares the ingredients for a while then heads straight to the cauldron. The entire routine plays out from start to finish just as we defined in the Behavior Tree. But, I am thinking it would be a lot more engaging if the alchemist responded to a tap interaction.\n\nLet's make it so that the alchemist stays at the table preparing the ingredients... until someone taps it to indicate that it's time to brew the potion. To set this up, let's jump back to the Behavior Tree. First, I'll delete the Wait node in the table sub-sequence. The alchemist should wait indefinitely now until told to move on. In the Inputs Inspector, I'll add a Boolean input called readyToBrew with a default value of false. Next, I'll select the cauldron sub-sequence. In the Inspector, I'll add a Precondition, and choose Bool Condition.\n\nWith that set up the alchemist is configured to stay at the table until readyToBrew becomes true. After which it moves on to the cauldron. Now I'll go back to the Script Graph to wire up the tap interaction to make our new behavior fully functional. I'll use the Mac Virtual Display along with the Live Preview feature in Reality Composer Pro 3 that will allow me to work on my scene immersively on Apple Vision Pro. Let me show you what that looks like. I'll add an On Tap node. This node listens for tap gesture events on the entity. I'll also need a Set Entity Parameter node. This node will help me set the pre-condition parameter that I previously defined in my Behavior Tree. In the name field, I'll define the parameter name as \"readyToBrew\" which will match with the name I defined in my Behavior Tree. This parameter will indicate to my character when it should move to the cauldron. I will enable the toggle to set it to true. Finally, I'll connect the the On Tap node's event output to the Set Entity Parameter node.\n\nI've started a live preview session on Apple Vision Pro using the Reality Composer Pro Companion App.\n\nThe alchemist turns to the table, and stays there preparing the ingredients. When I tap the character, it turns and walks to the cauldron to begin brewing. The scene is now interactive! The alchemist now animates, follows a routine, and responds to interaction, all authored visually in the editor with a combination of Animation Graph, Behavior Tree, and Script Graph. Next, I want to make my character move on an autonomous path and even avoid obstacles in the scene. For that, I will use the new Navigation Mesh feature A Navigation Mesh defines the walkable surfaces in a scene. The navigation controller uses the Navigation Mesh to route characters from one point to another, automatically avoiding obstacles like the trees and water shown here.\n\nAnd when meshes aren't naturally connected, like this bridge on the right, an off-mesh connection can be authored to link them.\n\nLet me show you how the village scene in the Chaparral Village project uses Navigation Mesh to guide the character around the village. Here's the Navigation Mesh component that I set up for the village scene. Starting with the Shapes section. This is where the bounding box is defined for mesh generation. The bounding box determines which parts of the mesh geometry in the scene should be included when computing the Navigation Mesh. Any parts of the scene within the bounding box will be used for generating a Navigation Mesh resource. Next is the Off-Mesh Connections section that can help define links between areas that the mesh wouldn't otherwise connect, like the ladder here that lets the character climb up to the building's roof. Each connection has a start and end point. You can adjust the positions directly using the gizmo in the viewport. This makes it quick to set up and reposition connections as the scene evolves. Next is the Generation Parameters section. These control how the scene geometry is sampled. For example, cell size sets the voxel size used during sampling. Smaller values capture finer detail, while larger values produce a more approximate mesh.\n\nThere are many more parameters available for fine-tuning the Navigation Mesh. For a complete reference, check out the documentation on developer.apple.com. To learn how to use the Navigation Mesh in code, check out Dennis's session \"Explore Advances in RealityKit\".\n\nOnce the Navigation Mesh is set up, you can use it with a Behavior Tree, Animation Graph, or your own custom Swift system through the navigation component to guide your character through the scene. Just like the character in the village here, navigating to the tap location autonomously while avoiding obstacles along the way. And with an off-mesh connection on the ladder, the character even knows it can climb up to the rooftop. Next, let me show you how I built the smoke particle effects for the cauldron using Compute Graph. The Compute Graph is a visual, node-based tool for building GPU-driven particle simulations backed by Metal directly in Reality Composer Pro. It gives you full control over spawning, simulation, and rendering, with support for custom shaders. A Compute Graph is organized into four phases, each handling a different stage of a particle's life. The first phase is the Emitter Phase, which controls how and when particles are born, whether continuously, in bursts, or as a single shot. The second phase is the Initialize Phase, and it runs once per particle at birth, setting starting values like velocity, lifetime, and size. Next is the Simulate Phase. It runs every frame, applying forces like gravity and turbulence to evolve particles over time.\n\nAnd the final phase is the Output Phase, which controls how particles look on the screen, defining their visual appearance as they age and move through space. Let me walk you through the Compute Graph I built for the cauldron's smoke effect. Every Compute Graph starts with these four phases as a template. For the Emitter Phase, I'm using a Continuous Emit node. This gives a dense, rolling flow of particles while keeping each frame's spawn count in check. In the Initialize Phase, I first set the size so each particle starts at the right scale, with some random variation. Then I randomize the lifetime, so particles don't all disappear at the same instant, giving the smoke a more natural feel. Next, a Spawn in Sphere node distributes particles within a spherical volume. This node comes from a custom Compute Graph bundle, please check out the link for how you can write your custom Compute Graph node using a bundle. And finally, a Set Position node clamps the Y position to zero, flattening the spawn area to the surface of the sphere. This way, particles spawn in a circle, like smoke rising from the surface of the liquid. In the Simulate Phase, I'm applying a negative gravity force. This makes the smoke particles drift upward, just like rising steam from the cauldron.\n\nLastly, In the Output Phase, particles fade in and out over lifetime, scale down as they rise, and shift color based on height. For rendering, Compute Graph uses a Shader Graph material. Here, I'm using one that draws a circle on a billboard for a rounder look.\n\nAnd that's it! The particle effect looks beautiful in my scene. Thanks to the Compute Graph feature in Reality Composer Pro! Next, I would like to share some enhancements to Shader Graph. Shader Graph is getting some powerful updates this year, like the Subsurface Scattering effect that you may have seen in the Jupiter environment that improves the realism of the ice on the Moon's surface. In addition to Subsurface Scattering effect, there are many other enhancements to Shader Graph this year.\n\nRealityKit PBR Surface 2 is now available in Shader Graph. It expands the original RealityKit PBR surface node with new surface properties like sheen, Subsurface Scattering, plus more accurate Diffuse and Occlusion Shading.\n\nHair surface is a dedicated surface shader for rendering hair and fur. It models how light reflects along and scatters through fine strands for realistic results. Portal surface and Portal Geometry Modifier are also available in Shader Graph. You can now modify the per-pixel opacity of a Portal surface and drive vertex-animated portal geometry, giving you much more flexibility over how portals look and behave. If you are new to Shader Graph, I recommend you go over Niel's session, \"Explore materials in Reality Composer Pro.\" Those were some exciting new features I covered in my session. I showed you how to bring a scene to life using visual, node-based tools in Reality Composer Pro 3 to supercharge your workflow with building character animations, defining complex behaviors, to even adding interactivity and particle effects to your scene. And this is just scratching the surface of what you can do with Reality Composer Pro 3. Before I wrap up, I highly encourage you to download Reality Composer Pro 3 from developer.apple.com and start building your own interactive experience. For a deeper dive, there are also some great related sessions to check out. \"Design No-Code Games with Reality Composer Pro 3\" goes deeper into building games with the Script Graph.\n\nAnd \"Explore Advances in RealityKit\" covers the RealityKit features that power many of the tools shown today. Thanks for watching, and I can't wait to see what you build with Reality Composer Pro!",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/393/4/e68b947f-f7f3-49b5-b959-7a70fd9899c3/downloads/wwdc2026-393_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/393/4/e68b947f-f7f3-49b5-b959-7a70fd9899c3/downloads/wwdc2026-393_sd.mp4?dl=1"
+ },
+ "relatedVideos": [
+ {
+ "id": "252",
+ "year": "2026",
+ "title": "Design no-code games with Reality Composer Pro 3",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/252"
+ },
+ {
+ "id": "279",
+ "year": "2026",
+ "title": "Explore advances in RealityKit",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/279"
+ },
+ {
+ "id": "281",
+ "year": "2026",
+ "title": "Extend Reality Composer Pro 3 functionality with Xcode",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/281"
+ },
+ {
+ "id": "280",
+ "year": "2026",
+ "title": "Iterate your spatial scenes faster with Reality Composer Pro 3",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/280"
+ },
+ {
+ "id": "10202",
+ "year": "2023",
+ "title": "Explore materials in Reality Composer Pro",
+ "url": "https://developer.apple.com/videos/play/wwdc2023/10202"
+ }
+ ],
+ "extractedAt": "2026-06-12T10:24:24.459Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-394.json b/data/wwdc/videos/2026-394.json
new file mode 100644
index 0000000..45a22ca
--- /dev/null
+++ b/data/wwdc/videos/2026-394.json
@@ -0,0 +1,28 @@
+{
+ "id": "394",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/394/",
+ "title": "Get ready for WWDC26",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Essentials"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Hello developers. WWDC26 is almost here. It’s a full week of technology, creativity, and community, all online and free.\n\nHere’s how you can get ready right now. Download the Apple Developer app. It’s a great place to experience the entire conference. Plus, it’s got great new stickers, including our favorite dogcow Clarus. Make sure you have a free Apple Developer account to get personalized recommendations for activities and more than 100 sessions on developer.apple.com.\n\nSet reminders. Monday, June 8th, Keynote at 10 a.m. Pacific, and Platforms State of the Union follows at one. Next, sign up for Group Labs to check out live streaming Q&A’s hosted by the Apple engineers and designers who build the technologies you use every day.\n\nGet answers on the Apple Developer Forums now and during WWDC.\n\nFind your community. WWDC is for all developers and activities are happening all week online and around the world. And say hello to this year’s WWDC playlist made by our friends at Apple Music.\n\nCheck out the 2026 Apple Design Award finalists.\n\nThe best apps and games in the world.\n\nGet the WWDC26 wallpaper. New wallpaper, new you. And practice using the Alien sticker in all of your threads.\n\nWWDC26 takes place June 8th through the 12th.\n\nSee you there.",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [
+ {
+ "title": "Sign up for Group Labs",
+ "url": "https://developer.apple.com/wwdc26/schedule/group-labs"
+ }
+ ],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/394/3/957ab100-9008-44f0-804b-37ad25ee524c/downloads/wwdc2026-394_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/394/3/957ab100-9008-44f0-804b-37ad25ee524c/downloads/wwdc2026-394_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:24.503Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-397.json b/data/wwdc/videos/2026-397.json
new file mode 100644
index 0000000..2e5d4ae
--- /dev/null
+++ b/data/wwdc/videos/2026-397.json
@@ -0,0 +1,23 @@
+{
+ "id": "397",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/397/",
+ "title": "Dub Dub Daily: Day 2",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Essentials"
+ ],
+ "hasTranscript": true,
+ "hasCode": false,
+ "transcript": {
+ "fullText": "Welcome to Dub Dub Daily. It's day two of WWDC26 and we are just getting started. I'm Lauren Grimm from Worldwide Developer Relations, coming to you from Apple Park. And every day this week, I'll be your guide to what's happening at the conference. Yesterday was a lot. Keynote, platform state of the union, and if you already watched both, you're a hero. And if you're still catching up, we've got you covered. Head to the Apple Developer website and app to watch both on demand. We also dropped a SOTU recap video if you're short on time, plus an article with all of the big takeaways. Now, I have to tell you who's coming up next because I have been so excited about this since we filmed it. You might recognize Josh Schaffer from yesterday. He MC'd the platform State of the Union, and my colleague Jeff talked to him earlier for some additional insights on what we announced this year. Here's Jeff and Josh.\n\nJosh Shaffer, Senior Director of Platform Technologies, thank you for being here. You were the host of SOTU this year. We can see you right now on the Apple Developer app and website, YouTube and BiliBili. This seems like a question for you. Can you give us the headline for SOTU this year? Oh yeah. Well, I mean, SOTU itself is sort of the headline for the whole show, but if it had its own headline, I guess it would be the three themes of the year. It would be, you know, rebuilt intelligence, refine platforms, and transform tools. What do we mean by transformed tools? Oh, well, you know, intelligence is just transforming the way that we're doing development, and the tools themselves have this year really undergone a huge transformation towards agentic coding. That's just making it so much easier to build stuff.\n\nLots to dig into there in SOTU and in the sessions that are... A ton to go into across everything for sure. That's fantastic.\n\nThere's a really lovely line in SOTU, I like this line a lot. It's about the intelligent platform and I'm paraphrasing, but the line is about how rich native experiences and intelligent natural language interfaces come together. Can you tell me a little bit more about what that means? Yeah, intelligence is changing so many things, but in this particular case, there's the intelligence coming into apps that's making apps themselves more powerful, and adding capabilities within them that weren't previously even possible. And then there's intelligence at the system level, like with Siri, where it's making, you know, system-wide interactions more powerful. And these things are really complementary. The apps are just as important as they have always been, now made even more powerful by intelligence, and accessible in new ways through the system level intelligence. It's just a really great combination at both layers.\n\nThere's a lot this year. What are some of the things, this is not an entirely fair question, what are some of the things that you're most excited for people to check out? Boy, I mean there's just so much, but obviously when you're gonna get started, you'll download Xcode and install it, and try out all the new capabilities with agents, and new ways to code and develop. It's just a pretty incredible leap forward. Lots of headlines, lots going on this year, right out front. What's an example of something that you and your team are especially proud of that might not be as apparent, might not be as outwardly obvious to people? You know, I think building with frontier models seems like it should be really easy.\n\nBut there's a lot of barrier to getting started. I mean you have to figure out a business model where you can pay for all these token usage and figure out how to connect to somebody's server and all this stuff. There's a lot of, especially for somebody just getting started, things to figure out. So what I'm really excited about with the Foundation Models framework this year, is the ability to now just access one of these frontier models in Private Cloud Compute with basically no setup or friction at all. I think it's gonna democratize access to this in a a really incredible way and we're gonna start seeing apps that will get developed that might not have otherwise even happened because of the the barriers to entry.\n\nThis is going to be a very big week. Once again, we got more than a hundred sessions this year, covering all sorts of everything. Can you tell me a little bit about what people can expect when they go to dive into that list this year? Yeah, I mean the great thing about the session list is that it covers such a broad range. Like there's a ton of things that are just really broad interest that everyone's gonna really enjoy seeing. Down to really niche things that are super interesting, but maybe, you know, relevant to a smaller subset of people. And it goes really deep in all these areas. It's really exciting. What do you and your team most look forward to? It's kind of a mature question. When WWDC is approaching, what do you most look forward to? Oh man, I mean the exciting thing about Dub Dub coming up is just the anticipation of getting to put this out in the world and have everybody see what we've been working on for so long, and see the reactions, and hopefully great positive reactions, but also get the feedback of areas where there's, you know, an opportunity to do better, and listen to how it's being received, and figure out what we do next. Do you get feedback all week long? Oh, we get feedback all the time. But certainly all week long for sure! And the exciting part about Dub Dub week is getting to talk to some people in person about it too, and interact with folks online and the more direct engagement is really exciting. I love it.\n\nAlright, let's talk about today because the calendar is packed. We have Group Labs running all day! Group Labs are live online presentations and Q&A sessions hosted by the Apple engineers and designers who build the technologies you use every day. So drop in, bring your questions, and anyone can watch live or on demand.\n\nA few more things before you go.\n\nTry out the AI-powered answers on developer.apple.com. Go through the search bar to get more information and insights from WWDC.\n\nOur friends at Apple Music put together brand new playlists for the entire week.\n\nSearch WWDC26 in Apple Music.\n\nMore than 100 sessions dropped yesterday on everything from Core AI to Xcode 27 to design foundations.\n\nEvery one of them is streaming right now.\n\nI've already got 17 tabs open with the sessions I'll watch after this.\n\nThat's day two. See you tomorrow for day three of WWDC.",
+ "segments": []
+ },
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/397/2/84ab23e3-fe1e-4fa1-9896-67424f14dda6/downloads/wwdc2026-397_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/397/2/84ab23e3-fe1e-4fa1-9896-67424f14dda6/downloads/wwdc2026-397_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:24.540Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-398.json b/data/wwdc/videos/2026-398.json
new file mode 100644
index 0000000..1630fdb
--- /dev/null
+++ b/data/wwdc/videos/2026-398.json
@@ -0,0 +1,19 @@
+{
+ "id": "398",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/398/",
+ "title": "Dub Dub Daily: Day 3",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Essentials"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/398/2/410c7536-3689-45c3-a343-661e3cdd641f/downloads/wwdc2026-398_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/398/2/410c7536-3689-45c3-a343-661e3cdd641f/downloads/wwdc2026-398_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:24.723Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-399.json b/data/wwdc/videos/2026-399.json
new file mode 100644
index 0000000..a15c202
--- /dev/null
+++ b/data/wwdc/videos/2026-399.json
@@ -0,0 +1,19 @@
+{
+ "id": "399",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/399/",
+ "title": "Dub Dub Daily: Day 4",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Essentials"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/399/2/3b82f5d9-fdd9-47cd-862b-8d6c6d9ffa02/downloads/wwdc2026-399_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/399/2/3b82f5d9-fdd9-47cd-862b-8d6c6d9ffa02/downloads/wwdc2026-399_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:24.621Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8001.json b/data/wwdc/videos/2026-8001.json
new file mode 100644
index 0000000..006ffe9
--- /dev/null
+++ b/data/wwdc/videos/2026-8001.json
@@ -0,0 +1,19 @@
+{
+ "id": "8001",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8001/",
+ "title": "Swift Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Swift"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/8001/1/6ee7f28d-d198-4690-af7e-ac35f4173c3c/downloads/wwdc2026-8001_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/8001/1/6ee7f28d-d198-4690-af7e-ac35f4173c3c/downloads/wwdc2026-8001_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:24.924Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8002.json b/data/wwdc/videos/2026-8002.json
new file mode 100644
index 0000000..b7a1edf
--- /dev/null
+++ b/data/wwdc/videos/2026-8002.json
@@ -0,0 +1,19 @@
+{
+ "id": "8002",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8002/",
+ "title": "SwiftUI for Beginners Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/8002/1/82335df8-72e8-43d1-9e9d-935affc5977b/downloads/wwdc2026-8002_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/8002/1/82335df8-72e8-43d1-9e9d-935affc5977b/downloads/wwdc2026-8002_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:24.951Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8003.json b/data/wwdc/videos/2026-8003.json
new file mode 100644
index 0000000..87aa858
--- /dev/null
+++ b/data/wwdc/videos/2026-8003.json
@@ -0,0 +1,19 @@
+{
+ "id": "8003",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8003/",
+ "title": "Power and Performance Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "System Services"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/8003/1/3e92bdbb-e289-4fca-a1b7-50dd13e870a1/downloads/wwdc2026-8003_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/8003/1/3e92bdbb-e289-4fca-a1b7-50dd13e870a1/downloads/wwdc2026-8003_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:24.984Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8004.json b/data/wwdc/videos/2026-8004.json
new file mode 100644
index 0000000..0886be4
--- /dev/null
+++ b/data/wwdc/videos/2026-8004.json
@@ -0,0 +1,19 @@
+{
+ "id": "8004",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8004/",
+ "title": "visionOS Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Spatial Computing"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/8004/1/52426afe-f1d2-41a2-9862-e0a34d23e575/downloads/wwdc2026-8004_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/8004/1/52426afe-f1d2-41a2-9862-e0a34d23e575/downloads/wwdc2026-8004_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:25.067Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8005.json b/data/wwdc/videos/2026-8005.json
new file mode 100644
index 0000000..d8dc78b
--- /dev/null
+++ b/data/wwdc/videos/2026-8005.json
@@ -0,0 +1,19 @@
+{
+ "id": "8005",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8005/",
+ "title": "Accessibility Technologies Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Accessibility & Inclusion"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/8005/1/88a5239b-516e-46f5-ad8b-2c1bc613d3b3/downloads/wwdc2026-8005_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/8005/1/88a5239b-516e-46f5-ad8b-2c1bc613d3b3/downloads/wwdc2026-8005_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:25.218Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8006.json b/data/wwdc/videos/2026-8006.json
new file mode 100644
index 0000000..a63828c
--- /dev/null
+++ b/data/wwdc/videos/2026-8006.json
@@ -0,0 +1,19 @@
+{
+ "id": "8006",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8006/",
+ "title": "SwiftUI Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/8006/1/b531da5e-7fd6-4d3b-8378-76364af6e675/downloads/wwdc2026-8006_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/8006/1/b531da5e-7fd6-4d3b-8378-76364af6e675/downloads/wwdc2026-8006_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:25.459Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8007.json b/data/wwdc/videos/2026-8007.json
new file mode 100644
index 0000000..97b3466
--- /dev/null
+++ b/data/wwdc/videos/2026-8007.json
@@ -0,0 +1,17 @@
+{
+ "id": "8007",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8007/",
+ "title": "Coding Intelligence for Beginners Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Developer Tools"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": []
+ },
+ "extractedAt": "2026-06-12T10:24:25.437Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8009.json b/data/wwdc/videos/2026-8009.json
new file mode 100644
index 0000000..5eb0c5c
--- /dev/null
+++ b/data/wwdc/videos/2026-8009.json
@@ -0,0 +1,17 @@
+{
+ "id": "8009",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8009/",
+ "title": "Privacy and Security Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Privacy & Security"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": []
+ },
+ "extractedAt": "2026-06-12T10:24:25.488Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8010.json b/data/wwdc/videos/2026-8010.json
new file mode 100644
index 0000000..c69e334
--- /dev/null
+++ b/data/wwdc/videos/2026-8010.json
@@ -0,0 +1,17 @@
+{
+ "id": "8010",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8010/",
+ "title": "App Store Connect Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Store, Distribution & Marketing"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": []
+ },
+ "extractedAt": "2026-06-12T10:24:25.296Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8011.json b/data/wwdc/videos/2026-8011.json
new file mode 100644
index 0000000..6f324b5
--- /dev/null
+++ b/data/wwdc/videos/2026-8011.json
@@ -0,0 +1,17 @@
+{
+ "id": "8011",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8011/",
+ "title": "Apple Intelligence Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Machine Learning & AI"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": []
+ },
+ "extractedAt": "2026-06-12T10:24:25.724Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8012.json b/data/wwdc/videos/2026-8012.json
new file mode 100644
index 0000000..7c0fa28
--- /dev/null
+++ b/data/wwdc/videos/2026-8012.json
@@ -0,0 +1,17 @@
+{
+ "id": "8012",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8012/",
+ "title": "Icon Composer for Beginners Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Design"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": []
+ },
+ "extractedAt": "2026-06-12T10:24:25.760Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8013.json b/data/wwdc/videos/2026-8013.json
new file mode 100644
index 0000000..2777b04
--- /dev/null
+++ b/data/wwdc/videos/2026-8013.json
@@ -0,0 +1,17 @@
+{
+ "id": "8013",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8013/",
+ "title": "Xcode Tips and Tricks Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Developer Tools"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": []
+ },
+ "extractedAt": "2026-06-12T10:24:25.968Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8014.json b/data/wwdc/videos/2026-8014.json
new file mode 100644
index 0000000..5ea803c
--- /dev/null
+++ b/data/wwdc/videos/2026-8014.json
@@ -0,0 +1,19 @@
+{
+ "id": "8014",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8014/",
+ "title": "watchOS Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services",
+ "Health & Fitness",
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": []
+ },
+ "extractedAt": "2026-06-12T10:24:26.036Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8015.json b/data/wwdc/videos/2026-8015.json
new file mode 100644
index 0000000..608715e
--- /dev/null
+++ b/data/wwdc/videos/2026-8015.json
@@ -0,0 +1,17 @@
+{
+ "id": "8015",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8015/",
+ "title": "Safari and Web Technologies Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Safari & Web"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": []
+ },
+ "extractedAt": "2026-06-12T10:24:26.070Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8016.json b/data/wwdc/videos/2026-8016.json
new file mode 100644
index 0000000..382f53a
--- /dev/null
+++ b/data/wwdc/videos/2026-8016.json
@@ -0,0 +1,18 @@
+{
+ "id": "8016",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8016/",
+ "title": "Machine Learning & AI Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": []
+ },
+ "extractedAt": "2026-06-12T10:24:26.199Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8017.json b/data/wwdc/videos/2026-8017.json
new file mode 100644
index 0000000..fa32326
--- /dev/null
+++ b/data/wwdc/videos/2026-8017.json
@@ -0,0 +1,17 @@
+{
+ "id": "8017",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8017/",
+ "title": "SwiftData Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "App Services"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": []
+ },
+ "extractedAt": "2026-06-12T10:24:26.270Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8018.json b/data/wwdc/videos/2026-8018.json
new file mode 100644
index 0000000..7037277
--- /dev/null
+++ b/data/wwdc/videos/2026-8018.json
@@ -0,0 +1,17 @@
+{
+ "id": "8018",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8018/",
+ "title": "Camera and Photo Technologies Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Photos & Camera"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": []
+ },
+ "extractedAt": "2026-06-12T10:24:26.496Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8120.json b/data/wwdc/videos/2026-8120.json
new file mode 100644
index 0000000..8a9b6cc
--- /dev/null
+++ b/data/wwdc/videos/2026-8120.json
@@ -0,0 +1,17 @@
+{
+ "id": "8120",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8120/",
+ "title": "SwiftUI Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "SwiftUI & UI Frameworks"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": []
+ },
+ "extractedAt": "2026-06-12T10:24:26.570Z"
+}
\ No newline at end of file
diff --git a/data/wwdc/videos/2026-8121.json b/data/wwdc/videos/2026-8121.json
new file mode 100644
index 0000000..4516231
--- /dev/null
+++ b/data/wwdc/videos/2026-8121.json
@@ -0,0 +1,20 @@
+{
+ "id": "8121",
+ "year": "2026",
+ "url": "https://developer.apple.com/videos/play/wwdc2026/8121/",
+ "title": "Coding Intelligence, Machine Learning & AI Group Lab",
+ "speakers": [],
+ "duration": "",
+ "topics": [
+ "Machine Learning & AI",
+ "Spatial Computing"
+ ],
+ "hasTranscript": false,
+ "hasCode": false,
+ "resources": {
+ "resourceLinks": [],
+ "hdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/8121/1/a84fe3e3-b640-4faa-9685-207aec99be2d/downloads/wwdc2026-8121_hd.mp4?dl=1",
+ "sdVideo": "https://devstreaming-cdn.apple.com/videos/wwdc/2026/8121/1/a84fe3e3-b640-4faa-9685-207aec99be2d/downloads/wwdc2026-8121_sd.mp4?dl=1"
+ },
+ "extractedAt": "2026-06-12T10:24:26.521Z"
+}
\ No newline at end of file