diff --git a/.vscode/mcp.json b/.vscode/mcp.json deleted file mode 100644 index 12d68532e..000000000 --- a/.vscode/mcp.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "inputs": [ - { - "type": "promptString", - "id": "github_token", - "description": "GitHub Personal Access Token", - "password": true - } - ], - "servers": { - "github": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server" - ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}" - } - }, - "ida": { - "command": "uvx", - "args": [ - "mcp-server-ida" - ] - } - } -} diff --git a/CLAUDE.md b/CLAUDE.md index ed7368513..308f73b44 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -94,6 +94,23 @@ The project uses **swift-testing framework** (not XCTest): - Use `@Test` attribute for test functions - Use `#expect()` for assertions instead of XCTest assertions - No comments in test case bodies - keep tests clean and self-explanatory +- For floating-point comparisons, use `isApproximatelyEqual` instead of `==` to handle precision issues: + ```swift + #expect(value.isApproximatelyEqual(to: expectedValue)) + ``` + +### Compatibility Tests + +When writing tests in `OpenSwiftUICompatibilityTests`: +- **DO NOT add conditional imports** - imports are handled in `Export.swift` +- **NEVER use module-qualified types** (e.g., `SwiftUI.PeriodicTimelineSchedule`) +- Write test code that works identically with both SwiftUI and OpenSwiftUI +- Simply use types directly without any module prefixes: + ```swift + // No conditional imports needed - Export.swift handles this + let schedule = PeriodicTimelineSchedule(from: startDate, by: interval) + let entries = schedule.entries(from: queryDate, mode: .normal) + ``` ### Code Style (from .github/copilot-instructions.md) diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 3d74c4485..7a06c926a 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -7,30 +7,28 @@ objects = { /* Begin PBXBuildFile section */ - 27186AE02D538A6B009E05F9 /* RenderBox.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6C4D42D2842810010502F /* RenderBox.xcframework */; }; 27186AE32D538A76009E05F9 /* AttributeGraph.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6C4D12D2842740010502F /* AttributeGraph.xcframework */; }; 278EF52D2E2272F2009C32EB /* Equatable in Frameworks */ = {isa = PBXBuildFile; productRef = 278EF52C2E2272F2009C32EB /* Equatable */; }; 278EF52F2E227304009C32EB /* Equatable in Frameworks */ = {isa = PBXBuildFile; productRef = 278EF52E2E227304009C32EB /* Equatable */; }; 279284972DFF136E00234D64 /* AttributeGraph.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6C4D12D2842740010502F /* AttributeGraph.xcframework */; }; - 279284982DFF136E00234D64 /* AttributeGraph.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6C4D12D2842740010502F /* AttributeGraph.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 279284992DFF136E00234D64 /* CoreUI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27EE91722DD0C753006C85FD /* CoreUI.xcframework */; }; - 2792849A2DFF136E00234D64 /* CoreUI.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 27EE91722DD0C753006C85FD /* CoreUI.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 2792849B2DFF136E00234D64 /* RenderBox.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6C4D42D2842810010502F /* RenderBox.xcframework */; }; - 2792849C2DFF136E00234D64 /* RenderBox.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6C4D42D2842810010502F /* RenderBox.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 2792849F2DFF137400234D64 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 2792849E2DFF137400234D64 /* SnapshotTesting */; }; 279FED052DF4566D00320390 /* AttributeGraph.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6C4D12D2842740010502F /* AttributeGraph.xcframework */; }; - 279FED062DF4566D00320390 /* AttributeGraph.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6C4D12D2842740010502F /* AttributeGraph.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 279FED082DF4567000320390 /* CoreUI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27EE91722DD0C753006C85FD /* CoreUI.xcframework */; }; - 279FED092DF4567000320390 /* CoreUI.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 27EE91722DD0C753006C85FD /* CoreUI.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 279FED0A2DF4567400320390 /* RenderBox.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6C4D42D2842810010502F /* RenderBox.xcframework */; }; - 279FED0B2DF4567400320390 /* RenderBox.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6C4D42D2842810010502F /* RenderBox.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 279FED0D2DF4567B00320390 /* OpenSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 279FED0C2DF4567B00320390 /* OpenSwiftUI */; }; + 27AF22B22E758F2900D534AB /* CoreUI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27EE91722DD0C753006C85FD /* CoreUI.xcframework */; }; + 27AF22B32E758F2900D534AB /* CoreUI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27EE91722DD0C753006C85FD /* CoreUI.xcframework */; }; + 27AF22B42E758F2900D534AB /* CoreUI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27EE91722DD0C753006C85FD /* CoreUI.xcframework */; }; + 27AF22B52E758F2900D534AB /* CoreUI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27EE91722DD0C753006C85FD /* CoreUI.xcframework */; }; + 27AF22B62E758F2E00D534AB /* BacklightServices.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27AF22B12E758F0F00D534AB /* BacklightServices.xcframework */; platformFilters = (ios, xros, ); }; + 27AF22B72E758F2E00D534AB /* BacklightServices.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27AF22B12E758F0F00D534AB /* BacklightServices.xcframework */; platformFilters = (ios, xros, ); }; + 27AF22B82E758F2E00D534AB /* BacklightServices.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27AF22B12E758F0F00D534AB /* BacklightServices.xcframework */; platformFilters = (ios, xros, ); }; + 27AF22B92E758F2E00D534AB /* BacklightServices.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27AF22B12E758F0F00D534AB /* BacklightServices.xcframework */; platformFilters = (ios, xros, ); }; + 27AF22BA2E758F3700D534AB /* RenderBox.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6C4D42D2842810010502F /* RenderBox.xcframework */; }; + 27AF22BB2E758F3700D534AB /* RenderBox.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6C4D42D2842810010502F /* RenderBox.xcframework */; }; + 27AF22BC2E758F3700D534AB /* RenderBox.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6C4D42D2842810010502F /* RenderBox.xcframework */; }; + 27AF22BD2E758F3700D534AB /* RenderBox.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6C4D42D2842810010502F /* RenderBox.xcframework */; }; 27CD0B5F2AFC8DA7003665EB /* OpenSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 27CD0B5E2AFC8DA7003665EB /* OpenSwiftUI */; }; 27D49E0E2BA60AF600F6E2E2 /* OpenSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 27D49E0D2BA60AF600F6E2E2 /* OpenSwiftUI */; }; 27E6C4D32D2842740010502F /* AttributeGraph.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6C4D12D2842740010502F /* AttributeGraph.xcframework */; }; - 27E6C4D62D2842810010502F /* RenderBox.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6C4D42D2842810010502F /* RenderBox.xcframework */; }; - 27EE91732DD0C753006C85FD /* CoreUI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27EE91722DD0C753006C85FD /* CoreUI.xcframework */; }; - 27EE91762DD0C77E006C85FD /* CoreUI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27EE91722DD0C753006C85FD /* CoreUI.xcframework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -50,39 +48,11 @@ }; /* End PBXContainerItemProxy section */ -/* Begin PBXCopyFilesBuildPhase section */ - 2792849D2DFF136E00234D64 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 2792849A2DFF136E00234D64 /* CoreUI.xcframework in Embed Frameworks */, - 279284982DFF136E00234D64 /* AttributeGraph.xcframework in Embed Frameworks */, - 2792849C2DFF136E00234D64 /* RenderBox.xcframework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; - 279FED072DF4566D00320390 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 279FED092DF4567000320390 /* CoreUI.xcframework in Embed Frameworks */, - 279FED062DF4566D00320390 /* AttributeGraph.xcframework in Embed Frameworks */, - 279FED0B2DF4567400320390 /* RenderBox.xcframework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - /* Begin PBXFileReference section */ 271D81642BB1E8E300A6D543 /* OpenAttributeGraph */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = OpenAttributeGraph; path = ../../OpenAttributeGraph; sourceTree = ""; }; 275751E32DEE1441003E467C /* TestingHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestingHost.app; sourceTree = BUILT_PRODUCTS_DIR; }; 279283B92DFF11CE00234D64 /* OpenSwiftUIUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OpenSwiftUIUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 27AF22B12E758F0F00D534AB /* BacklightServices.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BacklightServices.xcframework; path = ../../DarwinPrivateFrameworks/BLS/2024/BacklightServices.xcframework; sourceTree = ""; }; 27B7FC802BB31FF500272BA5 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 27CD0B492AFC8D37003665EB /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 27CD0B612AFC8E0E003665EB /* OpenSwiftUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = OpenSwiftUI; path = ..; sourceTree = ""; }; @@ -133,9 +103,10 @@ buildActionMask = 2147483647; files = ( 279FED0D2DF4567B00320390 /* OpenSwiftUI in Frameworks */, - 279FED082DF4567000320390 /* CoreUI.xcframework in Frameworks */, + 27AF22B82E758F2E00D534AB /* BacklightServices.xcframework in Frameworks */, + 27AF22B42E758F2900D534AB /* CoreUI.xcframework in Frameworks */, + 27AF22BC2E758F3700D534AB /* RenderBox.xcframework in Frameworks */, 279FED052DF4566D00320390 /* AttributeGraph.xcframework in Frameworks */, - 279FED0A2DF4567400320390 /* RenderBox.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -143,10 +114,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 279284992DFF136E00234D64 /* CoreUI.xcframework in Frameworks */, 2792849F2DFF137400234D64 /* SnapshotTesting in Frameworks */, + 27AF22B92E758F2E00D534AB /* BacklightServices.xcframework in Frameworks */, + 27AF22B52E758F2900D534AB /* CoreUI.xcframework in Frameworks */, + 27AF22BD2E758F3700D534AB /* RenderBox.xcframework in Frameworks */, 279284972DFF136E00234D64 /* AttributeGraph.xcframework in Frameworks */, - 2792849B2DFF136E00234D64 /* RenderBox.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -154,11 +126,12 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 27AF22B22E758F2900D534AB /* CoreUI.xcframework in Frameworks */, + 27AF22B62E758F2E00D534AB /* BacklightServices.xcframework in Frameworks */, 27CD0B5F2AFC8DA7003665EB /* OpenSwiftUI in Frameworks */, - 27E6C4D62D2842810010502F /* RenderBox.xcframework in Frameworks */, 278EF52F2E227304009C32EB /* Equatable in Frameworks */, - 27EE91762DD0C77E006C85FD /* CoreUI.xcframework in Frameworks */, 27E6C4D32D2842740010502F /* AttributeGraph.xcframework in Frameworks */, + 27AF22BA2E758F3700D534AB /* RenderBox.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -166,11 +139,12 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 27AF22B32E758F2900D534AB /* CoreUI.xcframework in Frameworks */, + 27AF22B72E758F2E00D534AB /* BacklightServices.xcframework in Frameworks */, 27D49E0E2BA60AF600F6E2E2 /* OpenSwiftUI in Frameworks */, - 27EE91732DD0C753006C85FD /* CoreUI.xcframework in Frameworks */, 278EF52D2E2272F2009C32EB /* Equatable in Frameworks */, - 27186AE02D538A6B009E05F9 /* RenderBox.xcframework in Frameworks */, 27186AE32D538A76009E05F9 /* AttributeGraph.xcframework in Frameworks */, + 27AF22BB2E758F3700D534AB /* RenderBox.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -211,6 +185,7 @@ 27D49E0C2BA60AF600F6E2E2 /* Frameworks */ = { isa = PBXGroup; children = ( + 27AF22B12E758F0F00D534AB /* BacklightServices.xcframework */, 27EE91722DD0C753006C85FD /* CoreUI.xcframework */, 27E6C4D42D2842810010502F /* RenderBox.xcframework */, 27E6C4D12D2842740010502F /* AttributeGraph.xcframework */, @@ -228,7 +203,6 @@ 275751DF2DEE1441003E467C /* Sources */, 275751E02DEE1441003E467C /* Frameworks */, 275751E12DEE1441003E467C /* Resources */, - 279FED072DF4566D00320390 /* Embed Frameworks */, ); buildRules = ( ); @@ -252,7 +226,6 @@ 279283B52DFF11CE00234D64 /* Sources */, 279283B62DFF11CE00234D64 /* Frameworks */, 279283B72DFF11CE00234D64 /* Resources */, - 2792849D2DFF136E00234D64 /* Embed Frameworks */, ); buildRules = ( ); diff --git a/Example/HostingExample/ViewController.swift b/Example/HostingExample/ViewController.swift index 357d59c6e..4d1b146b3 100644 --- a/Example/HostingExample/ViewController.swift +++ b/Example/HostingExample/ViewController.swift @@ -66,6 +66,6 @@ class ViewController: NSViewController { struct ContentView: View { var body: some View { - ObservationExample() + AnimatedColorTimelineView() } } diff --git a/Example/SharedExample/Animation/Timeline/AnimatedColorTimelineView.swift b/Example/SharedExample/Animation/Timeline/AnimatedColorTimelineView.swift new file mode 100644 index 000000000..6625d0aee --- /dev/null +++ b/Example/SharedExample/Animation/Timeline/AnimatedColorTimelineView.swift @@ -0,0 +1,132 @@ +// +// AnimatedColorTimelineView.swift +// SharedExample +// +// Created by Kyle on 2025/9/15. +// + +#if OPENSWIFTUI +import OpenSwiftUI +#else +import SwiftUI +#endif + +#if OPENSWIFTUI +// FIXME: Missing LinearGradient, Shape, Text and safeArea. +// We use a simplified version for OpenSwiftUI now. +struct AnimatedColorTimelineView: View { + var body: some View { + TimelineView(.animation) { context in + let time = context.date.timeIntervalSince1970 + + ZStack { + // Animated background color + Color( + hue: (sin(time * 0.5) + 1) / 2, + saturation: 0.8, + brightness: 0.9 + ) +// .ignoresSafeArea() + + VStack(spacing: 30) { +// Text("Animated Colors") +// .font(.largeTitle) +// .fontWeight(.bold) +// .foregroundColor(.white) + + // Pulsing circle that changes color +// Circle() +// .fill( + Color( + hue: (cos(time * 2) + 1) / 2, + saturation: 1.0, + brightness: 1.0 + ) +// ) + .frame( + width: 100 + sin(time * 3) * 20, + height: 100 + sin(time * 3) * 20 + ) + + // Display current color values + let currentHue = (sin(time * 0.5) + 1) / 2 + let _ = print(currentHue) +// Text("Background Hue: \(currentHue, specifier: "%.2f")") +// .font(.headline) +// .foregroundColor(.white) +// .padding() +// .background(Color.black.opacity(0.3)) +// .cornerRadius(10) + } + } + } + } +} +#else +struct AnimatedColorTimelineView: View { + var body: some View { + TimelineView(.animation) { timeline in + let time = timeline.date.timeIntervalSince1970 + + ZStack { + // Animated gradient background + LinearGradient( + colors: [ + Color( + hue: (sin(time * 0.5) + 1) / 2, + saturation: 0.8, + brightness: 0.9 + ), + Color( + hue: (cos(time * 0.3) + 1) / 2, + saturation: 0.6, + brightness: 0.7 + ), + Color( + hue: (sin(time * 0.7 + .pi) + 1) / 2, + saturation: 0.9, + brightness: 0.8 + ) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .ignoresSafeArea() + + // Content overlay + VStack(spacing: 30) { + Text("Animated Colors") + .font(.largeTitle) + .fontWeight(.bold) + .foregroundColor(.white) + .shadow(radius: 5) + + // Pulsing circle + Circle() + .fill( + Color( + hue: (sin(time * 2) + 1) / 2, + saturation: 1.0, + brightness: 1.0 + ) + ) + .frame( + width: 100 + sin(time * 3) * 20, + height: 100 + sin(time * 3) * 20 + ) + .shadow(radius: 10) + + // Color info + let currentHue = (sin(time * 0.5) + 1) / 2 + Text("Hue: \(currentHue, specifier: "%.2f")") + .font(.headline) + .foregroundColor(.white) + .padding() + .background(.ultraThinMaterial) + .cornerRadius(10) + } + } + } + } +} +#endif diff --git a/Example/SharedExample/Animation/Timeline/BreathingColorView.swift b/Example/SharedExample/Animation/Timeline/BreathingColorView.swift new file mode 100644 index 000000000..ec8032bc5 --- /dev/null +++ b/Example/SharedExample/Animation/Timeline/BreathingColorView.swift @@ -0,0 +1,49 @@ +// +// BreathingColorView.swift +// SharedExample +// +// Created by Kyle on 2025/9/15. +// + +#if OPENSWIFTUI +import OpenSwiftUI +#else +import SwiftUI +#endif + +#if !OPENSWIFTUI // FIXME: Missing Shape and Text +struct BreathingColorView: View { + var body: some View { + TimelineView(.animation) { timeline in + let time = timeline.date.timeIntervalSince1970 + let breathe = (sin(time) + 1) / 2 // Oscillates between 0 and 1 + + VStack(spacing: 40) { + Text("Breathing Colors") + .font(.title) + .fontWeight(.bold) + + // Main breathing circle + Circle() + .fill( + Color.blue.opacity(0.3 + breathe * 0.7) + ) + .frame(width: 200, height: 200) + .scaleEffect(0.8 + breathe * 0.4) + + // Color intensity indicator + Rectangle() + .fill(Color.blue) + .frame(width: 200, height: 20) + .opacity(0.3 + breathe * 0.7) + .cornerRadius(10) + + Text("Opacity: \(0.3 + breathe * 0.7, specifier: "%.2f")") + .font(.headline) + .foregroundColor(.secondary) + } + .padding() + } + } +} +#endif diff --git a/Example/SharedExample/Animation/Timeline/ColorCodedClockView.swift b/Example/SharedExample/Animation/Timeline/ColorCodedClockView.swift new file mode 100644 index 000000000..b381b22bf --- /dev/null +++ b/Example/SharedExample/Animation/Timeline/ColorCodedClockView.swift @@ -0,0 +1,184 @@ +// +// ColorCodedClockView.swift +// SharedExample +// +// Created by Kyle on 2025/9/15. +// + +#if OPENSWIFTUI +import OpenSwiftUI +#else +import SwiftUI +#endif + +#if !OPENSWIFTUI // FIXME: Missing Shape and Text +struct ColorCodedClockView: View { + var body: some View { + TimelineView(.periodic(from: .now, by: 1.0)) { timeline in + let date = timeline.date + let calendar = Calendar.current + let hour = calendar.component(.hour, from: date) + let minute = calendar.component(.minute, from: date) + let second = calendar.component(.second, from: date) + + VStack(spacing: 20) { + Text("Color-Coded Clock") + .font(.title) + .fontWeight(.bold) + + HStack(spacing: 10) { + // Hour + TimeComponentView( + value: hour, + maxValue: 24, + label: "Hours", + baseColor: .red + ) + + Text(":") + .font(.largeTitle) + .fontWeight(.bold) + + // Minute + TimeComponentView( + value: minute, + maxValue: 60, + label: "Minutes", + baseColor: .green + ) + + Text(":") + .font(.largeTitle) + .fontWeight(.bold) + + // Second + TimeComponentView( + value: second, + maxValue: 60, + label: "Seconds", + baseColor: .blue + ) + } + + // Color legend + HStack(spacing: 20) { + LegendItem(color: .red, label: "Hours") + LegendItem(color: .green, label: "Minutes") + LegendItem(color: .blue, label: "Seconds") + } + .padding() + .background(.ultraThinMaterial) + .cornerRadius(15) + + // Time progress bars + VStack(spacing: 10) { + ProgressBar(value: hour, maxValue: 24, color: .red, label: "Hour Progress") + ProgressBar(value: minute, maxValue: 60, color: .green, label: "Minute Progress") + ProgressBar(value: second, maxValue: 60, color: .blue, label: "Second Progress") + } + .padding() + } + .padding() + } + } +} + +struct TimeComponentView: View { + let value: Int + let maxValue: Int + let label: String + let baseColor: Color + + var body: some View { + VStack(spacing: 5) { + Text(String(format: "%02d", value)) + .font(.largeTitle) + .fontWeight(.bold) + .foregroundColor(.white) + .frame(width: 70, height: 70) + .background( + LinearGradient( + colors: [ + baseColor.opacity(0.6), + baseColor.opacity(Double(value) / Double(maxValue) + 0.3) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .cornerRadius(15) + .shadow(radius: 5) + + Text(label) + .font(.caption) + .foregroundColor(.secondary) + } + } +} + +struct LegendItem: View { + let color: Color + let label: String + + var body: some View { + HStack(spacing: 5) { + LinearGradient( + colors: [color.opacity(0.6), color], + startPoint: .leading, + endPoint: .trailing + ) + .frame(width: 15, height: 15) + .cornerRadius(3) + + Text(label) + .font(.caption) + } + } +} + +struct ProgressBar: View { + let value: Int + let maxValue: Int + let color: Color + let label: String + + var body: some View { + VStack(alignment: .leading, spacing: 3) { + HStack { + Text(label) + .font(.caption) + .foregroundColor(.secondary) + Spacer() + Text("\(value)/\(maxValue)") + .font(.caption) + .foregroundColor(.secondary) + } + + GeometryReader { geometry in + ZStack(alignment: .leading) { + // Background + RoundedRectangle(cornerRadius: 5) + .fill(Color.gray.opacity(0.2)) + .frame(height: 8) + + // Progress fill with gradient + LinearGradient( + colors: [ + color.opacity(0.7), + color + ], + startPoint: .leading, + endPoint: .trailing + ) + .frame( + width: geometry.size.width * (Double(value) / Double(maxValue)), + height: 8 + ) + .clipShape(RoundedRectangle(cornerRadius: 5)) + } + } + .frame(height: 8) + } + } +} +#endif diff --git a/Package.resolved b/Package.resolved index 0c1b98f95..05a573cb3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "9bd5f50dcfa75735a7e46a92248a1f4c358d021fa1af2222a0767d601b13d3b2", + "originHash" : "8d360f40fb5597175d979d470392550ddec9eab480e1079984d3ca5d305dfac7", "pins" : [ { "identity" : "darwinprivateframeworks", @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", "state" : { "branch" : "main", - "revision" : "c94747dbacafafe8a649222b3bf00455c1c482d7" + "revision" : "8a2d63286731ff9ba4d30776bb54aa64b0939167" } }, { @@ -16,7 +16,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenAttributeGraph", "state" : { "branch" : "main", - "revision" : "f92b2c8afbf8ded48206a71f0060f80d6c389406" + "revision" : "80d85ebb7cc2195f4115c830c190776a7b141c84" } }, { diff --git a/Package.swift b/Package.swift index 11eaaad5d..fe72e6c16 100644 --- a/Package.swift +++ b/Package.swift @@ -95,16 +95,35 @@ if development { // MARK: - [env] OPENSWIFTUI_LINK_COREUI let linkCoreUI = envEnable("OPENSWIFTUI_LINK_COREUI", default: buildForDarwinPlatform && !isSPIBuild) - +sharedCSettings.append(.define("OPENSWIFTUI_LINK_COREUI", to: linkCoreUI ? "1" : "0")) +sharedCxxSettings.append(.define("OPENSWIFTUI_LINK_COREUI", to: linkCoreUI ? "1" : "0")) if linkCoreUI { - sharedCSettings.append( - .define("OPENSWIFTUI_LINK_COREUI") + sharedSwiftSettings.append(.define("OPENSWIFTUI_LINK_COREUI")) +} + +// MARK: - [env] OPENSWIFTUI_LINK_BACKLIGHTSERVICES + +let linkBacklightServices = envEnable("OPENSWIFTUI_LINK_BACKLIGHTSERVICES", default: buildForDarwinPlatform && !isSPIBuild) +sharedCSettings.append( + .define( + "OPENSWIFTUI_LINK_BACKLIGHTSERVICES", + to: linkBacklightServices ? "1" : "0", + .when(platforms: [.iOS, .visionOS]) ) - sharedCxxSettings.append( - .define("OPENSWIFTUI_LINK_COREUI") +) +sharedCxxSettings.append( + .define( + "OPENSWIFTUI_LINK_BACKLIGHTSERVICES", + to: linkBacklightServices ? "1" : "0", + .when(platforms: [.iOS, .visionOS]) ) +) +if linkBacklightServices { sharedSwiftSettings.append( - .define("OPENSWIFTUI_LINK_COREUI") + .define( + "OPENSWIFTUI_LINK_BACKLIGHTSERVICES", + .when(platforms: [.iOS, .visionOS]) + ) ) } @@ -484,6 +503,18 @@ extension Target { dependencies.append(.product(name: "CoreUI", package: "DarwinPrivateFrameworks")) } + func addBacklightServicesSettings() { + // FIXME: Weird SwiftPM behavior for test Target. Otherwize we'll get the following error message + // "could not determine executable path for bundle 'BacklightServices.framework'" + dependencies.append( + .product( + name: "BacklightServices", + package: "DarwinPrivateFrameworks", + condition: .when(platforms: [.iOS, .visionOS]) + ) + ) + } + func addOpenCombineSettings() { dependencies.append(.product(name: "OpenCombine", package: "OpenCombine")) var swiftSettings = swiftSettings ?? [] @@ -548,6 +579,11 @@ if linkCoreUI { openSwiftUISPITarget.addCoreUISettings() } +if linkBacklightServices { + openSwiftUITarget.addBacklightServicesSettings() + openSwiftUISPITarget.addBacklightServicesSettings() +} + if useLocalDeps { var dependencies: [Package.Dependency] = [ .package(path: "../OpenCoreGraphics"), @@ -555,7 +591,7 @@ if useLocalDeps { .package(path: "../OpenRenderBox"), .package(path: "../OpenObservation"), ] - if attributeGraphCondition || renderBoxCondition || linkCoreUI { + if attributeGraphCondition || renderBoxCondition || linkCoreUI || linkBacklightServices { dependencies.append(.package(path: "../DarwinPrivateFrameworks")) } package.dependencies += dependencies diff --git a/Sources/OpenSwiftUI/Animation/Timeline/AlwaysOnBridge.swift b/Sources/OpenSwiftUI/Animation/Timeline/AlwaysOnBridge.swift new file mode 100644 index 000000000..73dd5c2c0 --- /dev/null +++ b/Sources/OpenSwiftUI/Animation/Timeline/AlwaysOnBridge.swift @@ -0,0 +1,210 @@ +// +// AlwaysOnBridge.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: ED1CCB5A10919A16BDE683BBA73F40A5 (SwiftUI) + +#if (os(iOS) || os(visionOS)) && OPENSWIFTUI_LINK_BACKLIGHTSERVICES + +import OpenAttributeGraphShims +@_spi(ForOpenSwiftUIOnly) +@_spi(Private) +import OpenSwiftUICore +import OpenSwiftUI_SPI + +// MARK: - AnyAlwaysOnBridge + +class AnyAlwaysOnBridge { + func invalidate(for reason: String) { + _openSwiftUIBaseClassAbstractMethod() + } + + func didRender() { + _openSwiftUIBaseClassAbstractMethod() + } +} + +// MARK: - AlwaysOnBridge + +final class AlwaysOnBridge: AnyAlwaysOnBridge where Content: View { + weak var hostingController:PlatformHostingController? + + private var updatingTraitsCount: UInt64 = .zero + + private var frameSpecifier: BLSAlwaysOnFrameSpecifier? = nil + + var isLuminanceReduced: Bool = false + + var isUpdatingForFrameSpecifier: Bool = false + + var timelineRegistrationsSeed: VersionSeed = .empty + + var timelineRegistrations: [DateSequenceTimeline] = [] { + didSet { + guard !oldValue.elementsEqual(timelineRegistrations) else { + return + } + invalidate(for: "Timeline registrations changed.") + } + } + + func configureTransaction(_ transaction: inout Transaction) { + updatingTraitsCount &+= 1 + transaction.addAnimationListener { + DispatchQueue.main.async { + self.updatingTraitsCount &-= 1 + } + } + } + + func hostingControllerWillDisappear() { + guard frameSpecifier != nil else { return } + frameSpecifier = nil + hostingController!.host.invalidateProperties(.environment) + } + + override func invalidate(for reason: String) { + let host = hostingController!.host + guard let window = host.window, + let scene = window.windowScene, + let backlightSceneEnvironment = scene._backlightSceneEnvironment + else { return } + backlightSceneEnvironment.invalidateAllTimelines(forReason: reason) + } + + var isActiveHost: Bool { + let host = hostingController!.host + guard let window = host.window, + let scene = window.windowScene + else { return false } + var controllers: [any UIViewController & _UIBacklightEnvironmentObserver] = [] + for window in scene.windows { + controllers.append(contentsOf: window.rootViewController?._effectiveControllersForAlwaysOnTimelines ?? []) + } + return controllers.contains { $0 === hostingController } + } + + func preferencesDidChange(_ preference: PreferenceValues) { + let value = preference[AlwaysOnTimelinesKey.self] + guard !value.seed.matches(timelineRegistrationsSeed) else { + return + } + timelineRegistrationsSeed = value.seed + timelineRegistrations = value.value + } + + func timelines(for _: DateInterval) -> [BLSAlwaysOnTimeline] { + timelineRegistrations + } + + func update(environment: inout EnvironmentValues) { + environment.suppliedBridges.formUnion(.alwaysOnBridge) + if isActiveHost { + environment[AlwaysOnFrameSpecifier.self] = frameSpecifier + } + environment[AlwaysOnInvalidationKey.self] = .init(bridge: self) + isLuminanceReduced = environment.isLuminanceReduced + } + + func update(with specifier: BLSAlwaysOnFrameSpecifier?) { + isUpdatingForFrameSpecifier = true + frameSpecifier = specifier + let viewGraph = hostingController!.host.viewGraph + var transaction = Transaction() + transaction.disablesAnimations = true + transaction.disablesContentTransitions = true + viewGraph.emptyTransaction(transaction) + hostingController!.host.invalidateProperties(.environment) + hostingController!.host.layoutIfNeeded() + } +} + +// MARK: - AlwaysOnTimelinesKey + +struct AlwaysOnTimelinesKey: HostPreferenceKey { + static let defaultValue: [DateSequenceTimeline] = [] + + static func reduce(value: inout [DateSequenceTimeline], nextValue: () -> [DateSequenceTimeline]) { + value.append(contentsOf: nextValue()) + } +} + +// MARK: - TimelineInvalidationAction + +struct TimelineInvalidationAction: Equatable { + weak var bridge: AnyAlwaysOnBridge? + + static func == (lhs: TimelineInvalidationAction, rhs: TimelineInvalidationAction) -> Bool { + lhs.bridge === rhs.bridge + } +} + +// MARK: - AlwaysOnFrameSpecifier + +private struct AlwaysOnFrameSpecifier: EnvironmentKey { + static var defaultValue: BLSAlwaysOnFrameSpecifier? { nil } +} + +extension CachedEnvironment.ID { + static let alwaysOnFrameSpecifier: CachedEnvironment.ID = .init() +} + +extension _GraphInputs { + var alwaysOnFrameSpecifier: Attribute { + mapEnvironment(id: .alwaysOnFrameSpecifier) { $0[AlwaysOnFrameSpecifier.self] } + } +} + +// MARK: - AlwaysOnInvalidationKey + +private struct AlwaysOnInvalidationKey: EnvironmentKey { + static let defaultValue: TimelineInvalidationAction = .init() +} + +extension CachedEnvironment.ID { + static let alwaysOnInvalidationAction: CachedEnvironment.ID = .init() +} + +extension _GraphInputs { + var alwaysOnInvalidationAction: Attribute { + mapEnvironment(id: .alwaysOnInvalidationAction) { $0[AlwaysOnInvalidationKey.self] } + } +} + +// MARK: - OpenSwiftUITextAlwaysOnProvider + +struct OpenSwiftUITextAlwaysOnProvider: TextAlwaysOnProvider { + static func makeAlwaysOn( + inputs: _ViewInputs, + schedule: @autoclosure () -> Attribute<(any TimelineSchedule)?>, + outputs: inout _ViewOutputs + ) { + + guard _UIAlwaysOnEnvironment._alwaysOnSupported else { + return + } + outputs.preferences.makePreferenceWriter( + inputs: inputs.preferences, + key: AlwaysOnTimelinesKey.self, + value: Attribute(AlwaysOnTimelinePreferenceWriter(id: .init(), schedule: schedule())) + ) + } +} + +// MARK: - AlwaysOnTimelinePreferenceWriter + +struct AlwaysOnTimelinePreferenceWriter: Rule { + var id: TimelineIdentifier + @Attribute var schedule: (any TimelineSchedule)? + + var value: [DateSequenceTimeline] { + guard let schedule else { + return [] + } + return [DateSequenceTimeline(identifier: id, schedule: schedule)] + } +} + +#endif diff --git a/Sources/OpenSwiftUI/Animation/Timeline/AnimationTimelineSchedule.swift b/Sources/OpenSwiftUI/Animation/Timeline/AnimationTimelineSchedule.swift new file mode 100644 index 000000000..0413f7122 --- /dev/null +++ b/Sources/OpenSwiftUI/Animation/Timeline/AnimationTimelineSchedule.swift @@ -0,0 +1,85 @@ +// +// AnimationTimelineSchedule.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +public import Foundation + +@available(OpenSwiftUI_v3_0, *) +extension TimelineSchedule where Self == AnimationTimelineSchedule { + + /// A pausable schedule of dates updating at a frequency no more quickly + /// than the provided interval. + @_alwaysEmitIntoClient + public static var animation: AnimationTimelineSchedule { + .init() + } + + /// A pausable schedule of dates updating at a frequency no more quickly + /// than the provided interval. + /// + /// - Parameters: + /// - minimumInterval: The minimum interval to update the schedule at. + /// Pass nil to let the system pick an appropriate update interval. + /// - paused: If the schedule should stop generating updates. + @_alwaysEmitIntoClient + public static func animation( + minimumInterval: Double? = nil, + paused: Bool = false + ) -> AnimationTimelineSchedule { + .init(minimumInterval: minimumInterval, paused: paused) + } +} + +/// A pausable schedule of dates updating at a frequency no more quickly than +/// the provided interval. +/// +/// You can also use ``TimelineSchedule/animation(minimumInterval:paused:)`` to +/// construct this schedule. +@available(OpenSwiftUI_v3_0, *) +public struct AnimationTimelineSchedule: TimelineSchedule, Sendable { + + private var minimumInterval: Double + + private var paused: Bool + + /// Create a pausable schedule of dates updating at a frequency no more + /// quickly than the provided interval. + /// + /// - Parameters: + /// - minimumInterval: The minimum interval to update the schedule at. + /// Pass nil to let the system pick an appropriate update interval. + /// - paused: If the schedule should stop generating updates. + public init(minimumInterval: Double? = nil, paused: Bool = false) { + self.minimumInterval = minimumInterval ?? (1.0 / 120.0) + self.paused = paused + } + + /// Returns entries at the frequency of the animation schedule. + /// + /// When in `.lowFrequency` mode, return no entries, effectively pausing the animation. + public func entries(from start: Date, mode: TimelineScheduleMode) -> AnimationTimelineSchedule.Entries { + Entries(date: start, interval: (paused || mode == .lowFrequency) ? nil : minimumInterval) + } + + public struct Entries: Sequence, IteratorProtocol, Sendable { + private var date: Date + + private var interval: Double? + + init(date: Date, interval: Double? = nil) { + self.date = date + self.interval = interval + } + + public mutating func next() -> Date? { + guard let interval else { + return nil + } + defer { date += interval } + return date + } + } +} diff --git a/Sources/OpenSwiftUI/Animation/Timeline/DateSequenceTimeline.swift b/Sources/OpenSwiftUI/Animation/Timeline/DateSequenceTimeline.swift new file mode 100644 index 000000000..a11742052 --- /dev/null +++ b/Sources/OpenSwiftUI/Animation/Timeline/DateSequenceTimeline.swift @@ -0,0 +1,166 @@ +// +// DateSequenceTimeline.swift +// OpenSwiftUI +// +// Audit for 6.5.4 +// Status: Complete +// ID: 4A16DECB179482C36B65AC864E5087D (SwiftUI?) + +#if (os(iOS) || os(visionOS)) && OPENSWIFTUI_LINK_BACKLIGHTSERVICES + +import BacklightServices +import OpenAttributeGraphShims + +// MARK: - DateSequenceTimeline + +class DateSequenceTimeline: BLSAlwaysOnTimeline { + var schedule: any TimelineSchedule + + init(identifier: TimelineIdentifier, schedule: any TimelineSchedule) { + self.schedule = schedule + super.init(identifier: identifier, configure: nil) + } + + override func requestedFidelityForStartEntry( + in interval: DateInterval, + withPreviousEntry entry: BLSAlwaysOnTimelineEntry? + ) -> BLSUpdateFidelity { + if let entry { + return entry.requestedFidelity + } else { + let entries = schedule.lazyEntries( + within: interval.start ..< .distantFuture, + mode: .lowFrequency, + limit: .minimumTimelineScheduleLimit + ) + let iterator = entries.makeIterator() + guard let current = iterator.next(), let next = iterator.next() else { + return .unspecified + } + return estimatedFidelity(forPresentationTime: current, nextPresentationTime: next) + } + } + + override func unconfiguredEntries( + for interval: DateInterval, + previousEntry entry: BLSAlwaysOnTimelineEntry? + ) -> [BLSAlwaysOnTimelineUnconfiguredEntry]? { + let clampedLimit = UInt((interval.duration * 4).clamp(min: -1, max: Double(UInt.max))) + let limit = max(.minimumTimelineScheduleLimit, clampedLimit) + let dates = schedule.entries( + within: interval, + mode: .lowFrequency, + limit: limit + ) + guard !dates.isEmpty else { + return [] + } + var unconfiguredEntries: [BLSAlwaysOnTimelineUnconfiguredEntry] = [] + unconfiguredEntries.reserveCapacity(dates.count) + for date in dates { + let unconfiguredEntry = BLSAlwaysOnTimelineUnconfiguredEntry( + forPresentationTime: date, + withRequestedFidelity: .unspecified + ) + unconfiguredEntries.append(unconfiguredEntry) + } + + return unconfiguredEntries + } + + static func == (lhs: DateSequenceTimeline, rhs: DateSequenceTimeline) -> Bool { + func areEqual(_ a: T, _ b: Any) -> Bool where T: Equatable { + guard let b = b as? T else { + return false + } + return a == b + } + guard let equatable = lhs.schedule as? any Equatable else { + return lhs === rhs + } + return areEqual(equatable, rhs.schedule) + } +} + +// MARK: - TimelineView.Context + invalidate + +@available(OpenSwiftUI_v4_0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +extension TimelineView.Context { + + /// Resets any pre-rendered views the system has from the timeline. + /// + /// When entering Always On Display, the system might pre-render frames. If the + /// content of these frames must change in a way that isn't reflected by + /// the schedule or the timeline view's current bindings --- for example, because + /// the user changes the title of a future calendar event --- call this method to + /// request that the frames be regenerated. + public func invalidateTimelineContent() { + guard let bridge = invalidationAction.bridge else { + return + } + bridge.invalidate(for: "Explicit timeline invalidation") + } +} + +// MARK: - TimelineIdentifier + +@objc +class TimelineIdentifier: NSObject, NSCopying { + private let identifier: UniqueID + + override init() { + identifier = .init() + super.init() + } + + init(identifier: UniqueID) { + self.identifier = identifier + super.init() + } + + override func isEqual(_ object: Any?) -> Bool { + guard let object, let other = object as? Self else { + return false + } + return identifier == other.identifier + } + + func copy(with zone: NSZone? = nil) -> Any { + self + } +} + +// MARK: - UpdateFidelityKey + +private struct UpdateFidelityKey: EnvironmentKey { + static var defaultValue: BLSUpdateFidelity = .seconds +} + +extension CachedEnvironment.ID { + static let updateFidelity: CachedEnvironment.ID = .init() +} + +extension _GraphInputs { + var updateFidelity: Attribute { + mapEnvironment(id: .updateFidelity) { $0[UpdateFidelityKey.self] } + } +} + +// MARK: - TimelineView.AlwaysOnTimelinePreferenceWriter + +extension TimelineView where Content: View { + struct AlwaysOnTimelinePreferenceWriter: Rule { + var id: TimelineIdentifier + @Attribute var schedule: Schedule + + var value: (inout [DateSequenceTimeline]) -> () { + let timeline = DateSequenceTimeline(identifier: id, schedule: schedule) + return { timelines in + timelines.append(timeline) + } + } + } +} +#endif diff --git a/Sources/OpenSwiftUI/Animation/Timeline/TimelineView.swift b/Sources/OpenSwiftUI/Animation/Timeline/TimelineView.swift new file mode 100644 index 000000000..e4ff9dfa4 --- /dev/null +++ b/Sources/OpenSwiftUI/Animation/Timeline/TimelineView.swift @@ -0,0 +1,485 @@ +// +// TimelineView.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: A009558074DBEAE1A969E3C6E8DD1422 (SwiftUI) + +public import Foundation +import OpenAttributeGraphShims +@_spi(ForOpenSwiftUIOnly) +public import OpenSwiftUICore + +#if (os(iOS) || os(visionOS)) && OPENSWIFTUI_LINK_BACKLIGHTSERVICES +import BacklightServices +#endif + +// MARK: - TimelineView + +/// A view that updates according to a schedule that you provide. +/// +/// A timeline view acts as a container with no appearance of its own. Instead, +/// it redraws the content it contains at scheduled points in time. +/// For example, you can update the face of an analog timer once per second: +/// +/// TimelineView(.periodic(from: startDate, by: 1)) { context in +/// AnalogTimerView(date: context.date) +/// } +/// +/// The closure that creates the content receives an input of type ``Context`` +/// that you can use to customize the content's appearance. The context includes +/// the ``Context/date`` that triggered the update. In the example above, +/// the timeline view sends that date to an analog timer that you create so the +/// timer view knows how to draw the hands on its face. +/// +/// The context also includes a ``Context/cadence-swift.property`` +/// property that you can use to hide unnecessary detail. For example, you +/// can use the cadence to decide when it's appropriate to display the +/// timer's second hand: +/// +/// TimelineView(.periodic(from: startDate, by: 1.0)) { context in +/// AnalogTimerView( +/// date: context.date, +/// showSeconds: context.cadence <= .seconds) +/// } +/// +/// The system might use a cadence that's slower than the schedule's +/// update rate. For example, a view on watchOS might remain visible when the +/// user lowers their wrist, but update less frequently, and thus require +/// less detail. +/// +/// You can define a custom schedule by creating a type that conforms to the +/// ``TimelineSchedule`` protocol, or use one of the built-in schedule types: +/// * Use an ``TimelineSchedule/everyMinute`` schedule to update at the +/// beginning of each minute. +/// * Use a ``TimelineSchedule/periodic(from:by:)`` schedule to update +/// periodically with a custom start time and interval between updates. +/// * Use an ``TimelineSchedule/explicit(_:)`` schedule when you need a finite number, or +/// irregular set of updates. +/// +/// For a schedule containing only dates in the past, +/// the timeline view shows the last date in the schedule. +/// For a schedule containing only dates in the future, +/// the timeline draws its content using the current date +/// until the first scheduled date arrives. +@available(OpenSwiftUI_v3_0, *) +public struct TimelineView where Schedule: TimelineSchedule { + + /// Information passed to a timeline view's content callback. + /// + /// The context includes both the ``date`` from the schedule that triggered + /// the callback, and a ``cadence-swift.property`` that you can use + /// to customize the appearance of your view. For example, you might choose + /// to display the second hand of an analog clock only when the cadence is + /// ``Cadence-swift.enum/seconds`` or faster. + public struct Context { + + /// A rate at which timeline views can receive updates. + /// + /// Use the cadence presented to content in a ``TimelineView`` to hide + /// information that updates faster than the view's current update rate. + /// For example, you could hide the millisecond component of a digital + /// timer when the cadence is ``seconds`` or ``minutes``. + /// + /// Because this enumeration conforms to the + /// [Comparable](https://developer.apple.com/documentation/swift/comparable) + /// protocol, you can compare cadences with relational operators. + /// Slower cadences have higher values, so you could perform the check + /// described above with the following comparison: + /// + /// let hideMilliseconds = cadence > .live + /// + public enum Cadence: Comparable, Sendable { + + /// Updates the view continuously. + case live + + /// Updates the view approximately once per second. + case seconds + + /// Updates the view approximately once per minute. + case minutes + } + + /// The date from the schedule that triggered the current view update. + /// + /// The first time a ``TimelineView`` closure receives this date, it + /// might be in the past. For example, if you create an + /// ``TimelineSchedule/everyMinute`` schedule at `10:09:55`, the + /// schedule creates entries `10:09:00`, `10:10:00`, `10:11:00`, and so + /// on. In response, the timeline view performs an initial update + /// immediately, at `10:09:55`, but the context contains the `10:09:00` + /// date entry. Subsequent entries arrive at their corresponding times. + public let date: Date + + /// The rate at which the timeline updates the view. + /// + /// Use this value to hide information that updates faster than the + /// view's current update rate. For example, you could hide the + /// millisecond component of a digital timer when the cadence is + /// anything slower than ``Cadence-swift.enum/live``. + /// + /// Because the ``Cadence-swift.enum`` enumeration conforms to the + /// [Comparable](https://developer.apple.com/documentation/swift/comparable) + /// protocol, you can compare cadences with relational operators. + /// Slower cadences have higher values, so you could perform the check + /// described above with the following comparison: + /// + /// let hideMilliseconds = cadence > .live + /// + public let cadence: Cadence + + #if (os(iOS) || os(visionOS)) && OPENSWIFTUI_LINK_BACKLIGHTSERVICES + let invalidationAction: TimelineInvalidationAction + #endif + } + + var schedule: Schedule + + var content: (Context) -> Content +} + +@available(*, unavailable) +extension TimelineView: Sendable {} + +@available(*, unavailable) +extension TimelineView.Context: Sendable {} + +/// Information passed to a timeline view's content callback. +/// +/// The context includes both the date from the schedule that triggered +/// the callback, and a cadence that you can use to customize the appearance of +/// your view. For example, you might choose to display the second hand of an +/// analog clock only when the cadence is +/// ``TimelineView/Context/Cadence-swift.enum/seconds`` or faster. +/// +/// > Note: This type alias uses a specific concrete instance of +/// ``TimelineView/Context`` that all timeline views can use. +/// It does this to prevent introducing an unnecessary generic parameter +/// dependency on the context type. +@available(OpenSwiftUI_v3_0, *) +public typealias TimelineViewDefaultContext = TimelineView.Context + +// MARK: - TimelineView + View [WIP] + +@available(OpenSwiftUI_v3_0, *) +extension TimelineView: View, PrimitiveView, UnaryView where Content: View { + + public typealias Body = Never + + /// Creates a new timeline view that uses the given schedule. + /// + /// - Parameters: + /// - schedule: A schedule that produces a sequence of dates that + /// indicate the instances when the view should update. + /// Use a type that conforms to ``TimelineSchedule``, like + /// ``TimelineSchedule/everyMinute``, or a custom timeline schedule + /// that you define. + /// - content: A closure that generates view content at the moments + /// indicated by the schedule. The closure takes an input of type + /// ``TimelineViewDefaultContext`` that includes the date from the schedule that + /// prompted the update, as well as a ``Context/Cadence-swift.enum`` + /// value that the view can use to customize its appearance. + @_alwaysEmitIntoClient + nonisolated public init( + _ schedule: Schedule, + @ViewBuilder content: @escaping (TimelineViewDefaultContext) -> Content + ) { + self.init(schedule) { (context: Context) -> Content in + content(unsafeBitCast(context, to: TimelineViewDefaultContext.self)) + } + } + + /// Creates a new timeline view that uses the given schedule. + /// + /// - Parameters: + /// - schedule: A schedule that produces a sequence of dates that + /// indicate the instances when the view should update. + /// Use a type that conforms to ``TimelineSchedule``, like + /// ``TimelineSchedule/everyMinute``, or a custom timeline schedule + /// that you define. + /// - content: A closure that generates view content at the moments + /// indicated by the schedule. The closure takes an input of type + /// ``Context`` that includes the date from the schedule that + /// prompted the update, as well as a ``Context/Cadence-swift.enum`` + /// value that the view can use to customize its appearance. + @available(*, deprecated, message: "Use TimelineViewDefaultContext for the type of the context parameter passed into TimelineView's content closure to resolve this warning. The new version of this initializer, using TimelineViewDefaultContext, improves compilation performance by using an independent generic type signature, which helps avoid unintended cyclical type dependencies.") + @_disfavoredOverload + nonisolated public init( + _ schedule: Schedule, + @ViewBuilder content: @escaping (Context) -> Content + ) { + self.schedule = schedule + self.content = content + } + + nonisolated public static func _makeView( + view: _GraphValue, + inputs: _ViewInputs + ) -> _ViewOutputs { + #if (os(iOS) || os(visionOS)) && OPENSWIFTUI_LINK_BACKLIGHTSERVICES + let id = TimelineIdentifier() + let filter = UpdateFilter( + view: view.value, + schedule: view.value[offset: { .of(&$0.schedule) }], + phase: inputs.viewPhase, + time: inputs.time, + referenceDate: inputs.base.referenceDate, + id: id, + frameSpecifier: inputs.base.alwaysOnFrameSpecifier, + fidelity: inputs.base.updateFidelity, + invalidationHandler: inputs.base.alwaysOnInvalidationAction, + hadFrameSpecifier: false, + resetSeed: .zero, + currentTime: -.infinity, + nextTime: .infinity, + cadence: .live + ) + let filterView = _GraphValue(filter) + var outputs = Content.makeDebuggableView(view: filterView, inputs: inputs) + outputs.preferences.makePreferenceTransformer( + inputs: inputs.preferences, + key: AlwaysOnTimelinesKey.self, + transform: Attribute( + AlwaysOnTimelinePreferenceWriter( + id: id, + schedule: view.value.unsafeBitCast(to: Schedule.self) + ) + ) + ) + return outputs + #else + let filter = UpdateFilter( + view: view.value, + schedule: view.value[offset: { .of(&$0.schedule) }], + phase: inputs.viewPhase, + time: inputs.time, + referenceDate: inputs.base.referenceDate, + resetSeed: .zero, + currentTime: -.infinity, + nextTime: .infinity, + cadence: .live + ) + let filterView = _GraphValue(filter) + let outputs = Content.makeDebuggableView(view: filterView, inputs: inputs) + return outputs + #endif + } + + private struct UpdateFilter: StatefulRule, AsyncAttribute { + @Attribute var view: TimelineView + @Attribute var schedule: Schedule + @Attribute var phase: _GraphInputs.Phase + @Attribute var time: Time + @WeakAttribute var referenceDate: (Date?)? + #if (os(iOS) || os(visionOS)) && OPENSWIFTUI_LINK_BACKLIGHTSERVICES + var id: TimelineIdentifier + @Attribute var frameSpecifier: BLSAlwaysOnFrameSpecifier? + @Attribute var fidelity: BLSUpdateFidelity + @Attribute var invalidationHandler: TimelineInvalidationAction + var hadFrameSpecifier: Bool + #endif + var resetSeed: UInt32 + var iterator: Schedule.Entries.Iterator? + var currentTime: Double + var nextTime: Double + var cadence: Context.Cadence + + #if (os(iOS) || os(visionOS)) && OPENSWIFTUI_LINK_BACKLIGHTSERVICES + init( + view: Attribute, + schedule: Attribute, + phase: Attribute<_GraphInputs.Phase>, + time: Attribute