@swiftbrowser/swift 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (393) hide show
  1. package/Package.swift +200 -9
  2. package/README.md +7 -9
  3. package/Sources/AVFoundation/AVFoundation.swift +131 -0
  4. package/Sources/AVFoundation/Asset.swift +487 -0
  5. package/Sources/AVFoundation/AudioSession.swift +144 -0
  6. package/Sources/AVFoundation/CMTime.swift +303 -0
  7. package/Sources/AVFoundation/Capture.swift +688 -0
  8. package/Sources/AVFoundation/Player.swift +749 -0
  9. package/Sources/AVFoundation/PlayerView.swift +117 -0
  10. package/Sources/AVKit/AVKit.swift +132 -0
  11. package/Sources/Accelerate/Accelerate.swift +122 -0
  12. package/Sources/AppIntents/AppIntents.swift +144 -0
  13. package/Sources/AppIntents/Parameters.swift +220 -0
  14. package/Sources/AppIntents/Shortcuts.swift +159 -0
  15. package/Sources/AudioToolbox/AudioToolbox.swift +55 -0
  16. package/Sources/AuthenticationServices/AuthenticationServices.swift +257 -0
  17. package/Sources/AuthenticationServices/Authorization.swift +251 -0
  18. package/Sources/Charts/Charts.swift +0 -4
  19. package/Sources/Combine/AnyCancellable.swift +65 -0
  20. package/Sources/Combine/AnyPublisher.swift +148 -0
  21. package/Sources/Combine/AnySubscriber.swift +230 -0
  22. package/Sources/Combine/Cancellable.swift +35 -0
  23. package/Sources/Combine/Codable.swift +29 -0
  24. package/Sources/Combine/CombineIdentifier.swift +45 -0
  25. package/Sources/Combine/Concurrency/ConcurrencyHelpers.swift +24 -0
  26. package/Sources/Combine/Concurrency/Future+Concurrency.swift +136 -0
  27. package/Sources/Combine/Concurrency/GENERATED-Publisher+Concurrency.swift +397 -0
  28. package/Sources/Combine/CurrentValueSubject.swift +252 -0
  29. package/Sources/Combine/CustomCombineIdentifierConvertible.swift +28 -0
  30. package/Sources/Combine/Future.swift +228 -0
  31. package/Sources/Combine/GENERATED-RootProtocols.swift +426 -0
  32. package/Sources/Combine/Helpers/ConduitBase.swift +40 -0
  33. package/Sources/Combine/Helpers/ConduitList.swift +59 -0
  34. package/Sources/Combine/Helpers/DebugHook.swift +177 -0
  35. package/Sources/Combine/Helpers/FilterProducer.swift +174 -0
  36. package/Sources/Combine/Helpers/Locking.swift +30 -0
  37. package/Sources/Combine/Helpers/PartialCompletion.swift +26 -0
  38. package/Sources/Combine/Helpers/ReduceProducer.swift +249 -0
  39. package/Sources/Combine/Helpers/SubjectSubscriber.swift +96 -0
  40. package/Sources/Combine/Helpers/SubscriberTap.swift +69 -0
  41. package/Sources/Combine/Helpers/SubscriptionStatus.swift +33 -0
  42. package/Sources/Combine/Helpers/SubscriptionTap.swift +33 -0
  43. package/Sources/Combine/Helpers/Utils.swift +30 -0
  44. package/Sources/Combine/Helpers/Violations.swift +33 -0
  45. package/Sources/Combine/ImmediateScheduler.swift +178 -0
  46. package/Sources/Combine/LICENSE +21 -0
  47. package/Sources/Combine/NotificationPublisher.swift +48 -0
  48. package/Sources/Combine/ObjectWillChange.swift +46 -0
  49. package/Sources/Combine/PassthroughSubject.swift +222 -0
  50. package/Sources/Combine/Publisher+Subscribe.swift +53 -0
  51. package/Sources/Combine/Publishers/Deferred.swift +48 -0
  52. package/Sources/Combine/Publishers/Empty.swift +54 -0
  53. package/Sources/Combine/Publishers/Fail.swift +42 -0
  54. package/Sources/Combine/Publishers/GENERATED-Publishers.Catch.swift +673 -0
  55. package/Sources/Combine/Publishers/GENERATED-Publishers.Encode.swift +388 -0
  56. package/Sources/Combine/Publishers/GENERATED-Publishers.MapKeyPath.swift +384 -0
  57. package/Sources/Combine/Publishers/Just.swift +313 -0
  58. package/Sources/Combine/Publishers/Optional.Publisher.swift +315 -0
  59. package/Sources/Combine/Publishers/Publishers.AllSatisfy.swift +217 -0
  60. package/Sources/Combine/Publishers/Publishers.AssertNoFailure.swift +158 -0
  61. package/Sources/Combine/Publishers/Publishers.Autoconnect.swift +193 -0
  62. package/Sources/Combine/Publishers/Publishers.Breakpoint.swift +231 -0
  63. package/Sources/Combine/Publishers/Publishers.Buffer.swift +356 -0
  64. package/Sources/Combine/Publishers/Publishers.Collect.swift +102 -0
  65. package/Sources/Combine/Publishers/Publishers.CollectByCount.swift +195 -0
  66. package/Sources/Combine/Publishers/Publishers.CompactMap.swift +241 -0
  67. package/Sources/Combine/Publishers/Publishers.Comparison.swift +371 -0
  68. package/Sources/Combine/Publishers/Publishers.Concatenate.swift +474 -0
  69. package/Sources/Combine/Publishers/Publishers.Contains.swift +293 -0
  70. package/Sources/Combine/Publishers/Publishers.Count.swift +87 -0
  71. package/Sources/Combine/Publishers/Publishers.Debounce.swift +332 -0
  72. package/Sources/Combine/Publishers/Publishers.Delay.swift +269 -0
  73. package/Sources/Combine/Publishers/Publishers.Drop.swift +155 -0
  74. package/Sources/Combine/Publishers/Publishers.DropUntilOutput.swift +283 -0
  75. package/Sources/Combine/Publishers/Publishers.DropWhile.swift +378 -0
  76. package/Sources/Combine/Publishers/Publishers.Filter.swift +248 -0
  77. package/Sources/Combine/Publishers/Publishers.First.swift +261 -0
  78. package/Sources/Combine/Publishers/Publishers.FlatMap.swift +533 -0
  79. package/Sources/Combine/Publishers/Publishers.HandleEvents.swift +260 -0
  80. package/Sources/Combine/Publishers/Publishers.IgnoreOutput.swift +109 -0
  81. package/Sources/Combine/Publishers/Publishers.Last.swift +252 -0
  82. package/Sources/Combine/Publishers/Publishers.MakeConnectable.swift +82 -0
  83. package/Sources/Combine/Publishers/Publishers.Map.swift +351 -0
  84. package/Sources/Combine/Publishers/Publishers.MapError.swift +140 -0
  85. package/Sources/Combine/Publishers/Publishers.MeasureInterval.swift +187 -0
  86. package/Sources/Combine/Publishers/Publishers.Multicast.swift +284 -0
  87. package/Sources/Combine/Publishers/Publishers.Output.swift +241 -0
  88. package/Sources/Combine/Publishers/Publishers.PrefixUntilOutput.swift +197 -0
  89. package/Sources/Combine/Publishers/Publishers.PrefixWhile.swift +176 -0
  90. package/Sources/Combine/Publishers/Publishers.Print.swift +206 -0
  91. package/Sources/Combine/Publishers/Publishers.ReceiveOn.swift +227 -0
  92. package/Sources/Combine/Publishers/Publishers.Reduce.swift +204 -0
  93. package/Sources/Combine/Publishers/Publishers.RemoveDuplicates.swift +264 -0
  94. package/Sources/Combine/Publishers/Publishers.ReplaceEmpty.swift +201 -0
  95. package/Sources/Combine/Publishers/Publishers.ReplaceError.swift +211 -0
  96. package/Sources/Combine/Publishers/Publishers.Retry.swift +274 -0
  97. package/Sources/Combine/Publishers/Publishers.Scan.swift +331 -0
  98. package/Sources/Combine/Publishers/Publishers.Sequence.swift +437 -0
  99. package/Sources/Combine/Publishers/Publishers.SetFailureType.swift +120 -0
  100. package/Sources/Combine/Publishers/Publishers.Share.swift +104 -0
  101. package/Sources/Combine/Publishers/Publishers.SubscribeOn.swift +197 -0
  102. package/Sources/Combine/Publishers/Publishers.SwitchToLatest.swift +444 -0
  103. package/Sources/Combine/Publishers/Publishers.Throttle.swift +359 -0
  104. package/Sources/Combine/Publishers/Publishers.Timeout.swift +301 -0
  105. package/Sources/Combine/Publishers/Publishers.Zip.swift +720 -0
  106. package/Sources/Combine/Publishers/Publishers.swift +13 -0
  107. package/Sources/Combine/Publishers/Record.swift +237 -0
  108. package/Sources/Combine/Publishers/Result.Publisher.swift +337 -0
  109. package/Sources/Combine/Result.swift +46 -0
  110. package/Sources/Combine/Scheduler.swift +89 -0
  111. package/Sources/Combine/Schedulers.swift +211 -0
  112. package/Sources/Combine/Subject+Void.swift +17 -0
  113. package/Sources/Combine/Subscriber+Void.swift +20 -0
  114. package/Sources/Combine/Subscribers/Subscribers.Assign.swift +175 -0
  115. package/Sources/Combine/Subscribers/Subscribers.Completion.swift +90 -0
  116. package/Sources/Combine/Subscribers/Subscribers.Demand.swift +476 -0
  117. package/Sources/Combine/Subscribers/Subscribers.Sink.swift +206 -0
  118. package/Sources/Combine/Subscribers/Subscribers.swift +9 -0
  119. package/Sources/Combine/Subscription.swift +22 -0
  120. package/Sources/Combine/Subscriptions.swift +42 -0
  121. package/Sources/Combine/SwiftUIObservation.swift +10 -0
  122. package/Sources/Combine/_Introspection.swift +107 -0
  123. package/Sources/CommonCrypto/CommonCrypto.swift +80 -0
  124. package/Sources/Compression/Compression.swift +365 -0
  125. package/Sources/Compression/Deflate.swift +407 -0
  126. package/Sources/Compression/Zlib.swift +179 -0
  127. package/Sources/CoreFoundationTypes/CoreFoundationTypes.swift +101 -0
  128. package/Sources/CoreGraphics/CoreGraphics.swift +411 -0
  129. package/Sources/CoreGraphics/ProjectionTransforms.swift +28 -0
  130. package/Sources/CoreHaptics/CoreHaptics.swift +399 -0
  131. package/Sources/CoreImage/CIColor.swift +41 -0
  132. package/Sources/CoreImage/CIContext.swift +31 -0
  133. package/Sources/CoreImage/CIDetector.swift +57 -0
  134. package/Sources/CoreImage/CIFilter.swift +243 -0
  135. package/Sources/CoreImage/CIFilterBuiltins.swift +136 -0
  136. package/Sources/CoreImage/CIImage.swift +162 -0
  137. package/Sources/CoreImage/QRCode.swift +393 -0
  138. package/Sources/CoreLocation/Coordinate.swift +30 -0
  139. package/Sources/CoreLocation/Location.swift +65 -0
  140. package/Sources/CoreML/Evaluation.swift +414 -0
  141. package/Sources/CoreML/FeatureValue.swift +266 -0
  142. package/Sources/CoreML/Model.swift +269 -0
  143. package/Sources/CoreML/MultiArray.swift +92 -0
  144. package/Sources/CoreML/Specification.swift +233 -0
  145. package/Sources/CoreText/CoreText.swift +64 -0
  146. package/Sources/CoreTransferable/CoreTransferable.swift +606 -0
  147. package/Sources/CryptoKit/ChaChaPoly.swift +258 -0
  148. package/Sources/CryptoKit/CryptoKit.swift +375 -0
  149. package/Sources/CryptoKit/Curve25519.swift +317 -0
  150. package/Sources/CryptoKit/P256.swift +321 -0
  151. package/Sources/FoundationBridge/AppStorage.swift +165 -0
  152. package/Sources/FoundationBridge/AssetResources.swift +90 -0
  153. package/Sources/FoundationBridge/AsyncBytes.swift +128 -0
  154. package/Sources/FoundationBridge/AsyncImage.swift +116 -0
  155. package/Sources/FoundationBridge/AttributedStrings.swift +16 -0
  156. package/Sources/{_FoundationCommon → FoundationBridge}/Bundle.swift +41 -4
  157. package/Sources/FoundationBridge/CombineExports.swift +8 -0
  158. package/Sources/FoundationBridge/CoreFoundation.swift +77 -0
  159. package/Sources/FoundationBridge/Dispatch.swift +458 -0
  160. package/Sources/FoundationBridge/EnvironmentFoundation.swift +41 -0
  161. package/Sources/FoundationBridge/FileImporter.swift +128 -0
  162. package/Sources/FoundationBridge/FileSystem.swift +68 -0
  163. package/Sources/FoundationBridge/FoundationBridge.swift +24 -72
  164. package/Sources/FoundationBridge/HostResolution.swift +31 -0
  165. package/Sources/FoundationBridge/ItemProvider.swift +655 -0
  166. package/Sources/FoundationBridge/KeyValueObserving.swift +138 -0
  167. package/Sources/FoundationBridge/Links.swift +127 -0
  168. package/Sources/FoundationBridge/ListFormatter.swift +32 -0
  169. package/Sources/FoundationBridge/Localization.swift +149 -0
  170. package/Sources/FoundationBridge/Locks.swift +50 -0
  171. package/Sources/FoundationBridge/Markdown.swift +527 -0
  172. package/Sources/FoundationBridge/MeasurementFormat.swift +95 -0
  173. package/Sources/FoundationBridge/NotificationCenter.swift +154 -0
  174. package/Sources/FoundationBridge/OpenURLHandlers.swift +60 -0
  175. package/Sources/FoundationBridge/Operation.swift +195 -0
  176. package/Sources/FoundationBridge/ProcessInfo.swift +32 -0
  177. package/Sources/FoundationBridge/RelativeDateTimeFormatter.swift +136 -0
  178. package/Sources/FoundationBridge/SceneStorage.swift +78 -0
  179. package/Sources/FoundationBridge/SecurityExports.swift +8 -0
  180. package/Sources/FoundationBridge/Selector.swift +226 -0
  181. package/Sources/FoundationBridge/StringComparators.swift +17 -0
  182. package/Sources/FoundationBridge/Table.swift +211 -0
  183. package/Sources/FoundationBridge/TextAttributes.swift +42 -0
  184. package/Sources/FoundationBridge/Timer.swift +6 -1
  185. package/Sources/FoundationBridge/URLSession.swift +718 -0
  186. package/Sources/{_FoundationCommon → FoundationBridge}/UserDefaults.swift +17 -4
  187. package/Sources/FoundationBridge/WebAuthRedirects.swift +227 -0
  188. package/Sources/FoundationBridge/WebSocket.swift +187 -0
  189. package/Sources/FoundationModels/FoundationModels.swift +1098 -0
  190. package/Sources/ImageIO/GIF.swift +72 -0
  191. package/Sources/ImageIO/ImageIO.swift +248 -0
  192. package/Sources/ImageIO/ImageTypes.swift +28 -0
  193. package/Sources/LinkPresentation/LinkPresentation.swift +249 -0
  194. package/Sources/LocalAuthentication/LAContext.swift +86 -0
  195. package/Sources/MapKit/Geometry.swift +223 -0
  196. package/Sources/MapKit/Map.swift +638 -0
  197. package/Sources/MapKit/MapView.swift +229 -0
  198. package/Sources/MobileCoreServices/MobileCoreServices.swift +28 -0
  199. package/Sources/NaturalLanguage/LanguageIdentifier.swift +317 -0
  200. package/Sources/NaturalLanguage/NaturalLanguage.swift +135 -0
  201. package/Sources/OSLog/OSLog.swift +6 -0
  202. package/Sources/Photos/Photos.swift +572 -0
  203. package/Sources/PhotosUI/PhotosPicker.swift +248 -0
  204. package/Sources/PhotosUI/PhotosPickerItem.swift +261 -0
  205. package/Sources/QuickLook/QuickLook.swift +168 -0
  206. package/Sources/SafariServices/SafariServices.swift +106 -0
  207. package/Sources/Security/Security.swift +332 -0
  208. package/Sources/StoreKit/AppStore.swift +207 -0
  209. package/Sources/StoreKit/Products.swift +280 -0
  210. package/Sources/StoreKit/RequestReview.swift +28 -0
  211. package/Sources/StoreKit/StoreKit1.swift +486 -0
  212. package/Sources/StoreKit/StoreViews.swift +428 -0
  213. package/Sources/StoreKit/Transactions.swift +197 -0
  214. package/Sources/SwiftData/Macros.swift +58 -0
  215. package/Sources/SwiftData/ModelActor.swift +38 -0
  216. package/Sources/SwiftData/ModelContainer.swift +129 -0
  217. package/Sources/SwiftData/ModelContext.swift +451 -0
  218. package/Sources/SwiftData/PersistentModel.swift +238 -0
  219. package/Sources/SwiftData/Properties.swift +261 -0
  220. package/Sources/SwiftData/Query.swift +256 -0
  221. package/Sources/SwiftData/Schema.swift +192 -0
  222. package/Sources/SwiftData/Store.swift +146 -0
  223. package/Sources/SwiftData/StringPredicates.swift +125 -0
  224. package/Sources/SwiftDataMacros/EntryMacro.swift +82 -0
  225. package/Sources/SwiftDataMacros/GenerableMacro.swift +183 -0
  226. package/Sources/SwiftDataMacros/ModelActorMacro.swift +46 -0
  227. package/Sources/SwiftDataMacros/ModelMacro.swift +260 -0
  228. package/Sources/SwiftDataMacros/Plugin.swift +18 -0
  229. package/Sources/SwiftDataMacros/PredicateMacro.swift +1244 -0
  230. package/Sources/SwiftUI/API/Accessibility.swift +55 -6
  231. package/Sources/SwiftUI/API/AccessibilityExtras.swift +33 -0
  232. package/Sources/SwiftUI/API/AccessibilityFocus.swift +63 -0
  233. package/Sources/SwiftUI/API/Alert.swift +1 -1
  234. package/Sources/SwiftUI/API/AlignmentGuides.swift +65 -0
  235. package/Sources/SwiftUI/API/Animation.swift +37 -5
  236. package/Sources/SwiftUI/API/App.swift +31 -1
  237. package/Sources/SwiftUI/API/BindingCollections.swift +35 -0
  238. package/Sources/SwiftUI/API/BindingForEach.swift +27 -0
  239. package/Sources/SwiftUI/API/Bitmaps.swift +254 -0
  240. package/Sources/SwiftUI/API/ButtonStyles.swift +96 -0
  241. package/Sources/SwiftUI/API/Canvas.swift +337 -0
  242. package/Sources/SwiftUI/API/Color.swift +104 -28
  243. package/Sources/SwiftUI/API/ColorEffects.swift +52 -0
  244. package/Sources/SwiftUI/API/ColorPickerAndLabeledContent.swift +138 -0
  245. package/Sources/SwiftUI/API/ColorSpaces.swift +161 -0
  246. package/Sources/SwiftUI/API/Commands.swift +106 -0
  247. package/Sources/SwiftUI/API/ContainerBackground.swift +28 -0
  248. package/Sources/SwiftUI/API/ContentShape.swift +33 -0
  249. package/Sources/SwiftUI/API/ContentSizeCategory.swift +35 -0
  250. package/Sources/SwiftUI/API/ContentUnavailable.swift +101 -0
  251. package/Sources/SwiftUI/API/Controls.swift +59 -27
  252. package/Sources/SwiftUI/API/CustomLayout.swift +49 -10
  253. package/Sources/SwiftUI/API/CustomToolbarContent.swift +46 -0
  254. package/Sources/SwiftUI/API/Decorations.swift +1 -1
  255. package/Sources/SwiftUI/API/EditMode.swift +25 -82
  256. package/Sources/SwiftUI/API/Effects.swift +105 -7
  257. package/Sources/SwiftUI/API/Entry.swift +8 -0
  258. package/Sources/SwiftUI/API/Environment.swift +93 -9
  259. package/Sources/SwiftUI/API/EnvironmentSendable.swift +6 -0
  260. package/Sources/SwiftUI/API/Focus.swift +11 -3
  261. package/Sources/SwiftUI/API/Gauge.swift +3 -2
  262. package/Sources/SwiftUI/API/GeometryEffects.swift +232 -0
  263. package/Sources/SwiftUI/API/GeometryReader.swift +13 -2
  264. package/Sources/SwiftUI/API/GestureState.swift +65 -0
  265. package/Sources/SwiftUI/API/Gestures.swift +1 -1
  266. package/Sources/SwiftUI/API/Glass.swift +127 -0
  267. package/Sources/SwiftUI/API/Grid.swift +1 -1
  268. package/Sources/SwiftUI/API/GroupBoxAndControlGroup.swift +130 -0
  269. package/Sources/SwiftUI/API/KeyPress.swift +54 -0
  270. package/Sources/SwiftUI/API/LabelStyle.swift +13 -6
  271. package/Sources/SwiftUI/API/Layout.swift +67 -12
  272. package/Sources/SwiftUI/API/LazyStacks.swift +49 -0
  273. package/Sources/SwiftUI/API/LegacyAlert.swift +97 -0
  274. package/Sources/SwiftUI/API/Lists.swift +61 -6
  275. package/Sources/SwiftUI/API/Menu.swift +8 -1
  276. package/Sources/SwiftUI/API/Modifiers.swift +80 -10
  277. package/Sources/SwiftUI/API/MoreModifiers.swift +100 -0
  278. package/Sources/SwiftUI/API/Namespace.swift +44 -0
  279. package/Sources/SwiftUI/API/Navigation.swift +24 -9
  280. package/Sources/SwiftUI/API/NavigationDestinationBindings.swift +55 -0
  281. package/Sources/SwiftUI/API/ObservableObject.swift +16 -2
  282. package/Sources/SwiftUI/API/Path.swift +543 -0
  283. package/Sources/SwiftUI/API/Phase8Controls.swift +48 -31
  284. package/Sources/SwiftUI/API/PhaseAnimator.swift +130 -0
  285. package/Sources/SwiftUI/API/Picker.swift +11 -2
  286. package/Sources/SwiftUI/API/PointerAndKeyboard.swift +77 -0
  287. package/Sources/SwiftUI/API/Popover.swift +41 -0
  288. package/Sources/SwiftUI/API/Preferences.swift +262 -0
  289. package/Sources/SwiftUI/API/Presentation.swift +17 -4
  290. package/Sources/SwiftUI/API/PresentationExtras.swift +60 -0
  291. package/Sources/SwiftUI/API/Primitives.swift +164 -37
  292. package/Sources/SwiftUI/API/Publishers.swift +4 -4
  293. package/Sources/SwiftUI/API/Redaction.swift +75 -0
  294. package/Sources/SwiftUI/API/RowActions.swift +3 -2
  295. package/Sources/SwiftUI/API/Scenes.swift +131 -0
  296. package/Sources/SwiftUI/API/ScrollModifiers.swift +132 -0
  297. package/Sources/SwiftUI/API/ScrollTargets.swift +3 -3
  298. package/Sources/SwiftUI/API/ScrollViewReader.swift +144 -0
  299. package/Sources/SwiftUI/API/SectionSupplements.swift +96 -0
  300. package/Sources/SwiftUI/API/Shape.swift +214 -0
  301. package/Sources/SwiftUI/API/ShapeStyle.swift +105 -7
  302. package/Sources/SwiftUI/API/ShapeTrim.swift +78 -0
  303. package/Sources/SwiftUI/API/Sharing.swift +193 -0
  304. package/Sources/SwiftUI/API/StackLayouts.swift +199 -0
  305. package/Sources/SwiftUI/API/State.swift +18 -1
  306. package/Sources/SwiftUI/API/StatusGaps.swift +121 -0
  307. package/Sources/SwiftUI/API/SymbolEffects.swift +203 -0
  308. package/Sources/SwiftUI/API/TabContent.swift +841 -0
  309. package/Sources/SwiftUI/API/Table.swift +382 -0
  310. package/Sources/SwiftUI/API/Tabs.swift +148 -4
  311. package/Sources/SwiftUI/API/Tasks.swift +2 -2
  312. package/Sources/SwiftUI/API/Text.swift +386 -0
  313. package/Sources/SwiftUI/API/TextDates.swift +61 -0
  314. package/Sources/SwiftUI/API/TextDecorations.swift +52 -0
  315. package/Sources/SwiftUI/API/TextFieldAxis.swift +103 -0
  316. package/Sources/SwiftUI/API/TextFieldCommit.swift +52 -0
  317. package/Sources/SwiftUI/API/Toolbar.swift +52 -16
  318. package/Sources/SwiftUI/API/Transfer.swift +145 -0
  319. package/Sources/SwiftUI/API/UnevenRoundedRectangle.swift +66 -0
  320. package/Sources/SwiftUI/API/Video.swift +64 -0
  321. package/Sources/SwiftUI/API/View.swift +10 -2
  322. package/Sources/SwiftUI/API/ViewModifier.swift +4 -1
  323. package/Sources/SwiftUI/API/VisualEffect.swift +74 -1
  324. package/Sources/SwiftUI/API/Windows.swift +25 -0
  325. package/Sources/SwiftUI/Charts/Axis.swift +7 -1
  326. package/Sources/SwiftUI/Charts/AxisFormatsAndRanges.swift +68 -0
  327. package/Sources/SwiftUI/Charts/Chart.swift +17 -3
  328. package/Sources/SwiftUI/Charts/ChartProxy.swift +57 -0
  329. package/Sources/SwiftUI/Localization/Localization.swift +498 -0
  330. package/Sources/SwiftUI/Localization/PluralRules.swift +127 -0
  331. package/Sources/SwiftUI/Runtime/Executor.swift +2 -0
  332. package/Sources/SwiftUI/Runtime/Exports.swift +37 -1
  333. package/Sources/SwiftUI/Runtime/ImageSource.swift +310 -0
  334. package/Sources/SwiftUI/Runtime/JSON.swift +76 -4
  335. package/Sources/SwiftUI/Runtime/LazyRows.swift +99 -0
  336. package/Sources/SwiftUI/Runtime/Measurements.swift +140 -0
  337. package/Sources/SwiftUI/Runtime/Ops.swift +58 -12
  338. package/Sources/SwiftUI/Runtime/Reconciler.swift +10 -1
  339. package/Sources/SwiftUI/Runtime/Replay.swift +382 -0
  340. package/Sources/SwiftUI/Runtime/Runtime.swift +977 -198
  341. package/Sources/SwiftUI/Runtime/Sameness.swift +152 -0
  342. package/Sources/SwiftUI/Runtime/ViewPath.swift +89 -0
  343. package/Sources/SwiftUI/Runtime/ViewTraits.swift +175 -0
  344. package/Sources/SwiftUI/Runtime/WebAuthHeadless.swift +49 -0
  345. package/Sources/Symbols/Symbols.swift +25 -0
  346. package/Sources/Translation/Translation.swift +423 -0
  347. package/Sources/UIKit/Activity.swift +213 -0
  348. package/Sources/UIKit/Alerts.swift +69 -0
  349. package/Sources/UIKit/AppDelegate.swift +222 -0
  350. package/Sources/UIKit/Appearance.swift +84 -0
  351. package/Sources/UIKit/ColorConversions.swift +70 -0
  352. package/Sources/UIKit/Controls.swift +225 -0
  353. package/Sources/UIKit/Device.swift +200 -0
  354. package/Sources/UIKit/Feedback.swift +65 -0
  355. package/Sources/UIKit/Font.swift +307 -0
  356. package/Sources/UIKit/Graphics.swift +204 -0
  357. package/Sources/UIKit/HostingController.swift +185 -0
  358. package/Sources/UIKit/Identity.swift +24 -0
  359. package/Sources/UIKit/Introspectable.swift +510 -0
  360. package/Sources/UIKit/Pasteboard.swift +363 -0
  361. package/Sources/UIKit/PhotoAlbum.swift +81 -0
  362. package/Sources/UIKit/RemoteNotifications.swift +15 -0
  363. package/Sources/UIKit/Representable.swift +288 -0
  364. package/Sources/UIKit/SafeArea.swift +12 -0
  365. package/Sources/UIKit/SceneDelegates.swift +132 -0
  366. package/Sources/UIKit/Scenes.swift +40 -0
  367. package/Sources/UIKit/ScrollView.swift +157 -0
  368. package/Sources/UIKit/ShortcutItems.swift +67 -0
  369. package/Sources/UIKit/SymbolConfiguration.swift +124 -0
  370. package/Sources/UIKit/TargetAction.swift +123 -0
  371. package/Sources/UIKit/TextPaste.swift +161 -0
  372. package/Sources/UIKit/TextView.swift +538 -0
  373. package/Sources/UIKit/UIKit.swift +292 -0
  374. package/Sources/UIKit/UIView.swift +855 -0
  375. package/Sources/UIKit/ViewTranslation.swift +311 -0
  376. package/Sources/UniformTypeIdentifiers/UTType.swift +196 -0
  377. package/Sources/UserNotifications/Content.swift +370 -0
  378. package/Sources/UserNotifications/NotificationCenter.swift +309 -0
  379. package/Sources/WeatherKit/WeatherKit.swift +228 -0
  380. package/Sources/WebKit/WebKit.swift +245 -0
  381. package/Sources/os/os.swift +376 -0
  382. package/Sources/zlib/zlib.swift +6 -0
  383. package/index.d.ts +1 -1
  384. package/index.mjs +2 -2
  385. package/manifest.d.mts +2 -0
  386. package/manifest.mjs +39 -0
  387. package/package.json +5 -3
  388. package/sync-sources.mjs +21 -7
  389. package/Sources/FoundationLite/Common.swift +0 -3
  390. package/Sources/FoundationLite/FoundationLite.swift +0 -171
  391. package/Sources/FoundationLite/Interaction.swift +0 -146
  392. package/Sources/FoundationLite/Timer.swift +0 -48
  393. package/Sources/_FoundationCommon/_FoundationCommon.swift +0 -10
