From 9c2cc8174a46623c9b57c2abb5f5e539aaec9271 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 14 Oct 2025 01:44:46 +0800 Subject: [PATCH] Add View.transition API support --- .../Transition/TransitionTraitKey.swift | 73 +++++++++++++++++-- ...anceActionModifierCompatibilityTests.swift | 45 ++++++++++++ 2 files changed, 113 insertions(+), 5 deletions(-) diff --git a/Sources/OpenSwiftUICore/Animation/Transition/TransitionTraitKey.swift b/Sources/OpenSwiftUICore/Animation/Transition/TransitionTraitKey.swift index 3f1eaa05c..511b1f880 100644 --- a/Sources/OpenSwiftUICore/Animation/Transition/TransitionTraitKey.swift +++ b/Sources/OpenSwiftUICore/Animation/Transition/TransitionTraitKey.swift @@ -5,7 +5,72 @@ // Audited for 6.5.4 // Status: Complete -// MARK: - ViewTraitCollection + canTransition +// MARK: - View + transition + +@available(OpenSwiftUI_v1_0, *) +extension View { + + /// Associates a transition with the view. + /// + /// When this view appears or disappears, the transition will be applied to + /// it, allowing for animating it in and out. + /// + /// The following code will conditionally show MyView, and when it appears + /// or disappears, will use a slide transition to show it. + /// + /// if isActive { + /// MyView() + /// .transition(.slide) + /// } + /// Button("Toggle") { + /// withAnimation { + /// isActive.toggle() + /// } + /// } + @inlinable + @_disfavoredOverload + nonisolated public func transition(_ t: AnyTransition) -> some View { + return _trait(TransitionTraitKey.self, t) + } + + /// Associates a transition with the view. + /// + /// When this view appears or disappears, the transition will be applied to + /// it, allowing for animating it in and out. + /// + /// The following code will conditionally show MyView, and when it appears + /// or disappears, will use a custom RotatingFadeTransition transition to + /// show it. + /// + /// if isActive { + /// MyView() + /// .transition(RotatingFadeTransition()) + /// } + /// Button("Toggle") { + /// withAnimation { + /// isActive.toggle() + /// } + /// } + @available(OpenSwiftUI_v5_0, *) + @_alwaysEmitIntoClient + nonisolated public func transition(_ transition: T) -> some View where T: Transition { + self.transition(AnyTransition(transition)) + } +} + +// MARK: - TransitionTraitKey + +@available(OpenSwiftUI_v1_0, *) +@usableFromInline +struct TransitionTraitKey: _ViewTraitKey { + @inlinable + static var defaultValue: AnyTransition { .opacity } +} + +@available(*, unavailable) +extension TransitionTraitKey: Sendable {} + +// MARK: - CanTransitionTraitKey @available(OpenSwiftUI_v1_0, *) @usableFromInline @@ -17,6 +82,8 @@ struct CanTransitionTraitKey: _ViewTraitKey { @available(*, unavailable) extension CanTransitionTraitKey: Sendable {} +// MARK: - ViewTraitCollection + canTransition + extension ViewTraitCollection { package var canTransition: Bool { get { self[CanTransitionTraitKey.self] } @@ -26,10 +93,6 @@ extension ViewTraitCollection { // MARK: - ViewTraitCollection + transition -struct TransitionTraitKey: _ViewTraitKey { - static var defaultValue: AnyTransition { .opacity } -} - extension ViewTraitCollection { package var transition: AnyTransition { self[TransitionTraitKey.self] diff --git a/Tests/OpenSwiftUICompatibilityTests/Modifier/ViewModifier/AppearanceActionModifierCompatibilityTests.swift b/Tests/OpenSwiftUICompatibilityTests/Modifier/ViewModifier/AppearanceActionModifierCompatibilityTests.swift index 2e2b7835e..1add71aaa 100644 --- a/Tests/OpenSwiftUICompatibilityTests/Modifier/ViewModifier/AppearanceActionModifierCompatibilityTests.swift +++ b/Tests/OpenSwiftUICompatibilityTests/Modifier/ViewModifier/AppearanceActionModifierCompatibilityTests.swift @@ -73,4 +73,49 @@ struct AppearanceActionModifierCompatibilityTests { } await #expect(Helper.result == "AFATDTDT") } + + @Test("idTest with identity transition") + func idTestWithIdentityTransition() async throws { + enum Helper { + @MainActor + static var result = "" + } + + struct ContentView: View { + @State private var toggle = false + var continuation: UnsafeContinuation + + var body: some View { + Color.red + .onAppear { + if Helper.result.isEmpty { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + toggle.toggle() + } + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + continuation.resume() + } + } + Helper.result += "A" + Helper.result += toggle ? "T" : "F" + } + .onDisappear { + Helper.result += "D" + Helper.result += toggle ? "T" : "F" + } + .transition(.identity) + .id(toggle) + } + } + + try await triggerLayoutWithWindow { continuation in + PlatformHostingController( + rootView: ContentView( + continuation: continuation + ) + ) + } + await #expect(Helper.result == "AFDTATDT") + } }