Skip to content

Commit c6e6fbd

Browse files
authored
[Bug Fix] Fix async render issue (#707)
1 parent 6aee157 commit c6e6fbd

File tree

5 files changed

+164
-16
lines changed

5 files changed

+164
-16
lines changed

Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView+Extension.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,26 @@ extension _UIHostingView: ViewRendererHost {
242242

243243
// MARK: - ViewRendererHost conformance
244244

245+
package var currentTimestamp: Time {
246+
get { base.currentTimestamp }
247+
set { base.currentTimestamp = newValue }
248+
}
249+
250+
package var propertiesNeedingUpdate: ViewRendererHostProperties {
251+
get { base.propertiesNeedingUpdate }
252+
set { base.propertiesNeedingUpdate = newValue }
253+
}
254+
255+
package var renderingPhase: ViewRenderingPhase {
256+
get { base.renderingPhase }
257+
set { base.renderingPhase = newValue }
258+
}
259+
260+
package var externalUpdateCount: Int {
261+
get { base.externalUpdateCount }
262+
set { base.externalUpdateCount = newValue }
263+
}
264+
245265
package func updateRootView() {
246266
let rootView = makeRootView()
247267
viewGraph.setRootView(rootView)

Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,6 @@ open class _UIHostingView<Content>: UIView, XcodeViewDebugDataProvider where Con
4848
final package let renderer = DisplayList.ViewRenderer(platform: .init(definition: UIViewPlatformViewDefinition.self))
4949

5050
// final package let eventBindingManager: EventBindingManager
51-
52-
package var currentTimestamp: Time = .zero
53-
54-
package var propertiesNeedingUpdate: ViewRendererHostProperties = .all
55-
56-
package var renderingPhase: ViewRenderingPhase {
57-
get { base.renderingPhase }
58-
set { base.renderingPhase = newValue }
59-
}
60-
61-
package var externalUpdateCount: Int {
62-
get { base.externalUpdateCount }
63-
set { base.externalUpdateCount = newValue }
64-
}
6551

6652
var allowUIKitAnimations: Int32 = .zero
6753

Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingViewBase.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ package class UIHostingViewBase {
458458
if let renderedTime {
459459
if renderedTime.seconds.isFinite {
460460
let delay = max(renderedTime - currentTimestamp, 1e-6)
461-
requestUpdate(after: delay)
461+
host.requestUpdate(after: delay)
462462
}
463463
if viewGraph.updateRequiredMainThread {
464464
displayLink?.cancelAsyncRendering()
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Async Rendering in UIKit Integration
2+
3+
Understand how OpenSwiftUI performs asynchronous rendering when hosted in UIKit views.
4+
5+
## Overview
6+
7+
OpenSwiftUI can perform view graph updates and display list rendering on a background thread to improve frame rates and reduce main thread blocking. This async rendering capability is particularly important for smooth animations.
8+
9+
## Architecture
10+
11+
The async rendering pipeline involves several key components:
12+
13+
1. DisplayLink: Manages the render loop and switches between main thread and async thread rendering
14+
2. ViewGraph: Provides `updateOutputsAsync` to update the view graph on a background thread
15+
3. UIHostingViewBase: Coordinates the rendering process and manages the display link
16+
4. ViewRendererHost: Protocol that provides `render` and `renderAsync` methods
17+
18+
## How Async Rendering Works
19+
20+
### The Rendering Flow
21+
22+
When a view needs to update, the following sequence occurs:
23+
24+
1. `DisplayLink` fires on a CADisplayLink callback
25+
2. `UIHostingViewBase.displayLinkTimer` is called with the current timestamp
26+
3. If `isAsyncThread` is true, `ViewRendererHost.renderAsync` is invoked
27+
4. `ViewGraph.updateOutputsAsync` attempts to update outputs on the async thread
28+
5. If successful, the display list is rendered asynchronously
29+
6. If async update fails, rendering falls back to the main thread
30+
31+
### The Async Thread
32+
33+
OpenSwiftUI creates a dedicated async rendering thread:
34+
35+
thread.name = "org.OpenSwiftUIProject.OpenSwiftUI.AsyncRenderer"
36+
thread.qualityOfService = .userInteractive
37+
38+
This thread runs a separate RunLoop and processes display link callbacks when async rendering is enabled.
39+
40+
## Conditions for Async Rendering
41+
42+
Async rendering is only possible when specific conditions are met. The `updateOutputsAsync` method checks:
43+
44+
guard _rootDisplayList.allowsAsyncUpdate(),
45+
hostPreferenceValues.allowsAsyncUpdate(),
46+
sizeThatFitsObservers.isEmpty || _rootLayoutComputer.allowsAsyncUpdate()
47+
else {
48+
return nil
49+
}
50+
51+
### Key Requirements
52+
53+
1. hostPreferenceValues must be non-nil: This attribute is set during `instantiateOutputs` when the view contains dynamic containers like `ForEach` or conditional views (`if`/`else`)
54+
55+
2. Attributes must allow async updates: An attribute allows async update when its value state does not contain both `dirty` and `mainThread` flags
56+
57+
3. No pending properties needing update: The host's `propertiesNeedingUpdate` must be empty
58+
59+
4. No pending transactions: The view graph must not have pending transactions
60+
61+
### The hostPreferenceValues Requirement
62+
63+
The `hostPreferenceValues` is set during `ViewGraph.instantiateOutputs`:
64+
65+
hostPreferenceValues = WeakAttribute(outputs.preferences[HostPreferencesKey.self])
66+
67+
This only works when the view hierarchy contains a `DynamicContainer`. Dynamic containers are created by:
68+
69+
- `ForEach` views
70+
- Conditional content (`if`/`else` statements)
71+
- Optional view unwrapping
72+
73+
Without these dynamic elements, `hostPreferenceValues` remains nil and `allowsAsyncUpdate()` returns false.
74+
75+
## Examples
76+
77+
### Views That Support Async Rendering
78+
79+
Views with dynamic content like `ForEach` enable async rendering:
80+
81+
struct AsyncRenderExample: View {
82+
@State private var items = [6]
83+
84+
var body: some View {
85+
VStack(spacing: 10) {
86+
ForEach(items, id: \.self) { item in
87+
Color.blue.opacity(Double(item) / 6.0)
88+
.frame(height: 50)
89+
.transition(.slide)
90+
}
91+
}
92+
.animation(.easeInOut(duration: 2), value: items)
93+
.onAppear {
94+
items.removeAll { $0 == 6 }
95+
}
96+
}
97+
}
98+
99+
In this example, the `ForEach` creates a `DynamicContainer`, which sets up the `hostPreferenceValues` attribute, enabling async rendering during the animation.
100+
101+
### Views That Cannot Use Async Rendering
102+
103+
Views without dynamic containers cannot use async rendering:
104+
105+
struct NoAsyncRenderExample: View {
106+
@State private var showRed = false
107+
108+
var body: some View {
109+
VStack {
110+
Color(platformColor: showRed ? .red : .blue)
111+
.onAppear {
112+
let animation = Animation.linear(duration: 5)
113+
.logicallyComplete(after: 1)
114+
withAnimation(animation, completionCriteria: .logicallyComplete) {
115+
showRed.toggle()
116+
} completion: {
117+
print("Complete")
118+
}
119+
}
120+
}
121+
}
122+
}
123+
124+
This view has no `ForEach` or conditional view structure. The color interpolation happens within a single view, so no `DynamicContainer` is created and `hostPreferenceValues` remains nil.
125+
126+
## Debugging Tips
127+
128+
To understand why async rendering is not enabled:
129+
130+
1. Check if your view hierarchy contains `ForEach` or conditional views
131+
2. Verify that animations are not using completion handlers that require main thread coordination
132+
3. Use the environment variable `OPENSWIFTUI_PRINT_TREE=1` to inspect the display list structure
133+
134+
## Topics
135+
136+
### Related Types
137+
138+
- ``_UIHostingView``
139+
- ``ViewGraph``
140+

Sources/OpenSwiftUICore/Graph/GraphHost.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,8 @@ extension GraphHost {
529529
}
530530
}
531531

532+
// MARK: GraphHost + preference [6.5.4]
533+
532534
@_spi(ForOpenSwiftUIOnly)
533535
extension GraphHost {
534536
package final func addPreference<K>(_ key: K.Type) where K: HostPreferenceKey {
@@ -561,7 +563,7 @@ extension GraphHost {
561563
package final func updatePreferences() -> Bool {
562564
let seed = hostPreferenceValues.value?.seed ?? .empty
563565
let lastSeed = lastHostPreferencesSeed
564-
let didUpdate = !seed.isInvalid || lastSeed.isInvalid || (seed.value != lastSeed.value)
566+
let didUpdate = !seed.isInvalid && !lastSeed.isInvalid && (seed.value != lastSeed.value)
565567
lastHostPreferencesSeed = seed
566568
return didUpdate
567569
}

0 commit comments

Comments
 (0)