@@ -19,8 +19,8 @@ final class _MountedNode {
19
19
  var boxes: [any _AnyStateBox] = []
20
20
  /// Identity within a keyed parent (`ForEach`).
21
21
  var key: AnyHashable?
22
- /// Position-and-type path used to restore state across hot reloads.
23
- var path = ""
22
+ /// Position-and-type path used to restore state across hot reloads (ViewPath.swift).
23
+ var path: _ViewPath?
24
24
  var environment = EnvironmentValues()
25
25
  /// Handler supplied by the runtime (navigation links and screens).
26
26
  var handler: ((_JSON) -> Void)?
@@ -28,9 +28,13 @@ final class _MountedNode {
28
28
  var navDisplayMode: String?
29
29
  var depth = 0
30
30
  var appeared = false
31
+ /// `.onDisappear`: run when the node leaves the tree.
32
+ var disappearAction: (@MainActor () -> Void)?
31
33
  /// `.animation(_:value:)`: the last tracked value and a counter bumped on change.
32
34
  var animationValue: Any?
33
35
  var animationToken = 0
36
+ /// The render pass in which `animationToken` was last bumped.
37
+ var animationTokenPass = -1
34
38
  /// Sheets: the mounted sheet content (second child) and its detents.
35
39
  var sheetDetents: [String]?
36
40
  /// `.task`: the running task and the id it was started for.
@@ -39,6 +43,12 @@ final class _MountedNode {
39
43
  var taskStarted = false
40
44
  /// `GeometryReader`: the size the renderer last reported.
41
45
  var geometrySize = CGSize.zero
46
+ /// A custom shape: the path it drew last (Shape.swift).
47
+ var pathState: _PathState?
48
+ /// The rows a lazily built keyed group has mounted so far (LazyRows.swift).
49
+ var lazyLimit = _LazyRows.batch
50
+ /// A `Menu` or context menu whose items are built (the page opened it).
51
+ var menuOpen = false
42
52
  /// Phase 12: this stack is a `.scrollTargetLayout()`; its props carry `scrollTarget`.
43
53
  var scrollTargetLayout = false
44
54
  /// `TabView` / `Picker`: option tags in order, and the selected index.
@@ -52,6 +62,7 @@ final class _MountedNode {
52
62
  /// each pass while the sidebar is reconciled.
53
63
  var splitSelection: _ListSelection?
54
64
  /// `NavigationStack` nested in another stack's screen: rendered in place.
65
+ /// Also a sidebar `TabView`, which renders its split view in place.
55
66
  var navFlattened = false
56
67
  /// `navscreen` / `List`: the edit mode this node owns (`EditButton` toggles it).
57
68
  var editMode = EditMode.inactive
@@ -76,6 +87,24 @@ final class _MountedNode {
76
87
  var cachedSheets: [_Element] = []
77
88
  var cachedAlerts: [_Element] = []
78
89
 
90
+ // Skipping unchanged views (Phase 16). When an ancestor re-runs, this
91
+ // node gets a new view value; if it is the same as `view` and so is
92
+ // everything the node received from its parent, the node is treated as
93
+ // clean, like SwiftUI does for a view whose stored properties are equal.
94
+ /// What the parent passed down the last time this node was reconciled.
95
+ var received: _ReceivedContext?
96
+ /// This node or a descendant writes to its ancestors while reconciled
97
+ /// (a navigation title, a destination, detents, a list selection, a stack
98
+ /// or tab structure): a parent that re-runs resets what they set and
99
+ /// expects them to set it again, so they are never skipped that way.
100
+ var writesContext = false
101
+
102
+ /// The preference values this subtree produced (see Preferences.swift),
103
+ /// kept for passes that skip it.
104
+ var preferences = _PreferenceValues()
105
+ /// What an `onPreferenceChange` saw last.
106
+ var preferenceSeen: Any?
107
+
79
108
  init(view: any View) {
80
109
  self.view = view
81
110
  }
@@ -150,8 +179,8 @@ final class _NavContext {
150
179
 
151
180
  struct _ReconcileContext {
152
181
  var environment: EnvironmentValues
153
- /// `index:Type/index:Type/...` from the root; keys state snapshots.
154
- var path: String = ""
182
+ /// `Type/index:Type/...` from the root (ViewPath.swift); keys state snapshots.
183
+ var path: _ViewPath
155
184
  var navStack: _NavContext?
156
185
  var screen: _MountedNode?
157
186
  /// The nearest enclosing presented sheet's node (for `presentationDetents`).
@@ -161,6 +190,52 @@ struct _ReconcileContext {
161
190
  var split: _MountedNode?
162
191
  /// Inside a `TabView` tab that is not selected: titles stay put.
163
192
  var inactiveTab = false
193
+ /// Reconciling the rows of a `List` or lazy stack (LazyRows.swift): a
194
+ /// keyed group here builds its rows as they come into view.
195
+ var lazyRows: Axis?
196
+ }
197
+
198
+ /// What a node received from its parent, to tell whether a new pass passes
199
+ /// the same thing (`_MountedNode.received`). Objects are kept by identity.
200
+ struct _ReceivedContext {
201
+ var environment: EnvironmentValues
202
+ var path: _ViewPath
203
+ var navStack: ObjectIdentifier?
204
+ var screen: ObjectIdentifier?
205
+ var sheet: ObjectIdentifier?
206
+ var split: ObjectIdentifier?
207
+ var inactiveTab: Bool
208
+ var lazyRows: Axis?
209
+
210
+ @MainActor init(_ context: _ReconcileContext) {
211
+ environment = context.environment
212
+ path = context.path
213
+ navStack = context.navStack.map(ObjectIdentifier.init)
214
+ screen = context.screen.map(ObjectIdentifier.init)
215
+ sheet = context.sheet.map(ObjectIdentifier.init)
216
+ split = context.split.map(ObjectIdentifier.init)
217
+ inactiveTab = context.inactiveTab
218
+ lazyRows = context.lazyRows
219
+ }
220
+
221
+ /// Same objects, path and tab state, and the same environment.
222
+ @MainActor func matches(_ context: _ReconcileContext) -> Bool {
223
+ path.matches(context.path)
224
+ && navStack == context.navStack.map(ObjectIdentifier.init)
225
+ && screen == context.screen.map(ObjectIdentifier.init)
226
+ && sheet == context.sheet.map(ObjectIdentifier.init)
227
+ && split == context.split.map(ObjectIdentifier.init)
228
+ && inactiveTab == context.inactiveTab
229
+ && lazyRows == context.lazyRows
230
+ && environment._isSame(as: context.environment)
231
+ }
232
+ }
233
+
234
+ /// The identity of a value the runtime re-creates on every pass for a node
235
+ /// (`_RuntimeIdentified`): the node and what the value is for.
236
+ struct _RuntimeKey: Hashable, Sendable {
237
+ let node: ObjectIdentifier
238
+ let role: String
164
239
  }
165
240
 
166
241
  /// Owns the mounted tree, schedules renders and routes events.
@@ -171,22 +246,37 @@ public final class _Runtime {
171
246
  private(set) var host: any _RenderHost = _BufferedHost.shared
172
247
  private var rootView: (any View)?
173
248
  private var root: _MountedNode?
249
+ /// The off-screen element measuring views for `_ViewMeasurements` (Measurements.swift).
250
+ private var measuringRoot: _MountedNode?
251
+ /// Set when measurements arrive: the next render re-runs every body, none skipped as unchanged.
252
+ private var renderEverythingPass = false
174
253
  private let reconciler = _Reconciler()
175
254
  private var needsRender = false
176
255
  /// The next render re-runs every body (environment changes, `invalidate()`).
177
256
  private var fullRender = true
178
257
  private var rendering = false
179
258
  private var pendingAppear: [@MainActor () -> Void] = []
259
+ /// `.onDisappear` actions of the nodes the last pass removed, run after it like the appear actions.
260
+ private var pendingDisappear: [@MainActor () -> Void] = []
180
261
  /// Values the renderer controls, such as the color scheme.
181
- private var baseEnvironment = EnvironmentValues()
262
+ private var baseEnvironment = EnvironmentValues() {
263
+ didSet { EnvironmentValues._page = baseEnvironment }
264
+ }
182
265
  /// `.preferredColorScheme` seen in the last pass (sticky), and what the host was told.
183
266
  private var preferredColorScheme: ColorScheme?
184
267
  private var sentColorScheme: ColorScheme?
185
268
  /// State values from a previous build, applied as views first mount.
186
269
  private var restoredState: [String: _JSON] = [:]
187
- /// Animation for the next render pass, set by `withAnimation`. Shared by
188
- /// every runtime instance because `withAnimation` is a free function.
189
- private static var transactionAnimation: Animation?
270
+ /// The transaction `withAnimation` (or an animated binding's write) has
271
+ /// open while its body runs: `.some(nil)` when it disables animations. Shared
272
+ /// by every runtime instance because `withAnimation` is a free function.
273
+ private static var openTransaction: Animation?? = nil
274
+ /// Animation for this runtime's next render pass: the open transaction's,
275
+ /// recorded when a change made inside it invalidates this runtime. A
276
+ /// transaction whose body changes nothing animates nothing later.
277
+ private var passAnimation: Animation?
278
+ /// Counts render passes (`_MountedNode.animationTokenPass`).
279
+ private var renderPass = 0
190
280
  /// Sheet elements collected during the current element pass, emitted after the tree.
191
281
  private var presentedSheets: [_Element] = []
192
282
  /// Alert elements collected during the current element pass, emitted after
@@ -199,29 +289,291 @@ public final class _Runtime {
199
289
 
200
290
  /// Called from `App.main()`.
201
291
  public func launch<A: App>(_ app: A) {
292
+ watchApp(app)
202
293
  guard let scene = app.body as? any _RootScene else {
203
294
  fatalError("\(A.Body.self) is not a scene the runtime can present. Use WindowGroup.")
204
295
  }
205
296
  let headless = _environment("SB_HEADLESS") == "1"
297
+ isHeadless = headless
298
+ if headless, let text = _environment("SB_RESPONSES"), let json = try? _JSON.parse(text), case .array(let items) = json {
299
+ headlessResponses = items
300
+ }
206
301
  if let state = _environment("SB_STATE"), let json = try? _JSON.parse(state), case .object(let values) = json {
207
302
  restoredState = values
208
303
  }
209
304
  mount(scene._rootView, host: headless ? _HeadlessHost() : _BufferedHost.shared)
305
+ _flushAfterUpdate()
210
306
 
211
307
  if headless {
212
- replay(events: _environment("SB_EVENTS") ?? "")
308
+ // A script that names an element not on screen stops the run.
309
+ if !replay(events: _environment("SB_EVENTS") ?? "") { exit(1) }
213
310
  if let limit = _environment("SB_RUN_MS").flatMap(Double.init), limit > 0 {
214
311
  runHeadlessClock(upTo: limit)
215
312
  }
216
313
  }
217
314
  }
218
315
 
316
+ /// Re-evaluates the app's `body` when the app's own `@State` changes or,
317
+ /// for an app with `@Environment` properties, when the environment the
318
+ /// host reports changes (`scenePhase`, the color scheme…), as SwiftUI
319
+ /// does: `.onChange(of: scenePhase)` on a scene and bindings to the app's
320
+ /// state (`TabView(selection: $selectedTab)`) work. The new root view
321
+ /// replaces the old and its node renders again. An app with neither is
322
+ /// evaluated once, as before.
323
+ private var refreshApp: (() -> Void)?
324
+
325
+ private func watchApp<A: App>(_ app: A) {
326
+ let properties = Mirror(reflecting: app).children.map(\.value)
327
+ let environment = properties.filter { $0 is any _EnvironmentProperty || $0 is any _EnvironmentReading }
328
+ // `@StateObject`s are observed by the views they are handed to.
329
+ let boxes = properties.filter { !($0 is any _ObservingProperty) }.compactMap { ($0 as? any _StateProperty)?._box }
330
+ guard !environment.isEmpty || !boxes.isEmpty else { return }
331
+ let refresh = { [unowned self] in
332
+ for property in environment {
333
+ (property as? any _EnvironmentProperty)?._resolve(in: baseEnvironment)
334
+ (property as? any _EnvironmentReading)?._resolve(in: baseEnvironment)
335
+ }
336
+ guard let scene = app.body as? any _RootScene else { return }
337
+ rootView = scene._rootView
338
+ if let root { invalidate(root) }
339
+ }
340
+ refreshApp = refresh
341
+ for box in boxes {
342
+ box._onChange = { [weak self] in self?.refreshApp?() }
343
+ }
344
+ for property in environment {
345
+ (property as? any _EnvironmentProperty)?._resolve(in: baseEnvironment)
346
+ (property as? any _EnvironmentReading)?._resolve(in: baseEnvironment)
347
+ }
348
+ }
349
+
350
+ /// Puts `object` in every view's environment, under its type: SwiftUI does
351
+ /// so with an app's scene delegate that is `Observable` or an
352
+ /// `ObservableObject` (UIKit's AppDelegate.swift connects the scene).
353
+ public func _addRootEnvironmentObject(_ object: AnyObject) {
354
+ baseEnvironment._setAnyObject(object)
355
+ refreshApp?()
356
+ if let root { invalidate(root) }
357
+ }
358
+
359
+ /// `launch` without the headless setup, for tests.
360
+ func _launchForTesting<A: App>(_ app: A, host: any _RenderHost) {
361
+ watchApp(app)
362
+ guard let scene = app.body as? any _RootScene else { fatalError("\(A.Body.self) is not a presentable scene") }
363
+ mount(scene._rootView, host: host)
364
+ }
365
+
219
366
  /// Sends the app's defaults to the host (a `defaults` op), which keeps them
220
- /// for the next launch. Called by `UserDefaults` (Sources/_FoundationCommon).
367
+ /// for the next launch. Called by `UserDefaults` (Sources/FoundationBridge).
221
368
  public func storeDefaults(_ values: _JSON) {
222
369
  host.apply([.defaults(values)])
223
370
  }
224
371
 
372
+ /// Sends one saved batch of a SwiftData store to the host (a `swiftdata`
373
+ /// op), which writes it to IndexedDB and hands the store back at the next
374
+ /// launch (`SB_SWIFTDATA`). Called by `ModelContext.save()` (Sources/SwiftData).
375
+ public func storeSwiftData(_ change: _JSON) {
376
+ host.apply([.swiftData(change)])
377
+ }
378
+
379
+ /// Keys of the image sources the host has been sent (`imagedata` ops).
380
+ private var sentImageSources: Set<String> = []
381
+
382
+ /// Sends a bitmap's source to the host unless it has it already. Called
383
+ /// before an element that draws it is emitted, and by `ShareLink` before
384
+ /// it shares one, so the host always has the bytes a recipe names.
385
+ public func _sendImageSource(_ source: _ImageSource) {
386
+ guard sentImageSources.insert(source.key).inserted else { return }
387
+ host.apply([.imageData(source.json)])
388
+ }
389
+
390
+ // MARK: After-update actions
391
+
392
+ /// One-shot actions to run once the current event, executor run or launch
393
+ /// has rendered: SwiftData's autosave saves there, after the changes an
394
+ /// event made and before the host gets control back.
395
+ private var afterUpdate: [@MainActor () -> Void] = []
396
+
397
+ /// Runs `action` at the end of the current event (or executor run), after
398
+ /// its render pass.
399
+ public func _afterUpdate(_ action: @escaping @MainActor () -> Void) {
400
+ afterUpdate.append(action)
401
+ }
402
+
403
+ /// Runs the actions `_afterUpdate` queued, and renders what they changed.
404
+ public func _flushAfterUpdate() {
405
+ var rounds = 0
406
+ while !afterUpdate.isEmpty, rounds < 16 {
407
+ rounds += 1
408
+ let actions = afterUpdate
409
+ afterUpdate.removeAll()
410
+ for action in actions { action() }
411
+ renderIfNeeded()
412
+ }
413
+ }
414
+
415
+ // MARK: Host requests (Phase 13)
416
+
417
+ private var isHeadless = false
418
+ /// Headless mode: canned answers from `SB_RESPONSES` (a JSON array; each
419
+ /// item names a `url`, or a `prefix`, and carries `status` / `headers` /
420
+ /// `body` or `bodyBase64` for a fetch, `width` / `height` for an image;
421
+ /// or carries a `pick` for a `pickFiles` request).
422
+ private var headlessResponses: [_JSON] = []
423
+ private var pendingRequests: [Int: CheckedContinuation<_JSON, any Error>] = [:]
424
+ private var nextRequestID = 1
425
+
426
+ /// Asks the host for something only it can do (a `fetch`, an `image`
427
+ /// probe; see docs/ops-protocol.md, "Phase 13") and waits for its
428
+ /// `response` event. Called by `URLSession` and `AsyncImage`
429
+ /// (Sources/FoundationBridge). Headless runs answer from `SB_RESPONSES`
430
+ /// at once, or fail, so an app's network paths replay deterministically;
431
+ /// a caller that knows its own headless answer passes it as `headless`
432
+ /// (the stand-in frameworks: UserNotifications).
433
+ public func _hostRequest(kind: String, params: _JSON, headless: _JSON? = nil) async throws -> _JSON {
434
+ let id = nextRequestID
435
+ nextRequestID += 1
436
+ return try await withCheckedThrowingContinuation { continuation in
437
+ pendingRequests[id] = continuation
438
+ host.apply([.request(id: id, kind: kind, params: params)])
439
+ if isHeadless {
440
+ if let headless { complete(request: id, with: ["value": headless]) }
441
+ else { answerHeadless(id: id, kind: kind, params: params) }
442
+ }
443
+ }
444
+ }
445
+
446
+ /// Completes the request `id` from a `response` event: `value` on
447
+ /// success, `error` (a message) on failure.
448
+ private func complete(request id: Int, with fields: [String: _JSON]) {
449
+ guard let continuation = pendingRequests.removeValue(forKey: id) else { return }
450
+ if case .string(let message)? = fields["error"] {
451
+ continuation.resume(throwing: _HostRequestError(message: message))
452
+ } else {
453
+ continuation.resume(returning: fields["value"] ?? .null)
454
+ }
455
+ }
456
+
457
+ private func answerHeadless(id: Int, kind: String, params: _JSON) {
458
+ if kind == "share" {
459
+ // Nothing to share with headless; the op in the log is the record.
460
+ complete(request: id, with: ["value": .object(["outcome": .string("headless")])])
461
+ return
462
+ }
463
+ if kind == "pickFiles" {
464
+ complete(request: id, with: ["value": headlessPick()])
465
+ return
466
+ }
467
+ if kind == "scanner" {
468
+ complete(request: id, with: ["value": headlessScan(params: params)])
469
+ return
470
+ }
471
+ if kind == "openURL" {
472
+ // No tabs headless: the op in the log is the record.
473
+ complete(request: id, with: ["value": .null])
474
+ return
475
+ }
476
+ if kind == "media" {
477
+ // AVFoundation (docs/ops-protocol.md, "Video and audio"): a file
478
+ // becomes a stand-in URL; a probe reads `duration` / `width` /
479
+ // `height` from the `SB_RESPONSES` entry for its URL.
480
+ complete(request: id, with: Self._headlessMedia(params: params, fixtures: headlessResponses, serial: id))
481
+ return
482
+ }
483
+ if kind == "websocket" {
484
+ // No sockets headless: the open fails, as it does offline.
485
+ complete(request: id, with: ["error": .string("headless: WebSockets are not available")])
486
+ return
487
+ }
488
+ if kind == "webAuth" {
489
+ // A sign-in window: a fixture plays the provider (WebAuthHeadless.swift).
490
+ complete(request: id, with: Self._headlessWebAuth(params: params, fixtures: headlessResponses))
491
+ return
492
+ }
493
+ guard case .object(let request) = params, case .string(let url)? = request["url"] else {
494
+ complete(request: id, with: ["error": .string("malformed request")])
495
+ return
496
+ }
497
+ let fixture = headlessResponses.first { item in
498
+ guard case .object(let fields) = item else { return false }
499
+ if case .string(let exact)? = fields["url"], exact == url { return true }
500
+ if case .string(let prefix)? = fields["prefix"], url.hasPrefix(prefix) { return true }
501
+ return false
502
+ }
503
+ guard case .object(let fields)? = fixture else {
504
+ complete(request: id, with: ["error": .string("headless: no SB_RESPONSES entry for \(url)")])
505
+ return
506
+ }
507
+ switch kind {
508
+ case "fetch":
509
+ var body = ""
510
+ if case .string(let text)? = fields["body"] { body = _base64(Array(text.utf8)) }
511
+ if case .string(let encoded)? = fields["bodyBase64"] { body = encoded }
512
+ let status: _JSON = fields["status"] ?? .number(200)
513
+ let headers: _JSON = fields["headers"] ?? .object([:])
514
+ complete(request: id, with: ["value": .object(["status": status, "headers": headers, "body": .string(body), "url": .string(url)])])
515
+ case "image":
516
+ guard let width = fields["width"], let height = fields["height"] else {
517
+ complete(request: id, with: ["error": .string("headless: SB_RESPONSES entry for \(url) has no width/height")])
518
+ return
519
+ }
520
+ complete(request: id, with: ["value": .object(["width": width, "height": height])])
521
+ default:
522
+ complete(request: id, with: ["error": .string("headless: unknown request kind \(kind)")])
523
+ }
524
+ }
525
+
526
+ /// `SB_RESPONSES` entries with a `pick` (a list of files), used in order.
527
+ private var headlessPicksUsed = 0
528
+
529
+ /// A `pickFiles` answer (`PhotosPicker`; docs/ops-protocol.md, "Photos
530
+ /// picker") from the next `SB_RESPONSES` entry with a `pick`: each file's
531
+ /// `name`, `type`, `body` (text) or `bodyBase64`, and for a picture
532
+ /// `width` / `height` (its `url` defaults to a data URL). With no entry
533
+ /// left, the viewer dismissed the chooser: no items.
534
+ private func headlessPick() -> _JSON {
535
+ let picks = headlessResponses.compactMap { item -> [_JSON]? in
536
+ guard case .object(let fields) = item, case .array(let files)? = fields["pick"] else { return nil }
537
+ return files
538
+ }
539
+ guard headlessPicksUsed < picks.count else { return .object(["items": .array([])]) }
540
+ let files = picks[headlessPicksUsed]
541
+ headlessPicksUsed += 1
542
+ let items = files.map { file -> _JSON in
543
+ guard case .object(let fields) = file else { return .null }
544
+ var item = fields
545
+ item.removeValue(forKey: "body")
546
+ item.removeValue(forKey: "bodyBase64")
547
+ var data = ""
548
+ if case .string(let text)? = fields["body"] { data = _base64(Array(text.utf8)) }
549
+ if case .string(let encoded)? = fields["bodyBase64"] { data = encoded }
550
+ item["data"] = .string(data)
551
+ if item["url"] == nil, case .string(let type)? = fields["type"], fields["width"] != nil {
552
+ item["url"] = .string("data:\(type);base64,\(data)")
553
+ }
554
+ return .object(item)
555
+ }
556
+ return .object(["items": .array(items)])
557
+ }
558
+
559
+ /// `SB_RESPONSES` entries with a `scan` (a code), used in order.
560
+ private var headlessScansUsed = 0
561
+
562
+ /// A `scanner` answer (AVFoundation's capture sessions; docs/ops-protocol.md,
563
+ /// "Code scanning"): no camera for a `probe`; for a `scan`, the next
564
+ /// `SB_RESPONSES` entry's `scan` (`string`, and optionally `format`,
565
+ /// `corners`, `image`), or the viewer closing the camera when none is left.
566
+ private func headlessScan(params: _JSON) -> _JSON {
567
+ if case .object(let fields) = params, case .string("probe")? = fields["action"] { return .object(["camera": .bool(false)]) }
568
+ let scans = headlessResponses.compactMap { item -> _JSON? in
569
+ guard case .object(let fields) = item, let scan = fields["scan"] else { return nil }
570
+ return scan
571
+ }
572
+ guard headlessScansUsed < scans.count else { return .object(["cancelled": .bool(true)]) }
573
+ headlessScansUsed += 1
574
+ return scans[headlessScansUsed - 1]
575
+ }
576
+
225
577
  /// Drives the executor with a virtual clock: runs jobs, jumps to the next
226
578
  /// timer, and stops when none remain or the clock would pass `limit` ms.
227
579
  private func runHeadlessClock(upTo limit: Double) {
@@ -229,8 +581,7 @@ public final class _Runtime {
229
581
  var guardCount = 0
230
582
  while guardCount < 10_000 {
231
583
  guardCount += 1
232
- let next = executor.runJobs()
233
- renderIfNeeded()
584
+ let next = runJobsAndRender()
234
585
  if next < 0 { break }
235
586
  let nextMs = Double(executor.nowNs + UInt64(next)) / 1_000_000
236
587
  if nextMs > limit { break }
@@ -242,8 +593,10 @@ public final class _Runtime {
242
593
  /// Mounts `view` as the root and performs the first render.
243
594
  func mount(_ view: any View, host: any _RenderHost) {
244
595
  self.host = host
596
+ sentImageSources = []
245
597
  rootView = view
246
598
  root = nil
599
+ measuringRoot = nil
247
600
  render()
248
601
  }
249
602
 
@@ -256,6 +609,7 @@ public final class _Runtime {
256
609
  public func dispatch(event: _JSON) {
257
610
  handle(event)
258
611
  renderIfNeeded()
612
+ _flushAfterUpdate()
259
613
  }
260
614
 
261
615
  /// Applies an event's state changes without rendering. A `batch` event
@@ -270,8 +624,17 @@ public final class _Runtime {
270
624
  case "batch":
271
625
  if case .array(let events)? = fields["events"] { events.forEach(handle) }
272
626
  case "tap":
273
- if let id { reconciler.handlers[id]?(.null) }
274
- case "text", "toggle", "select", "step", "slide", "date":
627
+ // A tap with a location (`onTapGesture { point in }`) carries the
628
+ // point in the element's coordinates; any other tap is `.null`.
629
+ if let id {
630
+ if let x = fields["x"], let y = fields["y"] {
631
+ reconciler.handlers[id]?(.object(["x": x, "y": y]))
632
+ } else {
633
+ reconciler.handlers[id]?(.null)
634
+ }
635
+ }
636
+ case "text", "toggle", "select", "step", "slide", "date", "link":
637
+ // `link`: a tap on a link in a text's runs; the value is its URL.
275
638
  if let id, let value = fields["value"] { reconciler.handlers[id]?(value) }
276
639
  case "geometry":
277
640
  if let id, let width = fields["width"], let height = fields["height"] {
@@ -281,11 +644,15 @@ public final class _Runtime {
281
644
  // Host measurement of a custom `Layout`'s children; the handler
282
645
  // parses the whole event (see CustomLayout.swift).
283
646
  if let id { reconciler.handlers[id]?(event) }
284
- case "drag", "longpress", "delete", "refresh", "focus", "submit":
647
+ case "drag", "longpress", "delete", "refresh", "focus", "submit", "video", "lazy", "menu":
285
648
  // Phase 8: the handler receives the whole event and reads `type`
286
649
  // (a `listrow` handles both `tap` and `delete`; Phase 12: a
287
- // `textfield` handles `text`, `focus` and `submit`).
650
+ // `textfield` handles `text`, `focus` and `submit`; a `video`
651
+ // reads its `event`, docs/ops-protocol.md, "Video and audio").
288
652
  if let id { reconciler.handlers[id]?(event) }
653
+ case "response":
654
+ // Phase 13: the host's answer to a `request` op.
655
+ if let id { complete(request: id, with: fields) }
289
656
  case "environment":
290
657
  if case .string(let scheme)? = fields["colorScheme"] {
291
658
  baseEnvironment.colorScheme = scheme == "dark" ? .dark : .light
@@ -303,15 +670,30 @@ public final class _Runtime {
303
670
  baseEnvironment.scenePhase = value
304
671
  invalidate()
305
672
  }
673
+ // The app's `@Environment` properties read the new values.
674
+ refreshApp?()
306
675
  default:
307
676
  break
308
677
  }
309
678
  }
310
679
 
680
+ /// A render soon (a view to measure was queued outside of one).
681
+ func _scheduleRender() {
682
+ if !rendering { needsRender = true }
683
+ }
684
+
685
+ /// A render that re-evaluates the whole tree and re-runs every body, even
686
+ /// those whose view is unchanged (new measurements, Measurements.swift).
687
+ func _renderEverything() {
688
+ renderEverythingPass = true
689
+ invalidate()
690
+ }
691
+
311
692
  /// Schedules a render that re-evaluates the whole tree.
312
693
  func invalidate() {
313
694
  needsRender = true
314
695
  fullRender = true
696
+ recordTransaction()
315
697
  }
316
698
 
317
699
  /// Schedules a render that re-evaluates `node`'s subtree (and whatever
@@ -319,6 +701,12 @@ public final class _Runtime {
319
701
  func invalidate(_ node: _MountedNode) {
320
702
  markDirty(node)
321
703
  needsRender = true
704
+ recordTransaction()
705
+ }
706
+
707
+ /// A change made inside a transaction takes its animation to the next pass.
708
+ private func recordTransaction() {
709
+ if let open = Self.openTransaction { passAnimation = open }
322
710
  }
323
711
 
324
712
  private func markDirty(_ node: _MountedNode) {
@@ -332,9 +720,35 @@ public final class _Runtime {
332
720
  }
333
721
  }
334
722
 
335
- /// Records the animation `withAnimation` wants for the changes that follow.
336
- func beginTransaction(_ animation: Animation?) {
337
- Self.transactionAnimation = animation
723
+ /// Opens a transaction for the changes `body` makes: they animate with
724
+ /// `animation` (none when it is nil), and changes after it do not.
725
+ func runInTransaction<Result>(_ animation: Animation?, _ body: () throws -> Result) rethrows -> Result {
726
+ let outer = beginTransaction(animation)
727
+ defer { endTransaction(outer) }
728
+ return try body()
729
+ }
730
+
731
+ /// Opens a transaction; returns the one it replaces, for `endTransaction`.
732
+ func beginTransaction(_ animation: Animation?) -> Animation?? {
733
+ let outer = Self.openTransaction
734
+ Self.openTransaction = .some(animation)
735
+ return outer
736
+ }
737
+
738
+ func endTransaction(_ outer: Animation??) {
739
+ Self.openTransaction = outer
740
+ }
741
+
742
+ /// Whether this pass may animate `node`'s changes: the transaction has an
743
+ /// animation, or an `.animation(_:value:)` above it saw its value change.
744
+ private func passAnimates(_ node: _MountedNode) -> Bool {
745
+ if passAnimation != nil { return true }
746
+ var ancestor: _MountedNode? = node
747
+ while let current = ancestor {
748
+ if current.animationTokenPass == renderPass { return true }
749
+ ancestor = current.parent
750
+ }
751
+ return false
338
752
  }
339
753
 
340
754
  /// Advances the executor's clock, runs queued jobs and due timers, renders
@@ -342,11 +756,28 @@ public final class _Runtime {
342
756
  public func runJobs(nowMilliseconds: Double) -> Double {
343
757
  let executor = _HostExecutor.shared
344
758
  executor.nowNs = UInt64(max(0, nowMilliseconds) * 1_000_000)
345
- let next = executor.runJobs()
346
- renderIfNeeded()
759
+ let next = runJobsAndRender()
347
760
  return next < 0 ? -1 : Double(next) / 1_000_000
348
761
  }
349
762
 
763
+ /// Runs the queued jobs and renders what they changed. A render can start
764
+ /// tasks of its own (an `onChange` action's `Task`, such as one loading
765
+ /// the item a `PhotosPicker` answer just selected); they run in the same
766
+ /// call instead of waiting for the next event or timer. Returns the
767
+ /// executor's delay until its next timer (nanoseconds, -1: none).
768
+ private func runJobsAndRender() -> Int64 {
769
+ let executor = _HostExecutor.shared
770
+ var next: Int64 = -1
771
+ var rounds = 0
772
+ repeat {
773
+ rounds += 1
774
+ next = executor.runJobs()
775
+ renderIfNeeded()
776
+ _flushAfterUpdate()
777
+ } while executor.hasQueuedJobs && rounds < 16
778
+ return next
779
+ }
780
+
350
781
  func renderIfNeeded() {
351
782
  if needsRender, !rendering { render() }
352
783
  }
@@ -357,23 +788,38 @@ public final class _Runtime {
357
788
  guard let rootView else { return }
358
789
  rendering = true
359
790
  needsRender = false
360
-
361
- let context = _ReconcileContext(environment: baseEnvironment, path: _typeName(of: rootView))
791
+ renderPass += 1
792
+ _ViewMeasurements.shared.runtime = self
793
+ // An app's own `ShapeStyle` resolves in the page's environment, a
794
+ // global: the runtime rendering now is the page's (tests mount
795
+ // several runtimes, in parallel suites, each with its own scheme).
796
+ EnvironmentValues._page = baseEnvironment
797
+
798
+ let context = _ReconcileContext(environment: baseEnvironment, path: _ViewPath(root: type(of: rootView)))
362
799
  let force = fullRender
363
800
  fullRender = false
364
801
  let node = reconcile(root, with: rootView, context: context, force: force)
365
802
  root = node
803
+ // The views measured for synchronous size questions, asked while the
804
+ // bodies above ran (Measurements.swift).
805
+ let measuring = _ViewMeasurements.shared.pending
806
+ let measuringNode: _MountedNode? = measuring.isEmpty ? nil : reconcile(
807
+ measuringRoot, with: _MeasuringView(entries: measuring),
808
+ context: _ReconcileContext(environment: baseEnvironment, path: _ViewPath(root: _MeasuringView.self)), force: true)
809
+ measuringRoot = measuringNode
810
+ renderEverythingPass = false
366
811
  presentedSheets = []
367
812
  presentedAlerts = []
813
+ barFree.nextPass()
368
814
  // Bodies are tracked per node (see `reconcileChildren`); an observable
369
815
  // read while producing elements (rare) falls back to a full render.
370
816
  let tree = withObservationTracking {
371
- elements(of: node) + presentedSheets + presentedAlerts
817
+ elements(of: node) + presentedSheets + presentedAlerts + (measuringNode.map { elements(of: $0) } ?? [])
372
818
  } onChange: { [weak self] in
373
819
  MainActor.assumeIsolated { self?.invalidate() }
374
820
  }
375
- let animation = Self.transactionAnimation?._json
376
- Self.transactionAnimation = nil
821
+ let animation = passAnimation?._json
822
+ passAnimation = nil
377
823
  var ops = reconciler.update(with: tree, animation: animation)
378
824
  if preferredColorScheme != sentColorScheme {
379
825
  // Phase 12: `.preferredColorScheme` reaches the host as a root update.
@@ -382,7 +828,11 @@ public final class _Runtime {
382
828
  }
383
829
  host.apply(ops)
384
830
  rendering = false
831
+ _ViewMeasurements.shared.renderFinished()
385
832
 
833
+ let disappearing = pendingDisappear
834
+ pendingDisappear.removeAll()
835
+ for action in disappearing { action() }
386
836
  let appearing = pendingAppear
387
837
  pendingAppear.removeAll()
388
838
  for action in appearing { action() }
@@ -392,14 +842,24 @@ public final class _Runtime {
392
842
  /// Re-evaluates the mounted tree against `view`, reusing nodes (and their
393
843
  /// state) whose view type is unchanged at the same position or key.
394
844
  /// `force`: the parent re-ran, so `view` is a new value and this node's
395
- /// body must run (and its children are forced in turn). Otherwise `view`
396
- /// is the node's own stored view and the body runs only when the node is
397
- /// dirty; a clean node with no dirty descendant is returned as is.
845
+ /// body must run (and its children are forced in turn), unless the value
846
+ /// and everything else the parent passed are the same as last time
847
+ /// (`canSkip`, Phase 16). Otherwise `view` is the node's own stored view
848
+ /// and the body runs only when the node is dirty; a clean node with no
849
+ /// dirty descendant is returned as is.
398
850
  private func reconcile(_ existing: _MountedNode?, with view: any View, context: _ReconcileContext,
399
851
  force: Bool) -> _MountedNode {
852
+ var force = force
853
+ if force, let existing, canSkip(existing, for: view, in: context) {
854
+ // The parent re-ran but passed the same view and context: the node
855
+ // is clean, and only its dirty descendants (if any) re-run.
856
+ force = false
857
+ skippedViews += 1
858
+ }
400
859
  if let existing, !force, !existing.dirty, !existing.subtreeDirty {
401
860
  return existing
402
861
  }
862
+ let received = _ReceivedContext(context)
403
863
  var context = context
404
864
  let node: _MountedNode
405
865
  let clean: Bool
@@ -430,9 +890,15 @@ public final class _Runtime {
430
890
  }
431
891
  node.dirty = false
432
892
  node.subtreeDirty = false
433
- if !clean { node.cachedElements = nil }
893
+ // A clean node reached here has a dirty descendant, whose elements are
894
+ // part of its own. Its cache was cleared when the descendant was marked,
895
+ // unless that happened during the last pass (an `onChange` action
896
+ // setting state while its body ran), which cached its elements again
897
+ // after the mark.
898
+ node.cachedElements = nil
434
899
  node.view = view
435
900
  node.path = path
901
+ node.received = received
436
902
  if !clean {
437
903
  node.boxes = boxes
438
904
  for box in boxes {
@@ -444,42 +910,58 @@ public final class _Runtime {
444
910
  }
445
911
  }
446
912
 
447
- if let modifier = view as? any _EnvironmentModifying {
913
+ let traits = _ViewTraits.of(view)
914
+ // Lazy rows (LazyRows.swift): a lazy container's content is its rows
915
+ // through groups, sections and views with a body; any other view is
916
+ // part of one row, whose groups are built with it.
917
+ let lazyRowsHere = context.lazyRows
918
+ if traits.contains(.lazyRows), let container = view as? any _LazyRowsContainer {
919
+ context.lazyRows = container._lazyAxis
920
+ } else if context.lazyRows != nil, !traits.contains(.group), !traits.contains(.section),
921
+ !traits.isDisjoint(with: .bodiless) {
922
+ context.lazyRows = nil
923
+ }
924
+ if traits.contains(.environmentModifying), let modifier = view as? any _EnvironmentModifying {
448
925
  modifier._apply(to: &context.environment)
449
926
  }
450
- if let preferred = view as? any _PreferredColorSchemeModifying, !context.inactiveTab {
927
+ if traits.contains(.preferredColorScheme), let preferred = view as? any _PreferredColorSchemeModifying, !context.inactiveTab {
451
928
  preferredColorScheme = preferred._preferredColorScheme
452
929
  }
453
- if context.environment._scrollTargetLayout, view is any _ScrollTargetContainer {
930
+ if context.environment._scrollTargetLayout, traits.contains(.scrollTargetContainer) {
454
931
  // This stack's children are the snap targets; nested stacks are not.
455
932
  node.scrollTargetLayout = true
456
933
  context.environment._scrollTargetLayout = false
457
934
  }
458
- if let title = view as? any _NavigationTitleModifying, let screen = context.screen, !context.inactiveTab {
935
+ if traits.contains(.navigationTitle), let title = view as? any _NavigationTitleModifying, let screen = context.screen, !context.inactiveTab {
459
936
  if let value = title._title { screen.navTitle = value }
460
937
  if let mode = title._displayMode { screen.navDisplayMode = mode }
461
938
  }
462
- if let appear = view as? any _OnAppearing, !node.appeared {
939
+ if traits.contains(.onDisappearing), let disappear = view as? any _OnDisappearing {
940
+ node.disappearAction = disappear._disappearAction
941
+ }
942
+ if traits.contains(.onAppearing), let appear = view as? any _OnAppearing, !node.appeared {
463
943
  node.appeared = true
464
944
  if let action = appear._action { pendingAppear.append(action) }
465
945
  }
466
- if let detents = view as? any _DetentsModifying, let sheet = context.sheet {
946
+ if traits.contains(.detents), let detents = view as? any _DetentsModifying, let sheet = context.sheet {
467
947
  sheet.sheetDetents = detents._detents
468
948
  }
469
- if let animated = view as? any _AnimatedView {
949
+ if traits.contains(.animated), let animated = view as? any _AnimatedView {
470
950
  if node === existing, animated._changed(since: node.animationValue) {
471
951
  node.animationToken += 1
952
+ node.animationTokenPass = renderPass
472
953
  }
473
954
  node.animationValue = animated._value
474
955
  }
475
- if let registration = view as? any _DestinationRegistering {
956
+ if traits.contains(.destinationRegistering), let registration = view as? any _DestinationRegistering {
476
957
  context.navStack?.register(registration)
477
958
  }
478
- if let sidebar = view as? _SplitSidebarView {
959
+ if traits.contains(.splitSidebar), let sidebar = view as? _SplitSidebarView {
479
960
  context.split = sidebar.split
480
961
  }
481
- if let list = view as? any _ListSelectionProviding {
482
- if let selection = list._selection {
962
+ if traits.contains(.listSelection), let list = view as? any _ListSelectionProviding {
963
+ if var selection = list._selection {
964
+ selection.identity = _RuntimeKey(node: ObjectIdentifier(node), role: "selection:\(selection.snapshot())")
483
965
  context.environment._listSelection = selection
484
966
  context.split?.splitSelection = selection
485
967
  }
@@ -488,7 +970,7 @@ public final class _Runtime {
488
970
  context.environment.editMode = editModeBinding(for: node)
489
971
  }
490
972
  }
491
- if let starting = view as? any _TaskStarting {
973
+ if traits.contains(.taskStarting), let starting = view as? any _TaskStarting {
492
974
  if !node.taskStarted || node.taskID != starting._taskID {
493
975
  node.task?.cancel()
494
976
  node.taskStarted = true
@@ -509,7 +991,7 @@ public final class _Runtime {
509
991
  node.handler = nil
510
992
  node.navFlattened = false
511
993
 
512
- if let split = view as? any _NavigationSplitHost {
994
+ if traits.contains(.navigationSplit), let split = view as? any _NavigationSplitHost {
513
995
  // Descendants fill these in as they are reconciled; a clean pass
514
996
  // (only some descendants re-run) keeps what the skipped ones set.
515
997
  if !clean { node.splitSelection = nil }
@@ -546,12 +1028,12 @@ public final class _Runtime {
546
1028
  return detailScreens + pushedScreens
547
1029
  }
548
1030
  }
549
- } else if let stack = view as? any _NavigationStackView, context.screen != nil, context.navStack != nil {
1031
+ } else if traits.contains(.navigationStack), let stack = view as? any _NavigationStackView, context.screen != nil, context.navStack != nil {
550
1032
  // Nested in another stack's screen: the root renders in place, its
551
1033
  // destinations and links use the enclosing stack, its path is ignored.
552
1034
  node.navFlattened = true
553
1035
  makeChildren = { [stack._root] }
554
- } else if let stack = view as? any _NavigationStackView {
1036
+ } else if traits.contains(.navigationStack), let stack = view as? any _NavigationStackView {
555
1037
  let nav = navContext(for: node, box: stack._pushedBox, path: stack._path, clean: clean)
556
1038
  context.navStack = nav
557
1039
  makeChildren = {
@@ -563,7 +1045,7 @@ public final class _Runtime {
563
1045
  }
564
1046
  return [_NavScreenView(content: stack._root, depth: 0)] + pathScreens + pushedScreens
565
1047
  }
566
- } else if let screen = view as? _NavScreenView {
1048
+ } else if traits.contains(.navScreen), let screen = view as? _NavScreenView {
567
1049
  context.screen = node
568
1050
  // A screen inside an unselected tab still owns its own title.
569
1051
  context.inactiveTab = false
@@ -575,7 +1057,9 @@ public final class _Runtime {
575
1057
  if screen.depth > 0, let nav = context.navStack {
576
1058
  let depth = screen.depth
577
1059
  node.handler = { _ in nav.pop(to: depth) }
578
- context.environment.dismiss = DismissAction { nav.pop(to: depth) }
1060
+ context.environment.dismiss = DismissAction(identity: _RuntimeKey(node: ObjectIdentifier(node), role: "pop\(depth)")) {
1061
+ nav.pop(to: depth)
1062
+ }
579
1063
  }
580
1064
  // Each screen owns the edit mode of its content (`EditButton`,
581
1065
  // `List.editing`), as on iOS.
@@ -589,9 +1073,9 @@ public final class _Runtime {
589
1073
  }
590
1074
  return [screen.content]
591
1075
  }
592
- } else if let link = view as? any _NavigationLinkView {
1076
+ } else if traits.contains(.navigationLink), let link = view as? any _NavigationLinkView {
593
1077
  let destination = link._destination
594
- let value = (view as? any _NavigationValueLinkView)?._value
1078
+ let value = traits.contains(.navigationValueLink) ? (view as? any _NavigationValueLinkView)?._value : nil
595
1079
  let nav = context.navStack
596
1080
  // In a split view's sidebar a value link selects the row of a
597
1081
  // `List(selection:)` instead of pushing.
@@ -606,7 +1090,7 @@ public final class _Runtime {
606
1090
  }
607
1091
  }
608
1092
  makeChildren = { (view as? any _HostView)?._content ?? [] }
609
- } else if let tabs = view as? any _TabViewHost {
1093
+ } else if traits.contains(.tabView), let tabs = view as? any _TabViewHost {
610
1094
  let roots = _flattenChildren(tabs._tabContent)
611
1095
  node.optionTags = roots.map { _tag(of: $0) }
612
1096
  node.selectedIndex = tabs._selectedIndex(tags: node.optionTags)
@@ -615,19 +1099,22 @@ public final class _Runtime {
615
1099
  if case .number(let index) = payload { tabs._select(index: Int(index), tags: tags) }
616
1100
  }
617
1101
  let selected = node.selectedIndex
618
- makeChildren = {
619
- roots.enumerated().map { index, root in
620
- let label = _tabLabel(of: root)
621
- return _TabScreenView(content: root,
622
- title: label.map { _textContent(of: $0) } ?? "",
623
- systemImage: label.flatMap { _systemImage(of: $0) },
624
- isSelected: index == selected)
1102
+ if _tabViewIsSidebar(context.environment) {
1103
+ // Regular-width `.sidebarAdaptable`: a split view whose sidebar
1104
+ // lists the tabs (Tabs.swift); the tab view adds no element.
1105
+ node.navFlattened = true
1106
+ makeChildren = { [_tabSidebar(roots: roots, selected: selected) { tabs._select(index: $0, tags: tags) }] }
1107
+ } else {
1108
+ makeChildren = {
1109
+ roots.enumerated().map { index, root in
1110
+ _TabScreenView(root: root, isSelected: index == selected)
1111
+ }
625
1112
  }
626
1113
  }
627
- } else if let tab = view as? _TabScreenView {
1114
+ } else if traits.contains(.tabScreen), let tab = view as? _TabScreenView {
628
1115
  if !tab.isSelected { context.inactiveTab = true }
629
1116
  makeChildren = { [tab.content] }
630
- } else if let picker = view as? any _PickerHost {
1117
+ } else if traits.contains(.picker), let picker = view as? any _PickerHost {
631
1118
  let options = _flattenPickerOptions(picker._options)
632
1119
  node.optionTags = options.map { $0.tag }
633
1120
  node.selectedIndex = picker._selectedIndex(tags: node.optionTags)
@@ -638,7 +1125,7 @@ public final class _Runtime {
638
1125
  if case .number(let index) = payload { picker._select(index: Int(index), tags: tags) }
639
1126
  }
640
1127
  makeChildren = { [] }
641
- } else if let reader = view as? any _GeometryReading {
1128
+ } else if traits.contains(.geometryReading), let reader = view as? any _GeometryReading {
642
1129
  let size = node.geometrySize
643
1130
  node.handler = { [unowned self] payload in
644
1131
  guard case .object(let fields) = payload,
@@ -651,7 +1138,7 @@ public final class _Runtime {
651
1138
  }
652
1139
  }
653
1140
  makeChildren = { [reader._content(for: size)] }
654
- } else if let sheet = view as? any _SheetPresenting {
1141
+ } else if traits.contains(.sheet), let sheet = view as? any _SheetPresenting {
655
1142
  // The base is always a child; the sheet content is a second child
656
1143
  // while presented, so its state survives re-renders.
657
1144
  let isPresented = sheet._isPresented
@@ -661,39 +1148,95 @@ public final class _Runtime {
661
1148
  isPresented.wrappedValue = false
662
1149
  onDismiss?()
663
1150
  }
664
- if isPresented.wrappedValue {
665
- makeChildren = { [sheet._base, sheet._sheetContent] }
666
- } else {
667
- makeChildren = { [sheet._base] }
668
- }
669
- } else if let alert = view as? any _AlertPresenting {
1151
+ // Read in the tracked closure: a binding into an `@Observable`
1152
+ // model presents when the model changes.
1153
+ makeChildren = { isPresented.wrappedValue ? [sheet._base, sheet._sheetContent] : [sheet._base] }
1154
+ } else if traits.contains(.alert), let alert = view as? any _AlertPresenting {
670
1155
  // Like a sheet: the actions are a second child while presented. A
671
1156
  // tap on the alert itself dismisses without running an action.
672
1157
  let isPresented = alert._isPresented
673
1158
  node.handler = { _ in isPresented.wrappedValue = false }
674
- if isPresented.wrappedValue {
675
- makeChildren = { [alert._base, alert._alertActions] }
676
- } else {
677
- makeChildren = { [alert._base] }
1159
+ makeChildren = { isPresented.wrappedValue ? [alert._base, alert._alertActions] : [alert._base] }
1160
+ } else if traits.contains(.preferenceDecorating), let decorating = view as? any _PreferenceValueDecorating {
1161
+ // The decoration is made from the base's values, so the base
1162
+ // reconciles first (and the decoration is made again every pass).
1163
+ makeChildren = { [decorating._base] }
1164
+ makeMoreChildren = { [unowned node] in
1165
+ [decorating._decoration(from: node.children.first?.preferences ?? _PreferenceValues())]
678
1166
  }
679
- } else if let group = view as? any _GroupView {
1167
+ } else if traits.contains(.group), let group = view as? any _GroupView {
680
1168
  keys = group._keys
681
1169
  makeChildren = { group._children }
682
- } else if let hostView = view as? any _HostView {
683
- makeChildren = { hostView._content }
1170
+ if let axis = lazyRowsHere, let allKeys = keys, allKeys.count > node.lazyLimit,
1171
+ let requests = context.environment._scrollReader?.requests {
1172
+ // `proxy.scrollTo(id)` where the rows carry their own ids
1173
+ // (`.id(item.id)`): the rows up to the target are built first.
1174
+ for request in requests {
1175
+ if let index = allKeys.firstIndex(of: request.target), index >= node.lazyLimit {
1176
+ node.lazyLimit = index + _LazyRows.batch
1177
+ }
1178
+ }
1179
+ }
1180
+ if let axis = lazyRowsHere, let allKeys = keys, allKeys.count > node.lazyLimit {
1181
+ // The first rows, and a spacer for the rest until the page
1182
+ // sees it near the visible area.
1183
+ let limit = node.lazyLimit
1184
+ keys = Array(allKeys.prefix(limit)) + [AnyHashable(_LazySpacerKey(built: limit))]
1185
+ let spacer = _LazyRowsSpacer(axis: axis, remaining: allKeys.count - limit) { [weak self, weak node] in
1186
+ guard let self, let node, node.lazyLimit == limit else { return }
1187
+ node.lazyLimit = limit + _LazyRows.batch
1188
+ self.invalidate(node)
1189
+ }
1190
+ makeChildren = { Array(group._children.prefix(limit)) + [spacer] }
1191
+ }
1192
+ } else if traits.contains(.lazyMenu), let menu = view as? any _LazyMenuHost {
1193
+ // Lazy menus (LazyRows.swift): the items only while open.
1194
+ node.handler = { [weak self, weak node] payload in
1195
+ guard let self, let node, case .object(let fields) = payload,
1196
+ case .bool(let open)? = fields["open"], node.menuOpen != open else { return }
1197
+ node.menuOpen = open
1198
+ self.invalidate(node)
1199
+ }
1200
+ let open = isHeadless || node.menuOpen
1201
+ makeChildren = { open ? [menu._menuBase, menu._menuItems] : [menu._menuBase] }
1202
+ } else if traits.contains(.host), let hostView = view as? any _HostView {
1203
+ if let dependent = view as? any _EnvironmentContent {
1204
+ // A button under a custom style (ButtonStyles.swift).
1205
+ let environment = context.environment
1206
+ makeChildren = { dependent._content(in: environment) }
1207
+ } else {
1208
+ makeChildren = { hostView._content }
1209
+ }
684
1210
  } else {
685
1211
  makeChildren = { [self.body(of: view)] }
686
1212
  }
687
1213
 
688
- if node.handler == nil, let refreshing = view as? any _Refreshing {
1214
+ if node.handler == nil, traits.contains(.refreshing), let refreshing = view as? any _Refreshing {
689
1215
  node.handler = refreshing._refresh(in: context.environment).handler
690
1216
  }
691
- if node.handler == nil, let handling = view as? any _EnvironmentHandling {
1217
+ if node.handler == nil, traits.contains(.environmentHandling), let handling = view as? any _EnvironmentHandling {
692
1218
  node.handler = handling._handler(in: context.environment)
693
1219
  }
1220
+ if traits.contains(.sizeReporting)
1221
+ || (traits.contains(.pathDrawing) && (view as? any _PathDrawing)?._drawsPath == true) {
1222
+ // The element reports its laid-out size, like a GeometryReader's;
1223
+ // an anchor preference resolves against it, a custom shape draws
1224
+ // its path for it.
1225
+ node.handler = { [unowned self] payload in
1226
+ guard case .object(let fields) = payload,
1227
+ case .number(let width)? = fields["width"],
1228
+ case .number(let height)? = fields["height"] else { return }
1229
+ let reported = CGSize(width: width, height: height)
1230
+ if reported != node.geometrySize {
1231
+ node.geometrySize = reported
1232
+ self.invalidate(node)
1233
+ }
1234
+ }
1235
+ }
694
1236
 
695
1237
  node.environment = context.environment
696
- if clean {
1238
+ let decorating = traits.contains(.preferenceDecorating)
1239
+ if clean && !decorating {
697
1240
  // The body's output is the children already mounted (the view
698
1241
  // value is unchanged); only the dirty ones among them re-run.
699
1242
  let kept = node.children
@@ -705,10 +1248,55 @@ public final class _Runtime {
705
1248
  }
706
1249
  var phases = [makeChildren]
707
1250
  if let makeMoreChildren { phases.append(makeMoreChildren) }
708
- reconcileChildren(of: node, phases: phases, keys: keys, context: context, force: !clean)
1251
+ reconcileChildren(of: node, phases: phases, keys: keys, context: context, force: !clean, forceLaterPhases: decorating)
1252
+
1253
+ // Preferences flow up: the children's values in order, then this view's own.
1254
+ var preferences = _PreferenceValues()
1255
+ for child in node.children where !child.preferences.isEmpty { preferences.merge(child.preferences) }
1256
+ if traits.contains(.preferenceWriting), let writer = view as? any _PreferenceWriting {
1257
+ writer._write(&preferences, measured: node.geometrySize)
1258
+ }
1259
+ node.preferences = preferences
1260
+ if traits.contains(.preferenceReading), let reader = view as? any _PreferenceReading {
1261
+ let (seen, action) = reader._read(preferences, last: node.preferenceSeen)
1262
+ node.preferenceSeen = seen
1263
+ // After the pass, like onAppear: the action usually sets state.
1264
+ if let action { pendingAppear.append(action) }
1265
+ }
1266
+ node.writesContext = !traits.isDisjoint(with: .contextWriting) || node.children.contains { $0.writesContext }
709
1267
  return node
710
1268
  }
711
1269
 
1270
+ /// Whether a node whose parent re-ran can be treated as clean: it is not
1271
+ /// dirty itself, nothing in its subtree writes to its ancestors, and the
1272
+ /// parent passed the same view value and the same context as last time.
1273
+ private func canSkip(_ existing: _MountedNode, for view: any View, in context: _ReconcileContext) -> Bool {
1274
+ guard !renderEverythingPass, !existing.dirty, !existing.writesContext, let received = existing.received,
1275
+ type(of: existing.view) == type(of: view) else { return false }
1276
+ let key = ObjectIdentifier(type(of: view))
1277
+ if incomparableTypes.contains(key) { return false }
1278
+ // Only views with a body of their own are compared. The shim's
1279
+ // containers and primitives (stacks, modifiers, text) are cheap to
1280
+ // re-run, and their values hold their whole content, so comparing
1281
+ // them would walk the subtree once per level; the views with bodies
1282
+ // inside them are compared on their own.
1283
+ if !_ViewTraits.of(view).isDisjoint(with: .bodiless) { return false }
1284
+ guard _Sameness.sameView(existing.view, view) else {
1285
+ // A type whose values never compare equal (it stores a closure or
1286
+ // a binding) is not compared again.
1287
+ if !_Sameness.canEverBeSame(view) { incomparableTypes.insert(key) }
1288
+ return false
1289
+ }
1290
+ return received.matches(context)
1291
+ }
1292
+
1293
+ /// See `_hoistBarItems`.
1294
+ private var barFree = _BarFreeTokens()
1295
+ /// View types whose values hold a closure or binding: never the same.
1296
+ private var incomparableTypes: Set<ObjectIdentifier> = []
1297
+ /// Forced nodes treated as clean because their view was unchanged (for tests).
1298
+ private(set) var skippedViews = 0
1299
+
712
1300
  /// The stack context of a `NavigationStack` / split node, created once and
713
1301
  /// pointed at the current view's storage on every pass that re-ran the body.
714
1302
  private func navContext(for node: _MountedNode, box: _StateBox<[_PushedScreen]>, path: _PathBinding?,
@@ -728,8 +1316,11 @@ public final class _Runtime {
728
1316
  /// Reconciles the node's children phase by phase: each phase's views are
729
1317
  /// produced only after the previous phase's children were reconciled.
730
1318
  /// Keys apply to the first phase's views.
1319
+ /// `forceLaterPhases`: the views of phases after the first are new values
1320
+ /// even on a clean pass (a preference decoration made from the first).
731
1321
  private func reconcileChildren(of node: _MountedNode, phases: [() -> [any View]],
732
- keys: [AnyHashable]?, context: _ReconcileContext, force: Bool) {
1322
+ keys: [AnyHashable]?, context: _ReconcileContext, force: Bool,
1323
+ forceLaterPhases: Bool = false) {
733
1324
  let old = node.children
734
1325
  var oldByKey: [AnyHashable: _MountedNode] = [:]
735
1326
  for child in old {
@@ -744,6 +1335,9 @@ public final class _Runtime {
744
1335
 
745
1336
  var children: [_MountedNode] = []
746
1337
  for (phase, makeViews) in phases.enumerated() {
1338
+ let force = force || (forceLaterPhases && phase > 0)
1339
+ // A later phase may read what the earlier ones mounted.
1340
+ if phase > 0 { node.children = children }
747
1341
  // The body (and a `ForEach`'s row closures) run under observation
748
1342
  // tracking: an `@Observable` property read here marks this node
749
1343
  // dirty when it changes, and nothing else re-renders.
@@ -764,22 +1358,23 @@ public final class _Runtime {
764
1358
  existing = nil
765
1359
  }
766
1360
  var childContext = context
767
- let segment = key.map { "\($0.base)" } ?? String(index)
768
- childContext.path += "/\(segment):\(_typeName(of: childView))"
1361
+ childContext.path = context.path.child(key.map(_ViewPath.Step.key) ?? .index(index), type(of: childView))
769
1362
  if index == 1, let sheet = node.view as? any _SheetPresenting {
770
1363
  let isPresented = sheet._isPresented
771
1364
  let onDismiss = sheet._onDismiss
772
1365
  childContext.sheet = node
773
1366
  childContext.navStack = nil
774
1367
  childContext.screen = nil
775
- childContext.environment.dismiss = DismissAction {
1368
+ childContext.environment.dismiss = DismissAction(identity: _RuntimeKey(node: ObjectIdentifier(node), role: "sheet")) {
776
1369
  isPresented.wrappedValue = false
777
1370
  onDismiss?()
778
1371
  }
779
1372
  }
780
1373
  if index == 1, let alert = node.view as? any _AlertPresenting {
781
1374
  let isPresented = alert._isPresented
782
- childContext.environment.dismiss = DismissAction { isPresented.wrappedValue = false }
1375
+ childContext.environment.dismiss = DismissAction(identity: _RuntimeKey(node: ObjectIdentifier(node), role: "alert")) {
1376
+ isPresented.wrappedValue = false
1377
+ }
783
1378
  }
784
1379
  let child = reconcile(existing, with: childView, context: childContext, force: force)
785
1380
  child.key = key
@@ -793,6 +1388,10 @@ public final class _Runtime {
793
1388
  /// The `editMode` environment binding a `navscreen` or `List` node owns.
794
1389
  private func editModeBinding(for node: _MountedNode) -> Binding<EditMode> {
795
1390
  Binding(
1391
+ // The mode is part of the identity: the binding reads the node's
1392
+ // live mode, so the environment of a pass must record which mode
1393
+ // it carried for a toggle to re-run the views below.
1394
+ identity: _RuntimeKey(node: ObjectIdentifier(node), role: "editMode:\(node.editMode)"),
796
1395
  get: { [weak node] in MainActor.assumeIsolated { node?.editMode ?? .inactive } },
797
1396
  set: { [weak self, weak node] mode in
798
1397
  MainActor.assumeIsolated {
@@ -804,11 +1403,16 @@ public final class _Runtime {
804
1403
  )
805
1404
  }
806
1405
 
807
- /// Cancels tasks in a subtree that left the tree.
1406
+ /// Cancels tasks in a subtree that left the tree, and queues its
1407
+ /// `.onDisappear` actions (innermost first, as SwiftUI calls them).
808
1408
  private func unmount(_ node: _MountedNode) {
809
1409
  node.task?.cancel()
810
1410
  node.task = nil
811
1411
  node.children.forEach(unmount)
1412
+ if let action = node.disappearAction {
1413
+ node.disappearAction = nil
1414
+ pendingDisappear.append(action)
1415
+ }
812
1416
  }
813
1417
 
814
1418
  private func body(of view: any View) -> any View {
@@ -822,35 +1426,72 @@ public final class _Runtime {
822
1426
  ///
823
1427
  /// Reflection is the most expensive step of a render, and most views in a
824
1428
  /// tree are the shim's own modifiers and primitives, which declare no
825
- /// such properties. The first instance of each type is reflected; when
826
- /// none of its stored properties is one of those wrappers the type is
827
- /// remembered as plain and never reflected again. The wrappers are
828
- /// concrete struct types, so whether a stored property is one is a
829
- /// property of the type, not of the instance.
1429
+ /// such properties. The first instance of each type is reflected and
1430
+ /// each stored property checked (`_RuntimeProperty`); the wrappers are
1431
+ /// concrete struct types, so which properties are wrappers is a property
1432
+ /// of the type, not of the instance. A struct type's answer is kept as a
1433
+ /// plan: none (it is never reflected again), or the positions to read,
1434
+ /// so later passes reflect it without checking each property again.
830
1435
  private func _reflectedProperties(of view: any View) -> [Any]? {
831
- let key = ObjectIdentifier(type(of: view))
832
- if let plain = plainTypes[key], plain { return nil }
833
- var values = Mirror(reflecting: view).children.map(\.value)
834
1436
  // `ModifiedContent` stores its `ViewModifier`, whose own `@State` and
835
1437
  // `@Environment` properties are managed like the view's.
836
- for value in values where value is any ViewModifier {
837
- values += Mirror(reflecting: value).children.map(\.value)
1438
+ // A custom `ButtonStyle` (ButtonStyles.swift) too.
1439
+ let values = runtimeProperties(of: view, lookInsideModifiers: _ViewTraits.of(view).contains(.holdsModifier))
1440
+ return values.isEmpty ? nil : values
1441
+ }
1442
+
1443
+ /// Which stored properties of a struct type the runtime reads: the
1444
+ /// wrappers', then (for `_HoldsModifier` views) the stored modifiers or
1445
+ /// styles whose own wrappers it reads, in that order.
1446
+ private struct PropertyPlan {
1447
+ var wrappers: [Int]
1448
+ var modifiers: [Int]
1449
+ static let none = PropertyPlan(wrappers: [], modifiers: [])
1450
+ var isNone: Bool { wrappers.isEmpty && modifiers.isEmpty }
1451
+ }
1452
+
1453
+ /// Per struct type: its plan. Per other type: `.none` when it declares no wrappers.
1454
+ private var propertyPlans: [ObjectIdentifier: PropertyPlan] = [:]
1455
+
1456
+ private func runtimeProperties(of value: Any, lookInsideModifiers: Bool) -> [Any] {
1457
+ let type = type(of: value)
1458
+ let key = ObjectIdentifier(type)
1459
+ let plan = propertyPlans[key]
1460
+ if let plan, plan.isNone { return [] }
1461
+ let mirrored = Mirror(reflecting: value).children
1462
+ if let plan {
1463
+ // A struct's children are random access: only the planned ones are read (and boxed).
1464
+ func child(_ offset: Int) -> Any { mirrored[mirrored.index(mirrored.startIndex, offsetBy: offset)].value }
1465
+ var values = plan.wrappers.map(child)
1466
+ for offset in plan.modifiers { values += runtimeProperties(of: child(offset), lookInsideModifiers: false) }
1467
+ return values
838
1468
  }
839
- if plainTypes[key] == nil {
840
- let relevant = values.contains { value in
841
- value is any _StateProperty || value is any _EnvironmentProperty || value is any _ObservingProperty
1469
+ let children = Array(mirrored)
1470
+ var wrappers: [Int] = []
1471
+ var modifiers: [Int] = []
1472
+ var values: [Any] = []
1473
+ var inner: [Any] = []
1474
+ for (index, child) in children.enumerated() {
1475
+ // One lookup per property type (`_RuntimeProperty`, ViewTraits.swift).
1476
+ if child.value is any _RuntimeProperty {
1477
+ wrappers.append(index)
1478
+ values.append(child.value)
1479
+ } else if lookInsideModifiers, child.value is any ViewModifier || child.value is any ButtonStyle {
1480
+ let found = runtimeProperties(of: child.value, lookInsideModifiers: false)
1481
+ if !found.isEmpty { modifiers.append(index) }
1482
+ inner += found
842
1483
  }
843
- plainTypes[key] = !relevant
844
- if !relevant { return nil }
1484
+ }
1485
+ values += inner
1486
+ if values.isEmpty {
1487
+ propertyPlans[key] = PropertyPlan.none
1488
+ } else if _ViewTraits.isStruct(type) {
1489
+ // Not an enum's: its children are its case's payload, which differ by case.
1490
+ propertyPlans[key] = PropertyPlan(wrappers: wrappers, modifiers: modifiers)
845
1491
  }
846
1492
  return values
847
1493
  }
848
1494
 
849
- /// Per view type: `true` when it declares no runtime-managed properties.
850
- private var plainTypes: [ObjectIdentifier: Bool] = [:]
851
- /// Per view type: its name without generic arguments.
852
- private var typeNames: [ObjectIdentifier: String] = [:]
853
-
854
1495
  private func stateBoxes(in properties: [Any]?) -> [any _AnyStateBox] {
855
1496
  guard let properties else { return [] }
856
1497
  return properties.compactMap { ($0 as? any _StateProperty)?._box }
@@ -864,21 +1505,13 @@ public final class _Runtime {
864
1505
  guard let properties else { return }
865
1506
  for value in properties {
866
1507
  (value as? any _EnvironmentProperty)?._resolve(in: environment)
1508
+ (value as? any _EnvironmentReading)?._resolve(in: environment)
867
1509
  (value as? any _ObservingProperty)?._observe(owner: node) { [weak self, weak node] in
868
1510
  if let node { self?.invalidate(node) }
869
1511
  }
870
1512
  }
871
1513
  }
872
1514
 
873
- private func _typeName(of view: any View) -> String {
874
- let key = ObjectIdentifier(type(of: view))
875
- if let cached = typeNames[key] { return cached }
876
- let full = String(describing: type(of: view))
877
- let name = if let angle = full.firstIndex(of: "<") { String(full[..<angle]) } else { full }
878
- typeNames[key] = name
879
- return name
880
- }
881
-
882
1515
  // MARK: Elements
883
1516
 
884
1517
  private func elements(of node: _MountedNode) -> [_Element] {
@@ -898,12 +1531,13 @@ public final class _Runtime {
898
1531
 
899
1532
  private func produceElements(of node: _MountedNode) -> [_Element] {
900
1533
  var produced: [_Element]
901
- if node.view is _NavScreenView {
1534
+ let traits = _ViewTraits.of(node.view)
1535
+ if traits.contains(.navScreen) {
902
1536
  // `.toolbar` / `.searchable` emit `toolbar` and `searchfield`
903
1537
  // elements wherever they were applied; the renderer wants them as
904
1538
  // the screen's own children, after the content.
905
1539
  var bar: [_Element] = []
906
- let content = node.children.flatMap(elements(of:)).map { _hoistBarItems(from: $0, into: &bar) }
1540
+ let content = node.children.flatMap(elements(of:)).map { _hoistBarItems(from: $0, into: &bar, barFree: &barFree) }
907
1541
  produced = [_Element(
908
1542
  kind: "navscreen",
909
1543
  props: .object([
@@ -914,7 +1548,7 @@ public final class _Runtime {
914
1548
  children: content + bar,
915
1549
  handler: node.handler
916
1550
  )]
917
- } else if let sheet = node.view as? any _SheetPresenting {
1551
+ } else if traits.contains(.sheet), let sheet = node.view as? any _SheetPresenting {
918
1552
  // Base inline; the presented content becomes a root-level `sheet` element.
919
1553
  produced = node.children.first.map(elements(of:)) ?? []
920
1554
  if node.children.count > 1 {
@@ -924,10 +1558,10 @@ public final class _Runtime {
924
1558
  props: .object(["detents": .array(detents.map(_JSON.string))]),
925
1559
  children: elements(of: node.children[1]),
926
1560
  handler: node.handler,
927
- key: AnyHashable(_SheetKey(path: node.path))
1561
+ key: AnyHashable(_SheetKey(path: node.path?.text ?? ""))
928
1562
  ))
929
1563
  }
930
- } else if let alert = node.view as? any _AlertPresenting {
1564
+ } else if traits.contains(.alert), let alert = node.view as? any _AlertPresenting {
931
1565
  // Base inline; the actions become a root-level `alert` element whose
932
1566
  // buttons also dismiss when tapped.
933
1567
  produced = node.children.first.map(elements(of:)) ?? []
@@ -943,10 +1577,10 @@ public final class _Runtime {
943
1577
  ]),
944
1578
  children: buttons,
945
1579
  handler: node.handler,
946
- key: AnyHashable(_AlertKey(path: node.path))
1580
+ key: AnyHashable(_AlertKey(path: node.path?.text ?? ""))
947
1581
  ))
948
1582
  }
949
- } else if node.view is any _NavigationSplitHost {
1583
+ } else if traits.contains(.navigationSplit) {
950
1584
  // Regular: `navsplit` over the two column stacks. Compact: one
951
1585
  // `navstack` whose screens are the sidebar, the detail and what
952
1586
  // the detail pushed.
@@ -957,16 +1591,18 @@ public final class _Runtime {
957
1591
  children: node.children.flatMap(elements(of:)),
958
1592
  handler: nil
959
1593
  )]
960
- } else if node.view is any _NavigationStackView, node.navFlattened {
1594
+ } else if traits.contains(.navigationStack), node.navFlattened {
1595
+ produced = node.children.flatMap(elements(of:))
1596
+ } else if traits.contains(.tabView), node.navFlattened {
961
1597
  produced = node.children.flatMap(elements(of:))
962
- } else if node.view is any _TabViewHost {
1598
+ } else if traits.contains(.tabView) {
963
1599
  produced = [_Element(
964
1600
  kind: "tabview",
965
1601
  props: .object(["selected": .number(Double(node.selectedIndex))]),
966
1602
  children: node.children.flatMap(elements(of:)),
967
1603
  handler: node.handler
968
1604
  )]
969
- } else if node.view is any _PickerHost {
1605
+ } else if traits.contains(.picker) {
970
1606
  produced = [_Element(
971
1607
  kind: "picker",
972
1608
  props: .object([
@@ -978,7 +1614,7 @@ public final class _Runtime {
978
1614
  children: [],
979
1615
  handler: node.handler
980
1616
  )]
981
- } else if let animated = node.view as? any _AnimatedView {
1617
+ } else if traits.contains(.animated), let animated = node.view as? any _AnimatedView {
982
1618
  var style: [String: _JSON] = ["animationToken": .number(Double(node.animationToken))]
983
1619
  if let animation = animated._animation { style["animation"] = animation._json }
984
1620
  produced = [_Element(
@@ -987,25 +1623,38 @@ public final class _Runtime {
987
1623
  children: node.children.flatMap(elements(of:)),
988
1624
  handler: nil
989
1625
  )]
990
- } else if !(node.view is any _GroupView), let hostView = node.view as? any _HostView {
991
- // Groups are checked first: casting an `Optional<SomeHostView>` to
992
- // `_HostView` unwraps the optional, which would misclassify it.
1626
+ } else if traits.contains(.host), !traits.contains(.group), let hostView = node.view as? any _HostView {
993
1627
  let kind = hostView._kind
994
1628
  var children = childElements(of: node)
995
1629
  if kind == "list" { children = wrapListRows(children, in: node) }
996
1630
  var props = hostView._props(in: node.environment)
1631
+ if traits.contains(.pathDrawing), let drawing = node.view as? any _PathDrawing, drawing._drawsPath {
1632
+ let state = node.pathState ?? _PathState()
1633
+ node.pathState = state
1634
+ props = drawing._pathProps(props, size: node.geometrySize, state: state, animated: passAnimates(node))
1635
+ }
1636
+ if traits.contains(.lazyMenu), !isHeadless, case .object(var fields) = props {
1637
+ fields["deferred"] = .bool(!node.menuOpen)
1638
+ props = .object(fields)
1639
+ }
1640
+ if kind == "image", let image = hostView as? Image { image._sendBitmapSource(through: self) }
1641
+ if kind == "text", let rich = hostView as? _RichText { rich._sendBitmapSources(through: self) }
997
1642
  if node.scrollTargetLayout, case .object(var fields) = props {
998
1643
  // Phase 12: this stack's children are the scroll view's snap targets.
999
1644
  fields["scrollTarget"] = .bool(true)
1000
1645
  props = .object(fields)
1001
1646
  }
1647
+ // A custom layout reads its children's `.layoutValue`s (CustomLayout.swift).
1648
+ if kind == "layout", let receiving = node.view as? any _LayoutValuesReceiving {
1649
+ receiving._receive(childLayoutValues: children.map { $0.layoutValues ?? [:] })
1650
+ }
1002
1651
  var element = _Element(
1003
1652
  kind: kind,
1004
1653
  props: props,
1005
1654
  children: children,
1006
1655
  handler: node.handler ?? hostView._handler
1007
1656
  )
1008
- if let link = node.view as? any _NavigationValueLinkView, let value = link._value {
1657
+ if traits.contains(.navigationValueLink), let link = node.view as? any _NavigationValueLinkView, let value = link._value {
1009
1658
  element.row = _RowMark(linkValue: value, linkHandler: element.handler)
1010
1659
  }
1011
1660
  // Wrappers that leave the row's content as their (first) child pass
@@ -1015,26 +1664,45 @@ public final class _Runtime {
1015
1664
  if transparent, let inner = children.first?.row {
1016
1665
  element.row = (element.row ?? _RowMark()).merged(with: inner)
1017
1666
  }
1667
+ // Modifiers outside a `.layoutValue` (`.zIndex`, `.offset`, an
1668
+ // overlay) leave the value on the view the layout sees.
1669
+ if kind == "styled" || kind == "gesture" || kind == "tapgesture" || kind == "decorated"
1670
+ || kind == "contextmenu" || kind == "swipeactions", let inner = children.first?.layoutValues {
1671
+ element.layoutValues = inner
1672
+ }
1018
1673
  produced = [element]
1019
1674
  } else {
1020
1675
  produced = childElements(of: node)
1021
1676
  }
1022
1677
 
1023
- if let tagged = node.view as? any _TagProviding {
1678
+ if traits.contains(.tagProviding), let tagged = node.view as? any _TagProviding {
1024
1679
  let tag = tagged._tag
1025
1680
  for index in produced.indices where produced[index].row?.tag == nil {
1026
1681
  produced[index].row = (produced[index].row ?? _RowMark()).merged(with: _RowMark(tag: tag))
1027
1682
  }
1028
1683
  }
1029
- if let disabling = node.view as? any _RowEditingDisabling, disabling._deleteDisabled {
1684
+ if traits.contains(.rowEditingDisabling), let disabling = node.view as? any _RowEditingDisabling, disabling._deleteDisabled {
1030
1685
  for index in produced.indices {
1031
1686
  produced[index].row = (produced[index].row ?? _RowMark()).merged(with: _RowMark(deleteDisabled: true))
1032
1687
  }
1033
1688
  }
1034
- if let tab = node.view as? _TabScreenView, !tab.isSelected {
1689
+ if traits.contains(.layoutValueProviding), let providing = node.view as? any _LayoutValueProviding {
1690
+ // The value closest to the view wins: an inner one is already set.
1691
+ let (key, value) = providing._layoutValue
1692
+ for index in produced.indices where produced[index].layoutValues?[key] == nil {
1693
+ produced[index].layoutValues = (produced[index].layoutValues ?? [:]).merging([key: value]) { inner, _ in inner }
1694
+ }
1695
+ }
1696
+ if traits.contains(.accessibilityProviding), let providing = node.view as? any _AccessibilityProviding {
1697
+ let mark = providing._accessibility
1698
+ for index in produced.indices {
1699
+ produced[index].accessibility = (produced[index].accessibility ?? _AccessibilityMark()).merged(with: mark)
1700
+ }
1701
+ }
1702
+ if traits.contains(.tabScreen), let tab = node.view as? _TabScreenView, !tab.isSelected {
1035
1703
  // An unselected tab's toolbars do not reach the enclosing screen.
1036
1704
  var dropped: [_Element] = []
1037
- produced = produced.map { _hoistBarItems(from: $0, into: &dropped) }
1705
+ produced = produced.map { _hoistBarItems(from: $0, into: &dropped, barFree: &barFree) }
1038
1706
  }
1039
1707
 
1040
1708
  if let key = node.key {
@@ -1054,7 +1722,7 @@ public final class _Runtime {
1054
1722
  /// the siblings. Without that the element diff matched the two groups'
1055
1723
  /// elements to each other on alternate passes, swapping their props.
1056
1724
  private func childElements(of node: _MountedNode) -> [_Element] {
1057
- let delete = (node.view as? any _DeletableGroup)?._delete
1725
+ let delete = _ViewTraits.of(node.view).contains(.deletableGroup) ? (node.view as? any _DeletableGroup)?._delete : nil
1058
1726
  return node.children.enumerated().flatMap { position, child -> [_Element] in
1059
1727
  var items = elements(of: child)
1060
1728
  // A `ForEach` id is the row's implicit tag; an `onDelete` group's
@@ -1069,7 +1737,7 @@ public final class _Runtime {
1069
1737
  items[index].row = (items[index].row ?? _RowMark()).merged(with: _RowMark(delete: { delete(position) }))
1070
1738
  }
1071
1739
  }
1072
- if child.key == nil, let group = child.view as? any _GroupView, group._keys != nil {
1740
+ if child.key == nil, _ViewTraits.of(child.view).contains(.group), let group = child.view as? any _GroupView, group._keys != nil {
1073
1741
  for index in items.indices {
1074
1742
  if let key = items[index].key {
1075
1743
  items[index].key = AnyHashable(_GroupKey(position: position, key: key))
@@ -1144,7 +1812,7 @@ public final class _Runtime {
1144
1812
  func walk(_ node: _MountedNode) {
1145
1813
  for (index, box) in node.boxes.enumerated() {
1146
1814
  if let json = _primitiveJSON(box._anyValue) {
1147
- values["\(node.path)#\(index)"] = json
1815
+ values["\(node.path?.text ?? "")#\(index)"] = json
1148
1816
  }
1149
1817
  }
1150
1818
  node.children.forEach(walk)
@@ -1153,10 +1821,11 @@ public final class _Runtime {
1153
1821
  return .object(values)
1154
1822
  }
1155
1823
 
1156
- private func restoreState(into boxes: [any _AnyStateBox], at path: String) {
1157
- guard !restoredState.isEmpty else { return }
1824
+ private func restoreState(into boxes: [any _AnyStateBox], at path: _ViewPath) {
1825
+ guard !restoredState.isEmpty, !boxes.isEmpty else { return }
1826
+ let text = path.text
1158
1827
  for (index, box) in boxes.enumerated() {
1159
- guard let saved = restoredState["\(path)#\(index)"] else { continue }
1828
+ guard let saved = restoredState["\(text)#\(index)"] else { continue }
1160
1829
  switch (box._anyValue, saved) {
1161
1830
  case (is Int, .number(let n)) where n == n.rounded(): box._anyValue = Int(n)
1162
1831
  case (is Double, .number(let n)): box._anyValue = n
@@ -1169,74 +1838,145 @@ public final class _Runtime {
1169
1838
 
1170
1839
  // MARK: Headless replay
1171
1840
 
1172
- /// Replays events separated by commas: `tap:12`, `text:7:Milk`,
1173
- /// `toggle:9:true`, `env:dark`, `size:regular`, `step:7:1`, `slide:7:0.35`,
1174
- /// `date:7:1700000000`, `drag:12:changed:40x-3`, `longpress:12`, `delete:31`,
1175
- /// `refresh:40`. Each is echoed before its ops.
1176
- func replay(events: String) {
1177
- for raw in events.split(separator: ",") {
1178
- let parts = raw.split(separator: ":", maxSplits: 2).map { String($0).trimmingSpaces() }
1179
- var event: [String: _JSON] = [:]
1180
- switch parts.first {
1181
- case "tap" where parts.count == 2:
1182
- guard let id = Double(parts[1]) else { break }
1183
- event = ["type": .string("tap"), "id": .number(id)]
1184
- case "text" where parts.count == 3:
1185
- guard let id = Double(parts[1]) else { break }
1186
- event = ["type": .string("text"), "id": .number(id), "value": .string(parts[2])]
1187
- case "toggle" where parts.count == 3:
1188
- guard let id = Double(parts[1]) else { break }
1189
- event = ["type": .string("toggle"), "id": .number(id), "value": .bool(parts[2] == "true")]
1190
- case "env" where parts.count == 2:
1191
- event = ["type": .string("environment"), "colorScheme": .string(parts[1])]
1192
- case "type" where parts.count == 2:
1193
- event = ["type": .string("environment"), "dynamicTypeSize": .string(parts[1])]
1194
- case "size" where parts.count == 2:
1195
- event = ["type": .string("environment"), "horizontalSizeClass": .string(parts[1])]
1196
- case "select" where parts.count == 3:
1197
- guard let id = Double(parts[1]), let index = Double(parts[2]) else { break }
1198
- event = ["type": .string("select"), "id": .number(id), "value": .number(index)]
1199
- case "step" where parts.count == 3, "slide" where parts.count == 3, "date" where parts.count == 3:
1200
- guard let id = Double(parts[1]), let value = Double(parts[2]) else { break }
1201
- event = ["type": .string(parts[0]), "id": .number(id), "value": .number(value)]
1202
- case "geometry" where parts.count == 3:
1203
- let dims = parts[2].split(separator: "x").compactMap { Double($0) }
1204
- guard let id = Double(parts[1]), dims.count == 2 else { break }
1205
- event = ["type": .string("geometry"), "id": .number(id), "width": .number(dims[0]), "height": .number(dims[1])]
1206
- case "drag" where parts.count == 3:
1207
- // drag:12:changed:40x-3 (phase and translation)
1208
- let rest = parts[2].split(separator: ":").map { String($0).trimmingSpaces() }
1209
- guard let id = Double(parts[1]), rest.count == 2 else { break }
1210
- let dims = rest[1].split(separator: "x").compactMap { Double($0) }
1211
- guard dims.count == 2 else { break }
1212
- event = [
1213
- "type": .string("drag"), "id": .number(id), "phase": .string(rest[0]),
1214
- "translation": .object(["width": .number(dims[0]), "height": .number(dims[1])]),
1215
- ]
1216
- case "longpress" where parts.count == 2, "delete" where parts.count == 2, "refresh" where parts.count == 2:
1217
- guard let id = Double(parts[1]) else { break }
1218
- event = ["type": .string(parts[0]), "id": .number(id)]
1219
- default:
1220
- break
1221
- }
1222
- guard !event.isEmpty else {
1223
- print("{\"message\":\"unrecognized event \(raw)\",\"op\":\"error\"}")
1841
+ /// Replays an event script (Sources/SwiftUI/Runtime/Replay.swift): events
1842
+ /// separated by commas or line breaks, such as `tap:"Add"`, `tap:12`,
1843
+ /// `text:"Name":Milk`, `toggle:"Done":true`, `select:"Sort":"Title"`,
1844
+ /// `env:dark`, `type:xxxLarge`, `size:regular`, `step:"8 hours":1`,
1845
+ /// `slide:7:0.35`, `date:7:1700000000`, `drag:12:changed:40x-3`,
1846
+ /// `longpress:12`, `delete:"Coffee"`, `refresh:40`, `back`, `dismiss`.
1847
+ /// Each is echoed, with the id its target resolved to, before its ops.
1848
+ /// Returns false, after printing an error op, at the first event that
1849
+ /// cannot be parsed or whose selector does not name exactly one element.
1850
+ @discardableResult
1851
+ func replay(events script: String) -> Bool {
1852
+ for raw in _EventScript.split(script) {
1853
+ // `wait:500`: the virtual clock moves on 500 ms, running what comes due
1854
+ // (work an event queued, a timer), before the next event.
1855
+ if raw.hasPrefix("wait:"), let ms = Double(String(raw.dropFirst(5)).trimmingSpaces()), ms >= 0 {
1856
+ runHeadlessClock(upTo: Double(_HostExecutor.shared.nowNs) / 1_000_000 + ms)
1224
1857
  continue
1225
1858
  }
1859
+ let event: [String: _JSON]
1860
+ do {
1861
+ event = try scriptEvent(raw)
1862
+ } catch {
1863
+ print(_JSON.object(["op": .string("error"), "message": .string(error.message)]).serialized)
1864
+ return false
1865
+ }
1226
1866
  host.apply([.event(.object(event))])
1227
1867
  dispatch(event: .object(event))
1228
1868
  }
1869
+ return true
1870
+ }
1871
+
1872
+ func scriptEvent(_ raw: String) throws(_ScriptError) -> [String: _JSON] {
1873
+ let parsed = try _EventScript.parse(raw)
1874
+ let unrecognized = _ScriptError(message: "unrecognized event \(raw)")
1875
+ func number(_ text: String?) throws(_ScriptError) -> _JSON {
1876
+ guard let text, let value = Double(text) else { throw unrecognized }
1877
+ return .number(value)
1878
+ }
1879
+ func size(_ text: String?) throws(_ScriptError) -> _JSON {
1880
+ let dims = (text ?? "").split(separator: "x").compactMap { Double(String($0)) }
1881
+ guard dims.count == 2 else { throw unrecognized }
1882
+ return .object(["width": .number(dims[0]), "height": .number(dims[1])])
1883
+ }
1884
+
1885
+ switch parsed.type {
1886
+ case "back", "dismiss":
1887
+ guard parsed.target == nil, parsed.value == nil else { throw unrecognized }
1888
+ let target = parsed.type == "back" ? try reconciler.backTarget() : try reconciler.dismissTarget()
1889
+ return ["type": .string("tap"), "id": .number(Double(target.id))]
1890
+ case "env", "type", "size":
1891
+ guard let value = parsed.value, !value.isEmpty else { throw unrecognized }
1892
+ let field = ["env": "colorScheme", "type": "dynamicTypeSize", "size": "horizontalSizeClass"][parsed.type]!
1893
+ return ["type": .string("environment"), field: .string(value)]
1894
+ default:
1895
+ break
1896
+ }
1897
+
1898
+ var element: _Reconciler.Rendered?
1899
+ let id: Int
1900
+ switch parsed.target {
1901
+ case .id(let number)?:
1902
+ id = number
1903
+ case .selector(let selector)?:
1904
+ let resolved = try reconciler.resolve(selector, for: parsed.type)
1905
+ element = resolved
1906
+ id = resolved.id
1907
+ case nil:
1908
+ throw unrecognized
1909
+ }
1910
+ var event: [String: _JSON] = ["type": .string(parsed.type), "id": .number(Double(id))]
1911
+ let value = parsed.value
1912
+
1913
+ switch parsed.type {
1914
+ case "tap", "longpress", "delete", "refresh":
1915
+ guard value == nil else { throw unrecognized }
1916
+ case "text":
1917
+ guard let value else { throw unrecognized }
1918
+ event["value"] = .string(value)
1919
+ case "toggle":
1920
+ guard value == "true" || value == "false" else { throw unrecognized }
1921
+ event["value"] = .bool(value == "true")
1922
+ case "select":
1923
+ if parsed.valueQuoted, let option = value {
1924
+ guard let target = element ?? reconciler.element(id: id) else { throw unrecognized }
1925
+ event["value"] = .number(Double(try reconciler.optionIndex(option, of: target)))
1926
+ } else {
1927
+ event["value"] = try number(value)
1928
+ }
1929
+ case "step", "slide", "date":
1930
+ event["value"] = try number(value)
1931
+ case "geometry":
1932
+ guard case .object(let dims) = try size(value) else { throw unrecognized }
1933
+ event["width"] = dims["width"]
1934
+ event["height"] = dims["height"]
1935
+ case "drag":
1936
+ // drag:12:changed:40x-3 (phase and translation)
1937
+ let rest = (value ?? "").split(separator: ":", maxSplits: 1).map { String($0).trimmingSpaces() }
1938
+ guard rest.count == 2 else { throw unrecognized }
1939
+ event["phase"] = .string(rest[0])
1940
+ event["translation"] = try size(rest[1])
1941
+ default:
1942
+ throw unrecognized
1943
+ }
1944
+ return event
1229
1945
  }
1230
1946
  }
1231
1947
 
1232
- private extension String {
1233
- func trimmingSpaces() -> String {
1234
- String(drop(while: { $0 == " " }).reversed().drop(while: { $0 == " " }).reversed())
1948
+ /// A host request failed: the host could not perform it (a network error,
1949
+ /// a missing headless fixture). `URLSession` maps it onto `URLError`.
1950
+ public struct _HostRequestError: Error, CustomStringConvertible, Sendable {
1951
+ public let message: String
1952
+ public init(message: String) { self.message = message }
1953
+ public var description: String { message }
1954
+ }
1955
+
1956
+ /// Standard base64 (RFC 4648, padded). The runtime has no Foundation.
1957
+ public func _base64(_ bytes: [UInt8]) -> String {
1958
+ let table = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".utf8)
1959
+ var out: [UInt8] = []
1960
+ out.reserveCapacity((bytes.count + 2) / 3 * 4)
1961
+ var index = 0
1962
+ while index + 3 <= bytes.count {
1963
+ let n = (UInt32(bytes[index]) << 16) | (UInt32(bytes[index + 1]) << 8) | UInt32(bytes[index + 2])
1964
+ out += [table[Int(n >> 18 & 63)], table[Int(n >> 12 & 63)], table[Int(n >> 6 & 63)], table[Int(n & 63)]]
1965
+ index += 3
1235
1966
  }
1967
+ let rest = bytes.count - index
1968
+ if rest == 1 {
1969
+ let n = UInt32(bytes[index]) << 16
1970
+ out += [table[Int(n >> 18 & 63)], table[Int(n >> 12 & 63)], 61, 61]
1971
+ } else if rest == 2 {
1972
+ let n = (UInt32(bytes[index]) << 16) | (UInt32(bytes[index + 1]) << 8)
1973
+ out += [table[Int(n >> 18 & 63)], table[Int(n >> 12 & 63)], table[Int(n >> 6 & 63)], 61]
1974
+ }
1975
+ return String(decoding: out, as: UTF8.self)
1236
1976
  }
1237
1977
 
1238
1978
  /// Views that run an action when first mounted.
1239
- protocol _OnAppearing {
1979
+ protocol _OnAppearing: _TraitBearing {
1240
1980
  var _action: (@MainActor () -> Void)? { get }
1241
1981
  }
1242
1982
 
@@ -1244,6 +1984,15 @@ extension _OnAppearView: _OnAppearing {
1244
1984
  var _action: (@MainActor () -> Void)? { action }
1245
1985
  }
1246
1986
 
1987
+ /// Views that run an action when they leave the tree.
1988
+ protocol _OnDisappearing: _TraitBearing {
1989
+ var _disappearAction: (@MainActor () -> Void)? { get }
1990
+ }
1991
+
1992
+ extension _OnDisappearView: _OnDisappearing {
1993
+ var _disappearAction: (@MainActor () -> Void)? { action }
1994
+ }
1995
+
1247
1996
  func _primitiveJSON(_ value: Any) -> _JSON? {
1248
1997
  switch value {
1249
1998
  case let v as Int: .number(Double(v))
@@ -1291,7 +2040,8 @@ private func _dismissing(_ element: _Element, with isPresented: Binding<Bool>) -
1291
2040
  /// the views they contribute, keeping ForEach ids as tags when asked.
1292
2041
  @MainActor
1293
2042
  func _flattenChildren(_ view: any View, keys: Bool) -> [(view: any View, tag: AnyHashable?)] {
1294
- if let group = view as? any _GroupView,
2043
+ // A `Text` is a group (of its one rendering) but one view to its container.
2044
+ if let group = view as? any _GroupView, !(view is Text),
1295
2045
  !(view is any _TabItemProviding), !(view is any _TagProviding) {
1296
2046
  let children = group._children
1297
2047
  let ids = keys ? group._keys : nil
@@ -1333,7 +2083,9 @@ func _tabLabel(of view: any View) -> (any View)? {
1333
2083
 
1334
2084
  /// Removes `toolbar` and `searchfield` elements from anywhere under `element`
1335
2085
  /// (appending them to `bar`) and returns the element without them.
1336
- private func _hoistBarItems(from element: _Element, into bar: inout [_Element]) -> _Element {
2086
+ private func _hoistBarItems(from element: _Element, into bar: inout [_Element],
2087
+ barFree: inout _BarFreeTokens) -> _Element {
2088
+ if barFree.contains(element.token) { return element }
1337
2089
  var kept: [_Element] = []
1338
2090
  var changed = false
1339
2091
  for child in element.children {
@@ -1341,20 +2093,47 @@ private func _hoistBarItems(from element: _Element, into bar: inout [_Element])
1341
2093
  bar.append(child)
1342
2094
  changed = true
1343
2095
  } else {
1344
- let hoisted = _hoistBarItems(from: child, into: &bar)
2096
+ let hoisted = _hoistBarItems(from: child, into: &bar, barFree: &barFree)
1345
2097
  if hoisted.token != child.token { changed = true }
1346
2098
  kept.append(hoisted)
1347
2099
  }
1348
2100
  }
1349
2101
  // Unchanged subtrees keep their element (and token), so the reconciler
1350
2102
  // can skip them; a modified copy is a new element.
1351
- guard changed else { return element }
2103
+ guard changed else {
2104
+ barFree.insert(element.token)
2105
+ return element
2106
+ }
1352
2107
  var copy = element
1353
2108
  copy.children = kept
1354
2109
  copy.token = _Element.nextToken()
1355
2110
  return copy
1356
2111
  }
1357
2112
 
2113
+ /// Tokens of element subtrees known to hold no `toolbar` or `searchfield`,
2114
+ /// which `_hoistBarItems` does not walk again. A clean node's cached elements
2115
+ /// keep their tokens from pass to pass, so a screen whose content changed in
2116
+ /// one place walks only the elements produced anew. Holds the tokens met in
2117
+ /// this pass and the previous one.
2118
+ struct _BarFreeTokens {
2119
+ private var current: Set<Int> = []
2120
+ private var previous: Set<Int> = []
2121
+
2122
+ mutating func contains(_ token: Int) -> Bool {
2123
+ if current.contains(token) { return true }
2124
+ guard previous.contains(token) else { return false }
2125
+ current.insert(token)
2126
+ return true
2127
+ }
2128
+
2129
+ mutating func insert(_ token: Int) { current.insert(token) }
2130
+
2131
+ mutating func nextPass() {
2132
+ swap(&current, &previous)
2133
+ current.removeAll(keepingCapacity: true)
2134
+ }
2135
+ }
2136
+
1358
2137
  /// A picker's options, looking through `Section`s (their headers are dropped).
1359
2138
  @MainActor
1360
2139
  func _flattenPickerOptions(_ options: any View) -> [(view: any View, tag: AnyHashable?)] {