diff --git a/Example/Shared/Render/Async/AsyncRenderExample.swift b/Example/Shared/Render/Async/AsyncRenderExample.swift deleted file mode 100644 index d5c610ea1..000000000 --- a/Example/Shared/Render/Async/AsyncRenderExample.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// GeometryEffectExample.swift -// Shared - -#if OPENSWIFTUI -import OpenSwiftUI -#else -import SwiftUI -#endif - -struct AsyncRenderExample: View { - @State private var items = [6] - - var body: some View { - VStack(spacing: 10) { - ForEach(items, id: \.self) { item in - Color.blue.opacity(Double(item) / 6.0) - .frame(height: 50) - .transition(.slide) - } - } - .animation(.easeInOut(duration: 2), value: items) - .onAppear { - items.removeAll { $0 == 6 } - } - } -} diff --git a/Example/Shared/Render/Async/AsyncRendererExample.swift b/Example/Shared/Render/Async/AsyncRendererExample.swift new file mode 100644 index 000000000..f6d83d186 --- /dev/null +++ b/Example/Shared/Render/Async/AsyncRendererExample.swift @@ -0,0 +1,59 @@ +// +// AsyncRendererExample.swift +// Shared + +#if OPENSWIFTUI +import OpenSwiftUI +#else +import SwiftUI +#endif + +struct AsyncRendererExample: View { + @State private var items = [6] + + var body: some View { + VStack(spacing: 10) { + ForEach(items, id: \.self) { item in + Color.blue.opacity(Double(item) / 6.0) + .frame(height: 50) + .transition(.slide) + } + } + .animation(.easeInOut(duration: 2), value: items) + .onAppear { + items.removeAll { $0 == 6 } + } + } +} + +struct AsyncRendererTransitionExample: View { + @State private var isVisible: Bool = false + + var body: some View { + Group { + if isVisible { + Color.red + .frame(width: 80, height: 80) + .clipShape(RoundedRectangle(cornerRadius: 0)) + .transition(RenderCrashTransition()) + } + } + .frame(width: 200, height: 200) + .animation(.linear(duration: 1), value: isVisible) + .onAppear { + guard !isVisible else { + return + } + isVisible = true + } + } + + // This custom transition forces the async renderer to update inherited view + // content while a transition phase is changing. It covers the path that used + // to trip Swift's exclusivity checks in DisplayList.ViewUpdater. + private struct RenderCrashTransition: Transition { + func body(content: Content, phase: TransitionPhase) -> some View { + content.opacity(phase.isIdentity ? 1 : 0.1) + } + } +} diff --git a/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift b/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift index 92dddca72..12e17c7c3 100644 --- a/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift +++ b/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift @@ -859,8 +859,25 @@ extension NSHostingView { } public func _renderAsyncForTest(interval: Double) -> Bool { - _openSwiftUIUnimplementedWarning() - return false + advanceTimeForTest(interval: interval) + canAdvanceTimeAutomatically = false + var result = true + repeat { + RunLoop.flushObservers() + let didRender = Update.locked { + renderAsync(targetTimestamp: nil) != nil + } + if didRender { + CATransaction.flush() + if result { + result = !viewGraph.updateRequiredMainThread + } + } else { + result = false + } + } while !propertiesNeedingUpdate.isEmpty + canAdvanceTimeAutomatically = true + return result } } diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewUpdater.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewUpdater.swift index a33054665..47cd9bb18 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewUpdater.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewUpdater.swift @@ -393,7 +393,7 @@ extension DisplayList { newItem: newItem, newState: newStatePtr, tag: .inherited - ) { layer, _, oldItem, oldState, newItem, newState in + ) { [platform] layer, _, oldItem, oldState, newItem, newState in platform.updateInheritedLayerAsync( layer: &layer, oldItem: oldItem,