@swiftbrowser/swift 0.0.0-stage → 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.
- package/Package.swift +214 -0
- package/README.md +10 -2
- package/Sources/AVFoundation/AVFoundation.swift +131 -0
- package/Sources/AVFoundation/Asset.swift +487 -0
- package/Sources/AVFoundation/AudioSession.swift +144 -0
- package/Sources/AVFoundation/CMTime.swift +303 -0
- package/Sources/AVFoundation/Capture.swift +688 -0
- package/Sources/AVFoundation/Player.swift +749 -0
- package/Sources/AVFoundation/PlayerView.swift +117 -0
- package/Sources/AVKit/AVKit.swift +132 -0
- package/Sources/Accelerate/Accelerate.swift +122 -0
- package/Sources/AppIntents/AppIntents.swift +144 -0
- package/Sources/AppIntents/Parameters.swift +220 -0
- package/Sources/AppIntents/Shortcuts.swift +159 -0
- package/Sources/AudioToolbox/AudioToolbox.swift +55 -0
- package/Sources/AuthenticationServices/AuthenticationServices.swift +257 -0
- package/Sources/AuthenticationServices/Authorization.swift +251 -0
- package/Sources/Charts/Charts.swift +61 -0
- package/Sources/Combine/AnyCancellable.swift +65 -0
- package/Sources/Combine/AnyPublisher.swift +148 -0
- package/Sources/Combine/AnySubscriber.swift +230 -0
- package/Sources/Combine/Cancellable.swift +35 -0
- package/Sources/Combine/Codable.swift +29 -0
- package/Sources/Combine/CombineIdentifier.swift +45 -0
- package/Sources/Combine/Concurrency/ConcurrencyHelpers.swift +24 -0
- package/Sources/Combine/Concurrency/Future+Concurrency.swift +136 -0
- package/Sources/Combine/Concurrency/GENERATED-Publisher+Concurrency.swift +397 -0
- package/Sources/Combine/CurrentValueSubject.swift +252 -0
- package/Sources/Combine/CustomCombineIdentifierConvertible.swift +28 -0
- package/Sources/Combine/Future.swift +228 -0
- package/Sources/Combine/GENERATED-RootProtocols.swift +426 -0
- package/Sources/Combine/Helpers/ConduitBase.swift +40 -0
- package/Sources/Combine/Helpers/ConduitList.swift +59 -0
- package/Sources/Combine/Helpers/DebugHook.swift +177 -0
- package/Sources/Combine/Helpers/FilterProducer.swift +174 -0
- package/Sources/Combine/Helpers/Locking.swift +30 -0
- package/Sources/Combine/Helpers/PartialCompletion.swift +26 -0
- package/Sources/Combine/Helpers/ReduceProducer.swift +249 -0
- package/Sources/Combine/Helpers/SubjectSubscriber.swift +96 -0
- package/Sources/Combine/Helpers/SubscriberTap.swift +69 -0
- package/Sources/Combine/Helpers/SubscriptionStatus.swift +33 -0
- package/Sources/Combine/Helpers/SubscriptionTap.swift +33 -0
- package/Sources/Combine/Helpers/Utils.swift +30 -0
- package/Sources/Combine/Helpers/Violations.swift +33 -0
- package/Sources/Combine/ImmediateScheduler.swift +178 -0
- package/Sources/Combine/LICENSE +21 -0
- package/Sources/Combine/NotificationPublisher.swift +48 -0
- package/Sources/Combine/ObjectWillChange.swift +46 -0
- package/Sources/Combine/PassthroughSubject.swift +222 -0
- package/Sources/Combine/Publisher+Subscribe.swift +53 -0
- package/Sources/Combine/Publishers/Deferred.swift +48 -0
- package/Sources/Combine/Publishers/Empty.swift +54 -0
- package/Sources/Combine/Publishers/Fail.swift +42 -0
- package/Sources/Combine/Publishers/GENERATED-Publishers.Catch.swift +673 -0
- package/Sources/Combine/Publishers/GENERATED-Publishers.Encode.swift +388 -0
- package/Sources/Combine/Publishers/GENERATED-Publishers.MapKeyPath.swift +384 -0
- package/Sources/Combine/Publishers/Just.swift +313 -0
- package/Sources/Combine/Publishers/Optional.Publisher.swift +315 -0
- package/Sources/Combine/Publishers/Publishers.AllSatisfy.swift +217 -0
- package/Sources/Combine/Publishers/Publishers.AssertNoFailure.swift +158 -0
- package/Sources/Combine/Publishers/Publishers.Autoconnect.swift +193 -0
- package/Sources/Combine/Publishers/Publishers.Breakpoint.swift +231 -0
- package/Sources/Combine/Publishers/Publishers.Buffer.swift +356 -0
- package/Sources/Combine/Publishers/Publishers.Collect.swift +102 -0
- package/Sources/Combine/Publishers/Publishers.CollectByCount.swift +195 -0
- package/Sources/Combine/Publishers/Publishers.CompactMap.swift +241 -0
- package/Sources/Combine/Publishers/Publishers.Comparison.swift +371 -0
- package/Sources/Combine/Publishers/Publishers.Concatenate.swift +474 -0
- package/Sources/Combine/Publishers/Publishers.Contains.swift +293 -0
- package/Sources/Combine/Publishers/Publishers.Count.swift +87 -0
- package/Sources/Combine/Publishers/Publishers.Debounce.swift +332 -0
- package/Sources/Combine/Publishers/Publishers.Delay.swift +269 -0
- package/Sources/Combine/Publishers/Publishers.Drop.swift +155 -0
- package/Sources/Combine/Publishers/Publishers.DropUntilOutput.swift +283 -0
- package/Sources/Combine/Publishers/Publishers.DropWhile.swift +378 -0
- package/Sources/Combine/Publishers/Publishers.Filter.swift +248 -0
- package/Sources/Combine/Publishers/Publishers.First.swift +261 -0
- package/Sources/Combine/Publishers/Publishers.FlatMap.swift +533 -0
- package/Sources/Combine/Publishers/Publishers.HandleEvents.swift +260 -0
- package/Sources/Combine/Publishers/Publishers.IgnoreOutput.swift +109 -0
- package/Sources/Combine/Publishers/Publishers.Last.swift +252 -0
- package/Sources/Combine/Publishers/Publishers.MakeConnectable.swift +82 -0
- package/Sources/Combine/Publishers/Publishers.Map.swift +351 -0
- package/Sources/Combine/Publishers/Publishers.MapError.swift +140 -0
- package/Sources/Combine/Publishers/Publishers.MeasureInterval.swift +187 -0
- package/Sources/Combine/Publishers/Publishers.Multicast.swift +284 -0
- package/Sources/Combine/Publishers/Publishers.Output.swift +241 -0
- package/Sources/Combine/Publishers/Publishers.PrefixUntilOutput.swift +197 -0
- package/Sources/Combine/Publishers/Publishers.PrefixWhile.swift +176 -0
- package/Sources/Combine/Publishers/Publishers.Print.swift +206 -0
- package/Sources/Combine/Publishers/Publishers.ReceiveOn.swift +227 -0
- package/Sources/Combine/Publishers/Publishers.Reduce.swift +204 -0
- package/Sources/Combine/Publishers/Publishers.RemoveDuplicates.swift +264 -0
- package/Sources/Combine/Publishers/Publishers.ReplaceEmpty.swift +201 -0
- package/Sources/Combine/Publishers/Publishers.ReplaceError.swift +211 -0
- package/Sources/Combine/Publishers/Publishers.Retry.swift +274 -0
- package/Sources/Combine/Publishers/Publishers.Scan.swift +331 -0
- package/Sources/Combine/Publishers/Publishers.Sequence.swift +437 -0
- package/Sources/Combine/Publishers/Publishers.SetFailureType.swift +120 -0
- package/Sources/Combine/Publishers/Publishers.Share.swift +104 -0
- package/Sources/Combine/Publishers/Publishers.SubscribeOn.swift +197 -0
- package/Sources/Combine/Publishers/Publishers.SwitchToLatest.swift +444 -0
- package/Sources/Combine/Publishers/Publishers.Throttle.swift +359 -0
- package/Sources/Combine/Publishers/Publishers.Timeout.swift +301 -0
- package/Sources/Combine/Publishers/Publishers.Zip.swift +720 -0
- package/Sources/Combine/Publishers/Publishers.swift +13 -0
- package/Sources/Combine/Publishers/Record.swift +237 -0
- package/Sources/Combine/Publishers/Result.Publisher.swift +337 -0
- package/Sources/Combine/Result.swift +46 -0
- package/Sources/Combine/Scheduler.swift +89 -0
- package/Sources/Combine/Schedulers.swift +211 -0
- package/Sources/Combine/Subject+Void.swift +17 -0
- package/Sources/Combine/Subscriber+Void.swift +20 -0
- package/Sources/Combine/Subscribers/Subscribers.Assign.swift +175 -0
- package/Sources/Combine/Subscribers/Subscribers.Completion.swift +90 -0
- package/Sources/Combine/Subscribers/Subscribers.Demand.swift +476 -0
- package/Sources/Combine/Subscribers/Subscribers.Sink.swift +206 -0
- package/Sources/Combine/Subscribers/Subscribers.swift +9 -0
- package/Sources/Combine/Subscription.swift +22 -0
- package/Sources/Combine/Subscriptions.swift +42 -0
- package/Sources/Combine/SwiftUIObservation.swift +10 -0
- package/Sources/Combine/_Introspection.swift +107 -0
- package/Sources/CommonCrypto/CommonCrypto.swift +80 -0
- package/Sources/Compression/Compression.swift +365 -0
- package/Sources/Compression/Deflate.swift +407 -0
- package/Sources/Compression/Zlib.swift +179 -0
- package/Sources/CoreFoundationTypes/CoreFoundationTypes.swift +101 -0
- package/Sources/CoreGraphics/CoreGraphics.swift +411 -0
- package/Sources/CoreGraphics/ProjectionTransforms.swift +28 -0
- package/Sources/CoreHaptics/CoreHaptics.swift +399 -0
- package/Sources/CoreImage/CIColor.swift +41 -0
- package/Sources/CoreImage/CIContext.swift +31 -0
- package/Sources/CoreImage/CIDetector.swift +57 -0
- package/Sources/CoreImage/CIFilter.swift +243 -0
- package/Sources/CoreImage/CIFilterBuiltins.swift +136 -0
- package/Sources/CoreImage/CIImage.swift +162 -0
- package/Sources/CoreImage/QRCode.swift +393 -0
- package/Sources/CoreLocation/Coordinate.swift +30 -0
- package/Sources/CoreLocation/Location.swift +65 -0
- package/Sources/CoreML/Evaluation.swift +414 -0
- package/Sources/CoreML/FeatureValue.swift +266 -0
- package/Sources/CoreML/Model.swift +269 -0
- package/Sources/CoreML/MultiArray.swift +92 -0
- package/Sources/CoreML/Specification.swift +233 -0
- package/Sources/CoreText/CoreText.swift +64 -0
- package/Sources/CoreTransferable/CoreTransferable.swift +606 -0
- package/Sources/CryptoKit/ChaChaPoly.swift +258 -0
- package/Sources/CryptoKit/CryptoKit.swift +375 -0
- package/Sources/CryptoKit/Curve25519.swift +317 -0
- package/Sources/CryptoKit/P256.swift +321 -0
- package/Sources/FoundationBridge/AppStorage.swift +165 -0
- package/Sources/FoundationBridge/AssetResources.swift +90 -0
- package/Sources/FoundationBridge/AsyncBytes.swift +128 -0
- package/Sources/FoundationBridge/AsyncImage.swift +116 -0
- package/Sources/FoundationBridge/AttributedStrings.swift +16 -0
- package/Sources/FoundationBridge/Bundle.swift +119 -0
- package/Sources/FoundationBridge/CombineExports.swift +8 -0
- package/Sources/FoundationBridge/CoreFoundation.swift +77 -0
- package/Sources/FoundationBridge/Dispatch.swift +458 -0
- package/Sources/FoundationBridge/EnvironmentFoundation.swift +41 -0
- package/Sources/FoundationBridge/FileImporter.swift +128 -0
- package/Sources/FoundationBridge/FileSystem.swift +68 -0
- package/Sources/FoundationBridge/FoundationBridge.swift +29 -0
- package/Sources/FoundationBridge/HostResolution.swift +31 -0
- package/Sources/FoundationBridge/ItemProvider.swift +655 -0
- package/Sources/FoundationBridge/KeyValueObserving.swift +138 -0
- package/Sources/FoundationBridge/Links.swift +127 -0
- package/Sources/FoundationBridge/ListFormatter.swift +32 -0
- package/Sources/FoundationBridge/Locale.swift +86 -0
- package/Sources/FoundationBridge/Localization.swift +149 -0
- package/Sources/FoundationBridge/Locks.swift +50 -0
- package/Sources/FoundationBridge/Markdown.swift +527 -0
- package/Sources/FoundationBridge/MeasurementFormat.swift +95 -0
- package/Sources/FoundationBridge/NotificationCenter.swift +154 -0
- package/Sources/FoundationBridge/OpenURLHandlers.swift +60 -0
- package/Sources/FoundationBridge/Operation.swift +195 -0
- package/Sources/FoundationBridge/ProcessInfo.swift +32 -0
- package/Sources/FoundationBridge/RelativeDateTimeFormatter.swift +136 -0
- package/Sources/FoundationBridge/SceneStorage.swift +78 -0
- package/Sources/FoundationBridge/SecurityExports.swift +8 -0
- package/Sources/FoundationBridge/Selector.swift +226 -0
- package/Sources/FoundationBridge/StringComparators.swift +17 -0
- package/Sources/FoundationBridge/Table.swift +211 -0
- package/Sources/FoundationBridge/TextAttributes.swift +42 -0
- package/Sources/FoundationBridge/Timer.swift +53 -0
- package/Sources/FoundationBridge/UITextChecker.swift +30 -0
- package/Sources/FoundationBridge/URLSession.swift +718 -0
- package/Sources/FoundationBridge/UserDefaults.swift +183 -0
- package/Sources/FoundationBridge/WebAuthRedirects.swift +227 -0
- package/Sources/FoundationBridge/WebSocket.swift +187 -0
- package/Sources/FoundationModels/FoundationModels.swift +1098 -0
- package/Sources/ImageIO/GIF.swift +72 -0
- package/Sources/ImageIO/ImageIO.swift +248 -0
- package/Sources/ImageIO/ImageTypes.swift +28 -0
- package/Sources/LinkPresentation/LinkPresentation.swift +249 -0
- package/Sources/LocalAuthentication/LAContext.swift +86 -0
- package/Sources/MapKit/Geometry.swift +223 -0
- package/Sources/MapKit/Map.swift +638 -0
- package/Sources/MapKit/MapView.swift +229 -0
- package/Sources/MobileCoreServices/MobileCoreServices.swift +28 -0
- package/Sources/NaturalLanguage/LanguageIdentifier.swift +317 -0
- package/Sources/NaturalLanguage/NaturalLanguage.swift +135 -0
- package/Sources/OSLog/OSLog.swift +6 -0
- package/Sources/Photos/Photos.swift +572 -0
- package/Sources/PhotosUI/PhotosPicker.swift +248 -0
- package/Sources/PhotosUI/PhotosPickerItem.swift +261 -0
- package/Sources/QuickLook/QuickLook.swift +168 -0
- package/Sources/SafariServices/SafariServices.swift +106 -0
- package/Sources/Security/Security.swift +332 -0
- package/Sources/StoreKit/AppStore.swift +207 -0
- package/Sources/StoreKit/Products.swift +280 -0
- package/Sources/StoreKit/RequestReview.swift +28 -0
- package/Sources/StoreKit/StoreKit1.swift +486 -0
- package/Sources/StoreKit/StoreViews.swift +428 -0
- package/Sources/StoreKit/Transactions.swift +197 -0
- package/Sources/SwiftData/Macros.swift +58 -0
- package/Sources/SwiftData/ModelActor.swift +38 -0
- package/Sources/SwiftData/ModelContainer.swift +129 -0
- package/Sources/SwiftData/ModelContext.swift +451 -0
- package/Sources/SwiftData/PersistentModel.swift +238 -0
- package/Sources/SwiftData/Properties.swift +261 -0
- package/Sources/SwiftData/Query.swift +256 -0
- package/Sources/SwiftData/Schema.swift +192 -0
- package/Sources/SwiftData/Store.swift +146 -0
- package/Sources/SwiftData/StringPredicates.swift +125 -0
- package/Sources/SwiftDataMacros/EntryMacro.swift +82 -0
- package/Sources/SwiftDataMacros/GenerableMacro.swift +183 -0
- package/Sources/SwiftDataMacros/ModelActorMacro.swift +46 -0
- package/Sources/SwiftDataMacros/ModelMacro.swift +260 -0
- package/Sources/SwiftDataMacros/Plugin.swift +18 -0
- package/Sources/SwiftDataMacros/PredicateMacro.swift +1244 -0
- package/Sources/SwiftUI/API/Accessibility.swift +181 -0
- package/Sources/SwiftUI/API/AccessibilityExtras.swift +33 -0
- package/Sources/SwiftUI/API/AccessibilityFocus.swift +63 -0
- package/Sources/SwiftUI/API/Alert.swift +221 -0
- package/Sources/SwiftUI/API/AlignmentGuides.swift +65 -0
- package/Sources/SwiftUI/API/Animation.swift +285 -0
- package/Sources/SwiftUI/API/App.swift +87 -0
- package/Sources/SwiftUI/API/BindingCollections.swift +35 -0
- package/Sources/SwiftUI/API/BindingForEach.swift +27 -0
- package/Sources/SwiftUI/API/Bitmaps.swift +254 -0
- package/Sources/SwiftUI/API/ButtonStyles.swift +96 -0
- package/Sources/SwiftUI/API/Canvas.swift +337 -0
- package/Sources/SwiftUI/API/Color.swift +237 -0
- package/Sources/SwiftUI/API/ColorEffects.swift +52 -0
- package/Sources/SwiftUI/API/ColorPickerAndLabeledContent.swift +138 -0
- package/Sources/SwiftUI/API/ColorSpaces.swift +161 -0
- package/Sources/SwiftUI/API/Commands.swift +106 -0
- package/Sources/SwiftUI/API/ContainerBackground.swift +28 -0
- package/Sources/SwiftUI/API/ContentShape.swift +33 -0
- package/Sources/SwiftUI/API/ContentSizeCategory.swift +35 -0
- package/Sources/SwiftUI/API/ContentUnavailable.swift +101 -0
- package/Sources/SwiftUI/API/Controls.swift +300 -0
- package/Sources/SwiftUI/API/CustomLayout.swift +442 -0
- package/Sources/SwiftUI/API/CustomToolbarContent.swift +46 -0
- package/Sources/SwiftUI/API/Decorations.swift +92 -0
- package/Sources/SwiftUI/API/EditMode.swift +212 -0
- package/Sources/SwiftUI/API/Effects.swift +388 -0
- package/Sources/SwiftUI/API/Entry.swift +8 -0
- package/Sources/SwiftUI/API/Environment.swift +439 -0
- package/Sources/SwiftUI/API/EnvironmentSendable.swift +6 -0
- package/Sources/SwiftUI/API/Focus.swift +137 -0
- package/Sources/SwiftUI/API/Font.swift +131 -0
- package/Sources/SwiftUI/API/Gauge.swift +290 -0
- package/Sources/SwiftUI/API/Geometry.swift +89 -0
- package/Sources/SwiftUI/API/GeometryEffects.swift +232 -0
- package/Sources/SwiftUI/API/GeometryReader.swift +135 -0
- package/Sources/SwiftUI/API/GestureState.swift +65 -0
- package/Sources/SwiftUI/API/Gestures.swift +342 -0
- package/Sources/SwiftUI/API/Glass.swift +127 -0
- package/Sources/SwiftUI/API/Grid.swift +188 -0
- package/Sources/SwiftUI/API/GroupBoxAndControlGroup.swift +130 -0
- package/Sources/SwiftUI/API/KeyPress.swift +54 -0
- package/Sources/SwiftUI/API/LabelStyle.swift +124 -0
- package/Sources/SwiftUI/API/Layout.swift +220 -0
- package/Sources/SwiftUI/API/LazyStacks.swift +49 -0
- package/Sources/SwiftUI/API/LegacyAlert.swift +97 -0
- package/Sources/SwiftUI/API/Lists.swift +331 -0
- package/Sources/SwiftUI/API/Menu.swift +58 -0
- package/Sources/SwiftUI/API/Modifiers.swift +313 -0
- package/Sources/SwiftUI/API/MoreModifiers.swift +100 -0
- package/Sources/SwiftUI/API/Namespace.swift +44 -0
- package/Sources/SwiftUI/API/Navigation.swift +386 -0
- package/Sources/SwiftUI/API/NavigationDestinationBindings.swift +55 -0
- package/Sources/SwiftUI/API/ObservableObject.swift +293 -0
- package/Sources/SwiftUI/API/OnChange.swift +48 -0
- package/Sources/SwiftUI/API/Path.swift +543 -0
- package/Sources/SwiftUI/API/Phase8Controls.swift +411 -0
- package/Sources/SwiftUI/API/PhaseAnimator.swift +130 -0
- package/Sources/SwiftUI/API/Picker.swift +185 -0
- package/Sources/SwiftUI/API/PointerAndKeyboard.swift +77 -0
- package/Sources/SwiftUI/API/Popover.swift +41 -0
- package/Sources/SwiftUI/API/Preferences.swift +262 -0
- package/Sources/SwiftUI/API/Presentation.swift +122 -0
- package/Sources/SwiftUI/API/PresentationExtras.swift +60 -0
- package/Sources/SwiftUI/API/Primitives.swift +517 -0
- package/Sources/SwiftUI/API/Publishers.swift +107 -0
- package/Sources/SwiftUI/API/Redaction.swift +75 -0
- package/Sources/SwiftUI/API/RowActions.swift +84 -0
- package/Sources/SwiftUI/API/Scenes.swift +131 -0
- package/Sources/SwiftUI/API/ScrollModifiers.swift +132 -0
- package/Sources/SwiftUI/API/ScrollTargets.swift +104 -0
- package/Sources/SwiftUI/API/ScrollViewReader.swift +144 -0
- package/Sources/SwiftUI/API/SectionSupplements.swift +96 -0
- package/Sources/SwiftUI/API/Shape.swift +214 -0
- package/Sources/SwiftUI/API/ShapeStyle.swift +409 -0
- package/Sources/SwiftUI/API/ShapeTrim.swift +78 -0
- package/Sources/SwiftUI/API/Sharing.swift +193 -0
- package/Sources/SwiftUI/API/StackLayouts.swift +199 -0
- package/Sources/SwiftUI/API/State.swift +144 -0
- package/Sources/SwiftUI/API/StatusGaps.swift +121 -0
- package/Sources/SwiftUI/API/StrokeStyle.swift +30 -0
- package/Sources/SwiftUI/API/SymbolEffects.swift +203 -0
- package/Sources/SwiftUI/API/TabContent.swift +841 -0
- package/Sources/SwiftUI/API/Table.swift +382 -0
- package/Sources/SwiftUI/API/Tabs.swift +263 -0
- package/Sources/SwiftUI/API/Tasks.swift +35 -0
- package/Sources/SwiftUI/API/Text.swift +386 -0
- package/Sources/SwiftUI/API/TextDates.swift +61 -0
- package/Sources/SwiftUI/API/TextDecorations.swift +52 -0
- package/Sources/SwiftUI/API/TextFieldAxis.swift +103 -0
- package/Sources/SwiftUI/API/TextFieldCommit.swift +52 -0
- package/Sources/SwiftUI/API/Timeline.swift +212 -0
- package/Sources/SwiftUI/API/Toolbar.swift +251 -0
- package/Sources/SwiftUI/API/Transfer.swift +145 -0
- package/Sources/SwiftUI/API/UnevenRoundedRectangle.swift +66 -0
- package/Sources/SwiftUI/API/Video.swift +64 -0
- package/Sources/SwiftUI/API/View.swift +215 -0
- package/Sources/SwiftUI/API/ViewModifier.swift +43 -0
- package/Sources/SwiftUI/API/VisualEffect.swift +155 -0
- package/Sources/SwiftUI/API/Windows.swift +25 -0
- package/Sources/SwiftUI/Charts/Axis.swift +325 -0
- package/Sources/SwiftUI/Charts/AxisFormatsAndRanges.swift +68 -0
- package/Sources/SwiftUI/Charts/Chart.swift +660 -0
- package/Sources/SwiftUI/Charts/ChartContent.swift +376 -0
- package/Sources/SwiftUI/Charts/ChartProxy.swift +57 -0
- package/Sources/SwiftUI/Charts/Marks.swift +204 -0
- package/Sources/SwiftUI/Charts/Plottable.swift +99 -0
- package/Sources/SwiftUI/Charts/Scales.swift +326 -0
- package/Sources/SwiftUI/Localization/Localization.swift +498 -0
- package/Sources/SwiftUI/Localization/PluralRules.swift +127 -0
- package/Sources/SwiftUI/Runtime/Executor.swift +85 -0
- package/Sources/SwiftUI/Runtime/Exports.swift +101 -0
- package/Sources/SwiftUI/Runtime/ImageSource.swift +310 -0
- package/Sources/SwiftUI/Runtime/JSON.swift +143 -0
- package/Sources/SwiftUI/Runtime/JSONParser.swift +443 -0
- package/Sources/SwiftUI/Runtime/LazyRows.swift +99 -0
- package/Sources/SwiftUI/Runtime/Measurements.swift +140 -0
- package/Sources/SwiftUI/Runtime/Ops.swift +152 -0
- package/Sources/SwiftUI/Runtime/Reconciler.swift +219 -0
- package/Sources/SwiftUI/Runtime/Replay.swift +382 -0
- package/Sources/SwiftUI/Runtime/Runtime.swift +2146 -0
- package/Sources/SwiftUI/Runtime/Sameness.swift +152 -0
- package/Sources/SwiftUI/Runtime/ViewPath.swift +89 -0
- package/Sources/SwiftUI/Runtime/ViewTraits.swift +175 -0
- package/Sources/SwiftUI/Runtime/WebAuthHeadless.swift +49 -0
- package/Sources/Symbols/Symbols.swift +25 -0
- package/Sources/Translation/Translation.swift +423 -0
- package/Sources/UIKit/Activity.swift +213 -0
- package/Sources/UIKit/Alerts.swift +69 -0
- package/Sources/UIKit/AppDelegate.swift +222 -0
- package/Sources/UIKit/Appearance.swift +84 -0
- package/Sources/UIKit/ColorConversions.swift +70 -0
- package/Sources/UIKit/Controls.swift +225 -0
- package/Sources/UIKit/Device.swift +200 -0
- package/Sources/UIKit/Feedback.swift +65 -0
- package/Sources/UIKit/Font.swift +307 -0
- package/Sources/UIKit/Graphics.swift +204 -0
- package/Sources/UIKit/HostingController.swift +185 -0
- package/Sources/UIKit/Identity.swift +24 -0
- package/Sources/UIKit/Introspectable.swift +510 -0
- package/Sources/UIKit/Pasteboard.swift +363 -0
- package/Sources/UIKit/PhotoAlbum.swift +81 -0
- package/Sources/UIKit/RemoteNotifications.swift +15 -0
- package/Sources/UIKit/Representable.swift +288 -0
- package/Sources/UIKit/SafeArea.swift +12 -0
- package/Sources/UIKit/SceneDelegates.swift +132 -0
- package/Sources/UIKit/Scenes.swift +40 -0
- package/Sources/UIKit/ScrollView.swift +157 -0
- package/Sources/UIKit/ShortcutItems.swift +67 -0
- package/Sources/UIKit/SymbolConfiguration.swift +124 -0
- package/Sources/UIKit/TargetAction.swift +123 -0
- package/Sources/UIKit/TextPaste.swift +161 -0
- package/Sources/UIKit/TextView.swift +538 -0
- package/Sources/UIKit/UIKit.swift +292 -0
- package/Sources/UIKit/UIView.swift +855 -0
- package/Sources/UIKit/ViewTranslation.swift +311 -0
- package/Sources/UniformTypeIdentifiers/UTType.swift +196 -0
- package/Sources/UserNotifications/Content.swift +370 -0
- package/Sources/UserNotifications/NotificationCenter.swift +309 -0
- package/Sources/WeatherKit/WeatherKit.swift +228 -0
- package/Sources/WebKit/WebKit.swift +245 -0
- package/Sources/os/os.swift +376 -0
- package/Sources/zlib/zlib.swift +6 -0
- package/index.d.ts +2 -0
- package/index.mjs +16 -0
- package/manifest.d.mts +2 -0
- package/manifest.mjs +39 -0
- package/package.json +30 -4
- package/sync-sources.mjs +33 -0
|
@@ -0,0 +1,2146 @@
|
|
|
1
|
+
import Observation
|
|
2
|
+
|
|
3
|
+
#if canImport(WASILibc)
|
|
4
|
+
import WASILibc
|
|
5
|
+
#elseif canImport(Glibc)
|
|
6
|
+
import Glibc
|
|
7
|
+
#elseif canImport(Musl)
|
|
8
|
+
import Musl
|
|
9
|
+
#elseif canImport(Darwin)
|
|
10
|
+
import Darwin
|
|
11
|
+
#endif
|
|
12
|
+
|
|
13
|
+
/// A view that has been mounted. Composite views keep their `State` boxes here
|
|
14
|
+
/// so values survive the parent recreating the view struct.
|
|
15
|
+
@MainActor
|
|
16
|
+
final class _MountedNode {
|
|
17
|
+
var view: any View
|
|
18
|
+
var children: [_MountedNode] = []
|
|
19
|
+
var boxes: [any _AnyStateBox] = []
|
|
20
|
+
/// Identity within a keyed parent (`ForEach`).
|
|
21
|
+
var key: AnyHashable?
|
|
22
|
+
/// Position-and-type path used to restore state across hot reloads (ViewPath.swift).
|
|
23
|
+
var path: _ViewPath?
|
|
24
|
+
var environment = EnvironmentValues()
|
|
25
|
+
/// Handler supplied by the runtime (navigation links and screens).
|
|
26
|
+
var handler: ((_JSON) -> Void)?
|
|
27
|
+
var navTitle: String?
|
|
28
|
+
var navDisplayMode: String?
|
|
29
|
+
var depth = 0
|
|
30
|
+
var appeared = false
|
|
31
|
+
/// `.onDisappear`: run when the node leaves the tree.
|
|
32
|
+
var disappearAction: (@MainActor () -> Void)?
|
|
33
|
+
/// `.animation(_:value:)`: the last tracked value and a counter bumped on change.
|
|
34
|
+
var animationValue: Any?
|
|
35
|
+
var animationToken = 0
|
|
36
|
+
/// The render pass in which `animationToken` was last bumped.
|
|
37
|
+
var animationTokenPass = -1
|
|
38
|
+
/// Sheets: the mounted sheet content (second child) and its detents.
|
|
39
|
+
var sheetDetents: [String]?
|
|
40
|
+
/// `.task`: the running task and the id it was started for.
|
|
41
|
+
var task: Task<Void, Never>?
|
|
42
|
+
var taskID: AnyHashable?
|
|
43
|
+
var taskStarted = false
|
|
44
|
+
/// `GeometryReader`: the size the renderer last reported.
|
|
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
|
|
52
|
+
/// Phase 12: this stack is a `.scrollTargetLayout()`; its props carry `scrollTarget`.
|
|
53
|
+
var scrollTargetLayout = false
|
|
54
|
+
/// `TabView` / `Picker`: option tags in order, and the selected index.
|
|
55
|
+
var optionTags: [AnyHashable?] = []
|
|
56
|
+
var selectedIndex = 0
|
|
57
|
+
/// `NavigationStack`: destinations registered by descendants, kept across renders.
|
|
58
|
+
var destinations: [ObjectIdentifier: (Any) -> (any View)?] = [:]
|
|
59
|
+
var pickerOptions: [String] = []
|
|
60
|
+
var pickerLabel = ""
|
|
61
|
+
/// `NavigationSplitView`: the sidebar's `List(selection:)` binding, registered
|
|
62
|
+
/// each pass while the sidebar is reconciled.
|
|
63
|
+
var splitSelection: _ListSelection?
|
|
64
|
+
/// `NavigationStack` nested in another stack's screen: rendered in place.
|
|
65
|
+
/// Also a sidebar `TabView`, which renders its split view in place.
|
|
66
|
+
var navFlattened = false
|
|
67
|
+
/// `navscreen` / `List`: the edit mode this node owns (`EditButton` toggles it).
|
|
68
|
+
var editMode = EditMode.inactive
|
|
69
|
+
/// `NavigationStack` / compact `NavigationSplitView`: the stack context its
|
|
70
|
+
/// screens and links share. Kept across passes: a clean pass skips the
|
|
71
|
+
/// phase that recomputes `detailShown`, and screen handlers capture it.
|
|
72
|
+
var navContext: _NavContext?
|
|
73
|
+
|
|
74
|
+
// Incremental rendering (Phase 9). A render re-runs a view's body only
|
|
75
|
+
// when the node is `dirty` (its own `@State`, observed object or
|
|
76
|
+
// `@Observable` read changed) or an ancestor re-ran (its view value is
|
|
77
|
+
// new); a clean node whose descendants are all clean is skipped whole,
|
|
78
|
+
// and the elements it produced last time are reused.
|
|
79
|
+
weak var parent: _MountedNode?
|
|
80
|
+
/// This node's body must run again.
|
|
81
|
+
var dirty = true
|
|
82
|
+
/// A descendant is dirty (the chain from the root to every dirty node).
|
|
83
|
+
var subtreeDirty = false
|
|
84
|
+
/// The elements this node produced last time, with the sheets and alerts
|
|
85
|
+
/// its subtree presented (they are collected as side effects).
|
|
86
|
+
var cachedElements: [_Element]?
|
|
87
|
+
var cachedSheets: [_Element] = []
|
|
88
|
+
var cachedAlerts: [_Element] = []
|
|
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
|
+
|
|
108
|
+
init(view: any View) {
|
|
109
|
+
self.view = view
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// The enclosing `NavigationStack`, shared by everything inside it.
|
|
114
|
+
@MainActor
|
|
115
|
+
final class _NavContext {
|
|
116
|
+
var box: _StateBox<[_PushedScreen]>
|
|
117
|
+
var path: _PathBinding?
|
|
118
|
+
/// The stack's mounted node, which owns the destination registry.
|
|
119
|
+
unowned let node: _MountedNode
|
|
120
|
+
|
|
121
|
+
init(box: _StateBox<[_PushedScreen]>, path: _PathBinding?, node: _MountedNode) {
|
|
122
|
+
self.box = box
|
|
123
|
+
self.path = path
|
|
124
|
+
self.node = node
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func push(_ view: any View) { box.value.append(_PushedScreen(view: view)) }
|
|
128
|
+
|
|
129
|
+
/// Pushes a value: onto the path binding when there is one, otherwise as
|
|
130
|
+
/// a resolved destination view.
|
|
131
|
+
func push(value: AnyHashable) {
|
|
132
|
+
if let path {
|
|
133
|
+
path.set(path.get() + [value])
|
|
134
|
+
} else {
|
|
135
|
+
push(destination(for: value))
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func register(_ registration: any _DestinationRegistering) {
|
|
140
|
+
node.destinations[registration._destinationType] = { registration._destination(for: $0) }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func destination(for value: AnyHashable) -> any View {
|
|
144
|
+
let type = ObjectIdentifier(Swift.type(of: value.base))
|
|
145
|
+
if let builder = node.destinations[type], let view = builder(value.base) { return view }
|
|
146
|
+
return Text("No navigationDestination(for: \(Swift.type(of: value.base)).self)")
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// Compact `NavigationSplitView`: set to the split view's node. While
|
|
150
|
+
/// `detailShown`, depth 1 is the detail screen, whose pop clears the
|
|
151
|
+
/// sidebar's selection; pushed screens follow it.
|
|
152
|
+
var splitNode: _MountedNode?
|
|
153
|
+
var detailShown = false
|
|
154
|
+
|
|
155
|
+
/// Pops so that `depth` screens below the popped one remain (`depth` 1 pops to the root).
|
|
156
|
+
func pop(to depth: Int) {
|
|
157
|
+
if let splitNode {
|
|
158
|
+
if detailShown, depth <= 1 {
|
|
159
|
+
box.value.removeAll()
|
|
160
|
+
splitNode.splitSelection?.set(nil)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
let pushedDepth = depth - (detailShown ? 1 : 0)
|
|
164
|
+
guard pushedDepth >= 1, box.value.count >= pushedDepth else { return }
|
|
165
|
+
box.value.removeSubrange((pushedDepth - 1)...)
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
let pathCount = path?.get().count ?? 0
|
|
169
|
+
if depth <= pathCount, let path {
|
|
170
|
+
path.set(Array(path.get().prefix(depth - 1)))
|
|
171
|
+
box.value.removeAll()
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
let pushedDepth = depth - pathCount
|
|
175
|
+
guard pushedDepth >= 1, box.value.count >= pushedDepth else { return }
|
|
176
|
+
box.value.removeSubrange((pushedDepth - 1)...)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
struct _ReconcileContext {
|
|
181
|
+
var environment: EnvironmentValues
|
|
182
|
+
/// `Type/index:Type/...` from the root (ViewPath.swift); keys state snapshots.
|
|
183
|
+
var path: _ViewPath
|
|
184
|
+
var navStack: _NavContext?
|
|
185
|
+
var screen: _MountedNode?
|
|
186
|
+
/// The nearest enclosing presented sheet's node (for `presentationDetents`).
|
|
187
|
+
var sheet: _MountedNode?
|
|
188
|
+
/// The `NavigationSplitView` whose sidebar is being reconciled: its
|
|
189
|
+
/// `List(selection:)` registers with it.
|
|
190
|
+
var split: _MountedNode?
|
|
191
|
+
/// Inside a `TabView` tab that is not selected: titles stay put.
|
|
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
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// Owns the mounted tree, schedules renders and routes events.
|
|
242
|
+
@MainActor
|
|
243
|
+
public final class _Runtime {
|
|
244
|
+
public static let shared = _Runtime()
|
|
245
|
+
|
|
246
|
+
private(set) var host: any _RenderHost = _BufferedHost.shared
|
|
247
|
+
private var rootView: (any View)?
|
|
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
|
|
253
|
+
private let reconciler = _Reconciler()
|
|
254
|
+
private var needsRender = false
|
|
255
|
+
/// The next render re-runs every body (environment changes, `invalidate()`).
|
|
256
|
+
private var fullRender = true
|
|
257
|
+
private var rendering = false
|
|
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] = []
|
|
261
|
+
/// Values the renderer controls, such as the color scheme.
|
|
262
|
+
private var baseEnvironment = EnvironmentValues() {
|
|
263
|
+
didSet { EnvironmentValues._page = baseEnvironment }
|
|
264
|
+
}
|
|
265
|
+
/// `.preferredColorScheme` seen in the last pass (sticky), and what the host was told.
|
|
266
|
+
private var preferredColorScheme: ColorScheme?
|
|
267
|
+
private var sentColorScheme: ColorScheme?
|
|
268
|
+
/// State values from a previous build, applied as views first mount.
|
|
269
|
+
private var restoredState: [String: _JSON] = [:]
|
|
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
|
|
280
|
+
/// Sheet elements collected during the current element pass, emitted after the tree.
|
|
281
|
+
private var presentedSheets: [_Element] = []
|
|
282
|
+
/// Alert elements collected during the current element pass, emitted after
|
|
283
|
+
/// the sheets so that an alert presented from a sheet covers it.
|
|
284
|
+
private var presentedAlerts: [_Element] = []
|
|
285
|
+
|
|
286
|
+
init() {}
|
|
287
|
+
|
|
288
|
+
// MARK: Entry points
|
|
289
|
+
|
|
290
|
+
/// Called from `App.main()`.
|
|
291
|
+
public func launch<A: App>(_ app: A) {
|
|
292
|
+
watchApp(app)
|
|
293
|
+
guard let scene = app.body as? any _RootScene else {
|
|
294
|
+
fatalError("\(A.Body.self) is not a scene the runtime can present. Use WindowGroup.")
|
|
295
|
+
}
|
|
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
|
+
}
|
|
301
|
+
if let state = _environment("SB_STATE"), let json = try? _JSON.parse(state), case .object(let values) = json {
|
|
302
|
+
restoredState = values
|
|
303
|
+
}
|
|
304
|
+
mount(scene._rootView, host: headless ? _HeadlessHost() : _BufferedHost.shared)
|
|
305
|
+
_flushAfterUpdate()
|
|
306
|
+
|
|
307
|
+
if headless {
|
|
308
|
+
// A script that names an element not on screen stops the run.
|
|
309
|
+
if !replay(events: _environment("SB_EVENTS") ?? "") { exit(1) }
|
|
310
|
+
if let limit = _environment("SB_RUN_MS").flatMap(Double.init), limit > 0 {
|
|
311
|
+
runHeadlessClock(upTo: limit)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
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
|
+
|
|
366
|
+
/// Sends the app's defaults to the host (a `defaults` op), which keeps them
|
|
367
|
+
/// for the next launch. Called by `UserDefaults` (Sources/FoundationBridge).
|
|
368
|
+
public func storeDefaults(_ values: _JSON) {
|
|
369
|
+
host.apply([.defaults(values)])
|
|
370
|
+
}
|
|
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
|
+
|
|
577
|
+
/// Drives the executor with a virtual clock: runs jobs, jumps to the next
|
|
578
|
+
/// timer, and stops when none remain or the clock would pass `limit` ms.
|
|
579
|
+
private func runHeadlessClock(upTo limit: Double) {
|
|
580
|
+
let executor = _HostExecutor.shared
|
|
581
|
+
var guardCount = 0
|
|
582
|
+
while guardCount < 10_000 {
|
|
583
|
+
guardCount += 1
|
|
584
|
+
let next = runJobsAndRender()
|
|
585
|
+
if next < 0 { break }
|
|
586
|
+
let nextMs = Double(executor.nowNs + UInt64(next)) / 1_000_000
|
|
587
|
+
if nextMs > limit { break }
|
|
588
|
+
executor.nowNs += UInt64(next)
|
|
589
|
+
host.apply([.event(.object(["type": .string("clock"), "ms": .number(nextMs.rounded())]))])
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/// Mounts `view` as the root and performs the first render.
|
|
594
|
+
func mount(_ view: any View, host: any _RenderHost) {
|
|
595
|
+
self.host = host
|
|
596
|
+
sentImageSources = []
|
|
597
|
+
rootView = view
|
|
598
|
+
root = nil
|
|
599
|
+
measuringRoot = nil
|
|
600
|
+
render()
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/// A tap on the element with this id.
|
|
604
|
+
public func dispatch(tap id: Int) {
|
|
605
|
+
dispatch(event: .object(["type": .string("tap"), "id": .number(Double(id))]))
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/// Handles one renderer event (see docs/ops-protocol.md) and renders if needed.
|
|
609
|
+
public func dispatch(event: _JSON) {
|
|
610
|
+
handle(event)
|
|
611
|
+
renderIfNeeded()
|
|
612
|
+
_flushAfterUpdate()
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/// Applies an event's state changes without rendering. A `batch` event
|
|
616
|
+
/// (`{"type": "batch", "events": [...]}`) applies each of its events, so
|
|
617
|
+
/// one host layout pass that measured several `geometry` and `layout`
|
|
618
|
+
/// elements costs one render instead of one per element.
|
|
619
|
+
private func handle(_ event: _JSON) {
|
|
620
|
+
guard case .object(let fields) = event, case .string(let type)? = fields["type"] else { return }
|
|
621
|
+
let id: Int? = if case .number(let n)? = fields["id"] { Int(n) } else { nil }
|
|
622
|
+
|
|
623
|
+
switch type {
|
|
624
|
+
case "batch":
|
|
625
|
+
if case .array(let events)? = fields["events"] { events.forEach(handle) }
|
|
626
|
+
case "tap":
|
|
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.
|
|
638
|
+
if let id, let value = fields["value"] { reconciler.handlers[id]?(value) }
|
|
639
|
+
case "geometry":
|
|
640
|
+
if let id, let width = fields["width"], let height = fields["height"] {
|
|
641
|
+
reconciler.handlers[id]?(.object(["width": width, "height": height]))
|
|
642
|
+
}
|
|
643
|
+
case "layout":
|
|
644
|
+
// Host measurement of a custom `Layout`'s children; the handler
|
|
645
|
+
// parses the whole event (see CustomLayout.swift).
|
|
646
|
+
if let id { reconciler.handlers[id]?(event) }
|
|
647
|
+
case "drag", "longpress", "delete", "refresh", "focus", "submit", "video", "lazy", "menu":
|
|
648
|
+
// Phase 8: the handler receives the whole event and reads `type`
|
|
649
|
+
// (a `listrow` handles both `tap` and `delete`; Phase 12: a
|
|
650
|
+
// `textfield` handles `text`, `focus` and `submit`; a `video`
|
|
651
|
+
// reads its `event`, docs/ops-protocol.md, "Video and audio").
|
|
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) }
|
|
656
|
+
case "environment":
|
|
657
|
+
if case .string(let scheme)? = fields["colorScheme"] {
|
|
658
|
+
baseEnvironment.colorScheme = scheme == "dark" ? .dark : .light
|
|
659
|
+
invalidate()
|
|
660
|
+
}
|
|
661
|
+
if case .string(let size)? = fields["dynamicTypeSize"], let value = DynamicTypeSize(name: size) {
|
|
662
|
+
baseEnvironment.dynamicTypeSize = value
|
|
663
|
+
invalidate()
|
|
664
|
+
}
|
|
665
|
+
if case .string(let name)? = fields["horizontalSizeClass"], let value = UserInterfaceSizeClass(name: name) {
|
|
666
|
+
baseEnvironment.horizontalSizeClass = value
|
|
667
|
+
invalidate()
|
|
668
|
+
}
|
|
669
|
+
if case .string(let name)? = fields["scenePhase"], let value = ScenePhase(name: name) {
|
|
670
|
+
baseEnvironment.scenePhase = value
|
|
671
|
+
invalidate()
|
|
672
|
+
}
|
|
673
|
+
// The app's `@Environment` properties read the new values.
|
|
674
|
+
refreshApp?()
|
|
675
|
+
default:
|
|
676
|
+
break
|
|
677
|
+
}
|
|
678
|
+
}
|
|
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
|
+
|
|
692
|
+
/// Schedules a render that re-evaluates the whole tree.
|
|
693
|
+
func invalidate() {
|
|
694
|
+
needsRender = true
|
|
695
|
+
fullRender = true
|
|
696
|
+
recordTransaction()
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/// Schedules a render that re-evaluates `node`'s subtree (and whatever
|
|
700
|
+
/// else is dirty); everything else is reused.
|
|
701
|
+
func invalidate(_ node: _MountedNode) {
|
|
702
|
+
markDirty(node)
|
|
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 }
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private func markDirty(_ node: _MountedNode) {
|
|
713
|
+
node.dirty = true
|
|
714
|
+
node.cachedElements = nil
|
|
715
|
+
var ancestor = node.parent
|
|
716
|
+
while let current = ancestor, !current.subtreeDirty {
|
|
717
|
+
current.subtreeDirty = true
|
|
718
|
+
current.cachedElements = nil
|
|
719
|
+
ancestor = current.parent
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
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
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/// Advances the executor's clock, runs queued jobs and due timers, renders
|
|
755
|
+
/// if anything changed, and returns milliseconds until the next timer (-1: none).
|
|
756
|
+
public func runJobs(nowMilliseconds: Double) -> Double {
|
|
757
|
+
let executor = _HostExecutor.shared
|
|
758
|
+
executor.nowNs = UInt64(max(0, nowMilliseconds) * 1_000_000)
|
|
759
|
+
let next = runJobsAndRender()
|
|
760
|
+
return next < 0 ? -1 : Double(next) / 1_000_000
|
|
761
|
+
}
|
|
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
|
+
|
|
781
|
+
func renderIfNeeded() {
|
|
782
|
+
if needsRender, !rendering { render() }
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// MARK: Rendering
|
|
786
|
+
|
|
787
|
+
private func render() {
|
|
788
|
+
guard let rootView else { return }
|
|
789
|
+
rendering = true
|
|
790
|
+
needsRender = false
|
|
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)))
|
|
799
|
+
let force = fullRender
|
|
800
|
+
fullRender = false
|
|
801
|
+
let node = reconcile(root, with: rootView, context: context, force: force)
|
|
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
|
|
811
|
+
presentedSheets = []
|
|
812
|
+
presentedAlerts = []
|
|
813
|
+
barFree.nextPass()
|
|
814
|
+
// Bodies are tracked per node (see `reconcileChildren`); an observable
|
|
815
|
+
// read while producing elements (rare) falls back to a full render.
|
|
816
|
+
let tree = withObservationTracking {
|
|
817
|
+
elements(of: node) + presentedSheets + presentedAlerts + (measuringNode.map { elements(of: $0) } ?? [])
|
|
818
|
+
} onChange: { [weak self] in
|
|
819
|
+
MainActor.assumeIsolated { self?.invalidate() }
|
|
820
|
+
}
|
|
821
|
+
let animation = passAnimation?._json
|
|
822
|
+
passAnimation = nil
|
|
823
|
+
var ops = reconciler.update(with: tree, animation: animation)
|
|
824
|
+
if preferredColorScheme != sentColorScheme {
|
|
825
|
+
// Phase 12: `.preferredColorScheme` reaches the host as a root update.
|
|
826
|
+
sentColorScheme = preferredColorScheme
|
|
827
|
+
ops.insert(.update(id: 0, props: .object(["colorScheme": preferredColorScheme.map { .string($0 == .dark ? "dark" : "light") } ?? .null])), at: 0)
|
|
828
|
+
}
|
|
829
|
+
host.apply(ops)
|
|
830
|
+
rendering = false
|
|
831
|
+
_ViewMeasurements.shared.renderFinished()
|
|
832
|
+
|
|
833
|
+
let disappearing = pendingDisappear
|
|
834
|
+
pendingDisappear.removeAll()
|
|
835
|
+
for action in disappearing { action() }
|
|
836
|
+
let appearing = pendingAppear
|
|
837
|
+
pendingAppear.removeAll()
|
|
838
|
+
for action in appearing { action() }
|
|
839
|
+
renderIfNeeded()
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/// Re-evaluates the mounted tree against `view`, reusing nodes (and their
|
|
843
|
+
/// state) whose view type is unchanged at the same position or key.
|
|
844
|
+
/// `force`: the parent re-ran, so `view` is a new value and this node's
|
|
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.
|
|
850
|
+
private func reconcile(_ existing: _MountedNode?, with view: any View, context: _ReconcileContext,
|
|
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
|
+
}
|
|
859
|
+
if let existing, !force, !existing.dirty, !existing.subtreeDirty {
|
|
860
|
+
return existing
|
|
861
|
+
}
|
|
862
|
+
let received = _ReceivedContext(context)
|
|
863
|
+
var context = context
|
|
864
|
+
let node: _MountedNode
|
|
865
|
+
let clean: Bool
|
|
866
|
+
let properties: [Any]?
|
|
867
|
+
let boxes: [any _AnyStateBox]
|
|
868
|
+
let path = context.path
|
|
869
|
+
|
|
870
|
+
if let existing, type(of: existing.view) == type(of: view) {
|
|
871
|
+
node = existing
|
|
872
|
+
clean = !force && !existing.dirty
|
|
873
|
+
if clean {
|
|
874
|
+
// Same view value, same state: no reflection, no body.
|
|
875
|
+
properties = nil
|
|
876
|
+
boxes = existing.boxes
|
|
877
|
+
} else {
|
|
878
|
+
properties = _reflectedProperties(of: view)
|
|
879
|
+
boxes = stateBoxes(in: properties)
|
|
880
|
+
for (old, new) in zip(existing.boxes, boxes) where old !== new {
|
|
881
|
+
new._adopt(old)
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
} else {
|
|
885
|
+
node = _MountedNode(view: view)
|
|
886
|
+
clean = false
|
|
887
|
+
properties = _reflectedProperties(of: view)
|
|
888
|
+
boxes = stateBoxes(in: properties)
|
|
889
|
+
restoreState(into: boxes, at: path)
|
|
890
|
+
}
|
|
891
|
+
node.dirty = false
|
|
892
|
+
node.subtreeDirty = false
|
|
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
|
|
899
|
+
node.view = view
|
|
900
|
+
node.path = path
|
|
901
|
+
node.received = received
|
|
902
|
+
if !clean {
|
|
903
|
+
node.boxes = boxes
|
|
904
|
+
for box in boxes {
|
|
905
|
+
// Weak: a `StateObject`'s publisher keeps this closure for as long
|
|
906
|
+
// as the object lives, which may be longer than the runtime (tests).
|
|
907
|
+
box._onChange = { [weak self, weak node] in
|
|
908
|
+
if let node { self?.invalidate(node) }
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
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 {
|
|
925
|
+
modifier._apply(to: &context.environment)
|
|
926
|
+
}
|
|
927
|
+
if traits.contains(.preferredColorScheme), let preferred = view as? any _PreferredColorSchemeModifying, !context.inactiveTab {
|
|
928
|
+
preferredColorScheme = preferred._preferredColorScheme
|
|
929
|
+
}
|
|
930
|
+
if context.environment._scrollTargetLayout, traits.contains(.scrollTargetContainer) {
|
|
931
|
+
// This stack's children are the snap targets; nested stacks are not.
|
|
932
|
+
node.scrollTargetLayout = true
|
|
933
|
+
context.environment._scrollTargetLayout = false
|
|
934
|
+
}
|
|
935
|
+
if traits.contains(.navigationTitle), let title = view as? any _NavigationTitleModifying, let screen = context.screen, !context.inactiveTab {
|
|
936
|
+
if let value = title._title { screen.navTitle = value }
|
|
937
|
+
if let mode = title._displayMode { screen.navDisplayMode = mode }
|
|
938
|
+
}
|
|
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 {
|
|
943
|
+
node.appeared = true
|
|
944
|
+
if let action = appear._action { pendingAppear.append(action) }
|
|
945
|
+
}
|
|
946
|
+
if traits.contains(.detents), let detents = view as? any _DetentsModifying, let sheet = context.sheet {
|
|
947
|
+
sheet.sheetDetents = detents._detents
|
|
948
|
+
}
|
|
949
|
+
if traits.contains(.animated), let animated = view as? any _AnimatedView {
|
|
950
|
+
if node === existing, animated._changed(since: node.animationValue) {
|
|
951
|
+
node.animationToken += 1
|
|
952
|
+
node.animationTokenPass = renderPass
|
|
953
|
+
}
|
|
954
|
+
node.animationValue = animated._value
|
|
955
|
+
}
|
|
956
|
+
if traits.contains(.destinationRegistering), let registration = view as? any _DestinationRegistering {
|
|
957
|
+
context.navStack?.register(registration)
|
|
958
|
+
}
|
|
959
|
+
if traits.contains(.splitSidebar), let sidebar = view as? _SplitSidebarView {
|
|
960
|
+
context.split = sidebar.split
|
|
961
|
+
}
|
|
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())")
|
|
965
|
+
context.environment._listSelection = selection
|
|
966
|
+
context.split?.splitSelection = selection
|
|
967
|
+
}
|
|
968
|
+
// A list outside a navigation screen owns its rows' edit mode.
|
|
969
|
+
if context.environment.editMode == nil {
|
|
970
|
+
context.environment.editMode = editModeBinding(for: node)
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
if traits.contains(.taskStarting), let starting = view as? any _TaskStarting {
|
|
974
|
+
if !node.taskStarted || node.taskID != starting._taskID {
|
|
975
|
+
node.task?.cancel()
|
|
976
|
+
node.taskStarted = true
|
|
977
|
+
node.taskID = starting._taskID
|
|
978
|
+
let action = starting._taskAction
|
|
979
|
+
node.task = Task { await action() }
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Decide how to produce the children first, because navigation views
|
|
984
|
+
// adjust the environment that must be resolved before any body runs.
|
|
985
|
+
var makeChildren: () -> [any View]
|
|
986
|
+
/// A second batch of children, produced after the first was reconciled
|
|
987
|
+
/// (a compact split view decides on its detail screen from the
|
|
988
|
+
/// selection its sidebar registered).
|
|
989
|
+
var makeMoreChildren: (() -> [any View])?
|
|
990
|
+
var keys: [AnyHashable]?
|
|
991
|
+
node.handler = nil
|
|
992
|
+
node.navFlattened = false
|
|
993
|
+
|
|
994
|
+
if traits.contains(.navigationSplit), let split = view as? any _NavigationSplitHost {
|
|
995
|
+
// Descendants fill these in as they are reconciled; a clean pass
|
|
996
|
+
// (only some descendants re-run) keeps what the skipped ones set.
|
|
997
|
+
if !clean { node.splitSelection = nil }
|
|
998
|
+
let sidebar = _SplitSidebarView(content: split._sidebar, split: node)
|
|
999
|
+
if context.environment.horizontalSizeClass == .regular {
|
|
1000
|
+
// Two columns, each its own stack; they are not nested in a
|
|
1001
|
+
// screen even when the split view is, so they are not flattened.
|
|
1002
|
+
context.navStack = nil
|
|
1003
|
+
context.screen = nil
|
|
1004
|
+
let detail = split._detail
|
|
1005
|
+
makeChildren = {
|
|
1006
|
+
let sidebarStack = NavigationStack {
|
|
1007
|
+
_NavigationTitleView(content: AnyView(sidebar), title: nil, displayMode: "large")
|
|
1008
|
+
}
|
|
1009
|
+
let detailStack: any View = detail is any _NavigationStackView
|
|
1010
|
+
? detail : NavigationStack { AnyView(detail) }
|
|
1011
|
+
return [sidebarStack, detailStack]
|
|
1012
|
+
}
|
|
1013
|
+
} else {
|
|
1014
|
+
// One stack rooted at the sidebar; the detail is pushed while
|
|
1015
|
+
// the sidebar's selection is set, and deeper links push after it.
|
|
1016
|
+
let nav = navContext(for: node, box: split._pushedBox, path: nil, clean: clean)
|
|
1017
|
+
nav.splitNode = node
|
|
1018
|
+
context.navStack = nav
|
|
1019
|
+
makeChildren = { [_NavScreenView(content: sidebar, depth: 0)] }
|
|
1020
|
+
let detail = split._detail
|
|
1021
|
+
makeMoreChildren = {
|
|
1022
|
+
nav.detailShown = node.splitSelection?.get() != nil
|
|
1023
|
+
let base = nav.detailShown ? 1 : 0
|
|
1024
|
+
let detailScreens = nav.detailShown ? [_NavScreenView(content: detail, depth: 1)] : []
|
|
1025
|
+
let pushedScreens = nav.box.value.enumerated().map { index, pushed in
|
|
1026
|
+
_NavScreenView(content: pushed.view, depth: base + index + 1)
|
|
1027
|
+
}
|
|
1028
|
+
return detailScreens + pushedScreens
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
} else if traits.contains(.navigationStack), let stack = view as? any _NavigationStackView, context.screen != nil, context.navStack != nil {
|
|
1032
|
+
// Nested in another stack's screen: the root renders in place, its
|
|
1033
|
+
// destinations and links use the enclosing stack, its path is ignored.
|
|
1034
|
+
node.navFlattened = true
|
|
1035
|
+
makeChildren = { [stack._root] }
|
|
1036
|
+
} else if traits.contains(.navigationStack), let stack = view as? any _NavigationStackView {
|
|
1037
|
+
let nav = navContext(for: node, box: stack._pushedBox, path: stack._path, clean: clean)
|
|
1038
|
+
context.navStack = nav
|
|
1039
|
+
makeChildren = {
|
|
1040
|
+
let pathScreens = (nav.path?.get() ?? []).enumerated().map { index, value in
|
|
1041
|
+
_NavScreenView(content: _PathScreenValue(value: value), depth: index + 1)
|
|
1042
|
+
}
|
|
1043
|
+
let pushedScreens = nav.box.value.enumerated().map { index, pushed in
|
|
1044
|
+
_NavScreenView(content: pushed.view, depth: pathScreens.count + index + 1)
|
|
1045
|
+
}
|
|
1046
|
+
return [_NavScreenView(content: stack._root, depth: 0)] + pathScreens + pushedScreens
|
|
1047
|
+
}
|
|
1048
|
+
} else if traits.contains(.navScreen), let screen = view as? _NavScreenView {
|
|
1049
|
+
context.screen = node
|
|
1050
|
+
// A screen inside an unselected tab still owns its own title.
|
|
1051
|
+
context.inactiveTab = false
|
|
1052
|
+
if !clean {
|
|
1053
|
+
node.navTitle = nil
|
|
1054
|
+
node.navDisplayMode = nil
|
|
1055
|
+
}
|
|
1056
|
+
node.depth = screen.depth
|
|
1057
|
+
if screen.depth > 0, let nav = context.navStack {
|
|
1058
|
+
let depth = screen.depth
|
|
1059
|
+
node.handler = { _ in nav.pop(to: depth) }
|
|
1060
|
+
context.environment.dismiss = DismissAction(identity: _RuntimeKey(node: ObjectIdentifier(node), role: "pop\(depth)")) {
|
|
1061
|
+
nav.pop(to: depth)
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
// Each screen owns the edit mode of its content (`EditButton`,
|
|
1065
|
+
// `List.editing`), as on iOS.
|
|
1066
|
+
context.environment.editMode = editModeBinding(for: node)
|
|
1067
|
+
let nav = context.navStack
|
|
1068
|
+
makeChildren = {
|
|
1069
|
+
// Path screens resolve their value through the stack's
|
|
1070
|
+
// registrations, which the root screen filled in just before.
|
|
1071
|
+
if let pathValue = screen.content as? _PathScreenValue, let nav {
|
|
1072
|
+
return [nav.destination(for: pathValue.value)]
|
|
1073
|
+
}
|
|
1074
|
+
return [screen.content]
|
|
1075
|
+
}
|
|
1076
|
+
} else if traits.contains(.navigationLink), let link = view as? any _NavigationLinkView {
|
|
1077
|
+
let destination = link._destination
|
|
1078
|
+
let value = traits.contains(.navigationValueLink) ? (view as? any _NavigationValueLinkView)?._value : nil
|
|
1079
|
+
let nav = context.navStack
|
|
1080
|
+
// In a split view's sidebar a value link selects the row of a
|
|
1081
|
+
// `List(selection:)` instead of pushing.
|
|
1082
|
+
let selection = context.environment._splitSidebar ? context.environment._listSelection : nil
|
|
1083
|
+
node.handler = { _ in
|
|
1084
|
+
if let value, let selection {
|
|
1085
|
+
selection.set(value)
|
|
1086
|
+
} else if let value {
|
|
1087
|
+
nav?.push(value: value)
|
|
1088
|
+
} else {
|
|
1089
|
+
nav?.push(destination)
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
makeChildren = { (view as? any _HostView)?._content ?? [] }
|
|
1093
|
+
} else if traits.contains(.tabView), let tabs = view as? any _TabViewHost {
|
|
1094
|
+
let roots = _flattenChildren(tabs._tabContent)
|
|
1095
|
+
node.optionTags = roots.map { _tag(of: $0) }
|
|
1096
|
+
node.selectedIndex = tabs._selectedIndex(tags: node.optionTags)
|
|
1097
|
+
let tags = node.optionTags
|
|
1098
|
+
node.handler = { payload in
|
|
1099
|
+
if case .number(let index) = payload { tabs._select(index: Int(index), tags: tags) }
|
|
1100
|
+
}
|
|
1101
|
+
let selected = node.selectedIndex
|
|
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
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
} else if traits.contains(.tabScreen), let tab = view as? _TabScreenView {
|
|
1115
|
+
if !tab.isSelected { context.inactiveTab = true }
|
|
1116
|
+
makeChildren = { [tab.content] }
|
|
1117
|
+
} else if traits.contains(.picker), let picker = view as? any _PickerHost {
|
|
1118
|
+
let options = _flattenPickerOptions(picker._options)
|
|
1119
|
+
node.optionTags = options.map { $0.tag }
|
|
1120
|
+
node.selectedIndex = picker._selectedIndex(tags: node.optionTags)
|
|
1121
|
+
node.pickerOptions = options.map { _textContent(of: $0.view) }
|
|
1122
|
+
node.pickerLabel = _textContent(of: picker._labelView)
|
|
1123
|
+
let tags = node.optionTags
|
|
1124
|
+
node.handler = { payload in
|
|
1125
|
+
if case .number(let index) = payload { picker._select(index: Int(index), tags: tags) }
|
|
1126
|
+
}
|
|
1127
|
+
makeChildren = { [] }
|
|
1128
|
+
} else if traits.contains(.geometryReading), let reader = view as? any _GeometryReading {
|
|
1129
|
+
let size = node.geometrySize
|
|
1130
|
+
node.handler = { [unowned self] payload in
|
|
1131
|
+
guard case .object(let fields) = payload,
|
|
1132
|
+
case .number(let width)? = fields["width"],
|
|
1133
|
+
case .number(let height)? = fields["height"] else { return }
|
|
1134
|
+
let reported = CGSize(width: width, height: height)
|
|
1135
|
+
if reported != node.geometrySize {
|
|
1136
|
+
node.geometrySize = reported
|
|
1137
|
+
self.invalidate(node)
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
makeChildren = { [reader._content(for: size)] }
|
|
1141
|
+
} else if traits.contains(.sheet), let sheet = view as? any _SheetPresenting {
|
|
1142
|
+
// The base is always a child; the sheet content is a second child
|
|
1143
|
+
// while presented, so its state survives re-renders.
|
|
1144
|
+
let isPresented = sheet._isPresented
|
|
1145
|
+
let onDismiss = sheet._onDismiss
|
|
1146
|
+
if !clean { node.sheetDetents = nil }
|
|
1147
|
+
node.handler = { _ in
|
|
1148
|
+
isPresented.wrappedValue = false
|
|
1149
|
+
onDismiss?()
|
|
1150
|
+
}
|
|
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 {
|
|
1155
|
+
// Like a sheet: the actions are a second child while presented. A
|
|
1156
|
+
// tap on the alert itself dismisses without running an action.
|
|
1157
|
+
let isPresented = alert._isPresented
|
|
1158
|
+
node.handler = { _ in isPresented.wrappedValue = false }
|
|
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())]
|
|
1166
|
+
}
|
|
1167
|
+
} else if traits.contains(.group), let group = view as? any _GroupView {
|
|
1168
|
+
keys = group._keys
|
|
1169
|
+
makeChildren = { group._children }
|
|
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
|
+
}
|
|
1210
|
+
} else {
|
|
1211
|
+
makeChildren = { [self.body(of: view)] }
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
if node.handler == nil, traits.contains(.refreshing), let refreshing = view as? any _Refreshing {
|
|
1215
|
+
node.handler = refreshing._refresh(in: context.environment).handler
|
|
1216
|
+
}
|
|
1217
|
+
if node.handler == nil, traits.contains(.environmentHandling), let handling = view as? any _EnvironmentHandling {
|
|
1218
|
+
node.handler = handling._handler(in: context.environment)
|
|
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
|
+
}
|
|
1236
|
+
|
|
1237
|
+
node.environment = context.environment
|
|
1238
|
+
let decorating = traits.contains(.preferenceDecorating)
|
|
1239
|
+
if clean && !decorating {
|
|
1240
|
+
// The body's output is the children already mounted (the view
|
|
1241
|
+
// value is unchanged); only the dirty ones among them re-run.
|
|
1242
|
+
let kept = node.children
|
|
1243
|
+
makeChildren = { kept.map(\.view) }
|
|
1244
|
+
makeMoreChildren = nil
|
|
1245
|
+
keys = kept.allSatisfy { $0.key != nil } && !kept.isEmpty ? kept.map { $0.key! } : nil
|
|
1246
|
+
} else {
|
|
1247
|
+
resolveEnvironment(in: properties, with: context.environment, owner: node)
|
|
1248
|
+
}
|
|
1249
|
+
var phases = [makeChildren]
|
|
1250
|
+
if let makeMoreChildren { phases.append(makeMoreChildren) }
|
|
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 }
|
|
1267
|
+
return node
|
|
1268
|
+
}
|
|
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
|
+
|
|
1300
|
+
/// The stack context of a `NavigationStack` / split node, created once and
|
|
1301
|
+
/// pointed at the current view's storage on every pass that re-ran the body.
|
|
1302
|
+
private func navContext(for node: _MountedNode, box: _StateBox<[_PushedScreen]>, path: _PathBinding?,
|
|
1303
|
+
clean: Bool) -> _NavContext {
|
|
1304
|
+
if let nav = node.navContext {
|
|
1305
|
+
if !clean {
|
|
1306
|
+
nav.box = box
|
|
1307
|
+
nav.path = path
|
|
1308
|
+
}
|
|
1309
|
+
return nav
|
|
1310
|
+
}
|
|
1311
|
+
let nav = _NavContext(box: box, path: path, node: node)
|
|
1312
|
+
node.navContext = nav
|
|
1313
|
+
return nav
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/// Reconciles the node's children phase by phase: each phase's views are
|
|
1317
|
+
/// produced only after the previous phase's children were reconciled.
|
|
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).
|
|
1321
|
+
private func reconcileChildren(of node: _MountedNode, phases: [() -> [any View]],
|
|
1322
|
+
keys: [AnyHashable]?, context: _ReconcileContext, force: Bool,
|
|
1323
|
+
forceLaterPhases: Bool = false) {
|
|
1324
|
+
let old = node.children
|
|
1325
|
+
var oldByKey: [AnyHashable: _MountedNode] = [:]
|
|
1326
|
+
for child in old {
|
|
1327
|
+
if let key = child.key { oldByKey[key] = child }
|
|
1328
|
+
}
|
|
1329
|
+
defer {
|
|
1330
|
+
let kept = Set(node.children.map(ObjectIdentifier.init))
|
|
1331
|
+
for dropped in old where !kept.contains(ObjectIdentifier(dropped)) {
|
|
1332
|
+
unmount(dropped)
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
var children: [_MountedNode] = []
|
|
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 }
|
|
1341
|
+
// The body (and a `ForEach`'s row closures) run under observation
|
|
1342
|
+
// tracking: an `@Observable` property read here marks this node
|
|
1343
|
+
// dirty when it changes, and nothing else re-renders.
|
|
1344
|
+
let views: [any View] = force
|
|
1345
|
+
? withObservationTracking(makeViews) { [weak self, weak node] in
|
|
1346
|
+
MainActor.assumeIsolated { if let node { self?.invalidate(node) } }
|
|
1347
|
+
}
|
|
1348
|
+
: makeViews()
|
|
1349
|
+
for childView in views {
|
|
1350
|
+
let index = children.count
|
|
1351
|
+
let key = phase == 0 ? keys?[index] : nil
|
|
1352
|
+
let existing: _MountedNode?
|
|
1353
|
+
if let key {
|
|
1354
|
+
existing = oldByKey.removeValue(forKey: key)
|
|
1355
|
+
} else if index < old.count, old[index].key == nil {
|
|
1356
|
+
existing = old[index]
|
|
1357
|
+
} else {
|
|
1358
|
+
existing = nil
|
|
1359
|
+
}
|
|
1360
|
+
var childContext = context
|
|
1361
|
+
childContext.path = context.path.child(key.map(_ViewPath.Step.key) ?? .index(index), type(of: childView))
|
|
1362
|
+
if index == 1, let sheet = node.view as? any _SheetPresenting {
|
|
1363
|
+
let isPresented = sheet._isPresented
|
|
1364
|
+
let onDismiss = sheet._onDismiss
|
|
1365
|
+
childContext.sheet = node
|
|
1366
|
+
childContext.navStack = nil
|
|
1367
|
+
childContext.screen = nil
|
|
1368
|
+
childContext.environment.dismiss = DismissAction(identity: _RuntimeKey(node: ObjectIdentifier(node), role: "sheet")) {
|
|
1369
|
+
isPresented.wrappedValue = false
|
|
1370
|
+
onDismiss?()
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
if index == 1, let alert = node.view as? any _AlertPresenting {
|
|
1374
|
+
let isPresented = alert._isPresented
|
|
1375
|
+
childContext.environment.dismiss = DismissAction(identity: _RuntimeKey(node: ObjectIdentifier(node), role: "alert")) {
|
|
1376
|
+
isPresented.wrappedValue = false
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
let child = reconcile(existing, with: childView, context: childContext, force: force)
|
|
1380
|
+
child.key = key
|
|
1381
|
+
child.parent = node
|
|
1382
|
+
children.append(child)
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
node.children = children
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/// The `editMode` environment binding a `navscreen` or `List` node owns.
|
|
1389
|
+
private func editModeBinding(for node: _MountedNode) -> Binding<EditMode> {
|
|
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)"),
|
|
1395
|
+
get: { [weak node] in MainActor.assumeIsolated { node?.editMode ?? .inactive } },
|
|
1396
|
+
set: { [weak self, weak node] mode in
|
|
1397
|
+
MainActor.assumeIsolated {
|
|
1398
|
+
guard let node, node.editMode != mode else { return }
|
|
1399
|
+
node.editMode = mode
|
|
1400
|
+
self?.invalidate(node)
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
)
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/// Cancels tasks in a subtree that left the tree, and queues its
|
|
1407
|
+
/// `.onDisappear` actions (innermost first, as SwiftUI calls them).
|
|
1408
|
+
private func unmount(_ node: _MountedNode) {
|
|
1409
|
+
node.task?.cancel()
|
|
1410
|
+
node.task = nil
|
|
1411
|
+
node.children.forEach(unmount)
|
|
1412
|
+
if let action = node.disappearAction {
|
|
1413
|
+
node.disappearAction = nil
|
|
1414
|
+
pendingDisappear.append(action)
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
private func body(of view: any View) -> any View {
|
|
1419
|
+
func open<V: View>(_ view: V) -> any View { view.body }
|
|
1420
|
+
return open(view)
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
/// The property-wrapper values of a view that the runtime acts on
|
|
1424
|
+
/// (`@State`, `@StateObject`, `@ObservedObject`, `@Environment`,
|
|
1425
|
+
/// `@EnvironmentObject`), or `nil` when its type declares none.
|
|
1426
|
+
///
|
|
1427
|
+
/// Reflection is the most expensive step of a render, and most views in a
|
|
1428
|
+
/// tree are the shim's own modifiers and primitives, which declare no
|
|
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.
|
|
1435
|
+
private func _reflectedProperties(of view: any View) -> [Any]? {
|
|
1436
|
+
// `ModifiedContent` stores its `ViewModifier`, whose own `@State` and
|
|
1437
|
+
// `@Environment` properties are managed like the view's.
|
|
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
|
|
1468
|
+
}
|
|
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
|
|
1483
|
+
}
|
|
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)
|
|
1491
|
+
}
|
|
1492
|
+
return values
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
private func stateBoxes(in properties: [Any]?) -> [any _AnyStateBox] {
|
|
1496
|
+
guard let properties else { return [] }
|
|
1497
|
+
return properties.compactMap { ($0 as? any _StateProperty)?._box }
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/// Resolves `@Environment`/`@EnvironmentObject` properties and subscribes
|
|
1501
|
+
/// `@ObservedObject`/`@EnvironmentObject` properties so that their
|
|
1502
|
+
/// object's `objectWillChange` invalidates this runtime. Resolution runs
|
|
1503
|
+
/// first because an environment object is observed through its resolved value.
|
|
1504
|
+
private func resolveEnvironment(in properties: [Any]?, with environment: EnvironmentValues, owner node: _MountedNode) {
|
|
1505
|
+
guard let properties else { return }
|
|
1506
|
+
for value in properties {
|
|
1507
|
+
(value as? any _EnvironmentProperty)?._resolve(in: environment)
|
|
1508
|
+
(value as? any _EnvironmentReading)?._resolve(in: environment)
|
|
1509
|
+
(value as? any _ObservingProperty)?._observe(owner: node) { [weak self, weak node] in
|
|
1510
|
+
if let node { self?.invalidate(node) }
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// MARK: Elements
|
|
1516
|
+
|
|
1517
|
+
private func elements(of node: _MountedNode) -> [_Element] {
|
|
1518
|
+
if let cached = node.cachedElements {
|
|
1519
|
+
presentedSheets += node.cachedSheets
|
|
1520
|
+
presentedAlerts += node.cachedAlerts
|
|
1521
|
+
return cached
|
|
1522
|
+
}
|
|
1523
|
+
let sheetsBefore = presentedSheets.count
|
|
1524
|
+
let alertsBefore = presentedAlerts.count
|
|
1525
|
+
let produced = produceElements(of: node)
|
|
1526
|
+
node.cachedElements = produced
|
|
1527
|
+
node.cachedSheets = Array(presentedSheets[sheetsBefore...])
|
|
1528
|
+
node.cachedAlerts = Array(presentedAlerts[alertsBefore...])
|
|
1529
|
+
return produced
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
private func produceElements(of node: _MountedNode) -> [_Element] {
|
|
1533
|
+
var produced: [_Element]
|
|
1534
|
+
let traits = _ViewTraits.of(node.view)
|
|
1535
|
+
if traits.contains(.navScreen) {
|
|
1536
|
+
// `.toolbar` / `.searchable` emit `toolbar` and `searchfield`
|
|
1537
|
+
// elements wherever they were applied; the renderer wants them as
|
|
1538
|
+
// the screen's own children, after the content.
|
|
1539
|
+
var bar: [_Element] = []
|
|
1540
|
+
let content = node.children.flatMap(elements(of:)).map { _hoistBarItems(from: $0, into: &bar, barFree: &barFree) }
|
|
1541
|
+
produced = [_Element(
|
|
1542
|
+
kind: "navscreen",
|
|
1543
|
+
props: .object([
|
|
1544
|
+
"title": .string(node.navTitle ?? ""),
|
|
1545
|
+
"displayMode": .string(node.navDisplayMode ?? "automatic"),
|
|
1546
|
+
"depth": .number(Double(node.depth)),
|
|
1547
|
+
]),
|
|
1548
|
+
children: content + bar,
|
|
1549
|
+
handler: node.handler
|
|
1550
|
+
)]
|
|
1551
|
+
} else if traits.contains(.sheet), let sheet = node.view as? any _SheetPresenting {
|
|
1552
|
+
// Base inline; the presented content becomes a root-level `sheet` element.
|
|
1553
|
+
produced = node.children.first.map(elements(of:)) ?? []
|
|
1554
|
+
if node.children.count > 1 {
|
|
1555
|
+
let detents = sheet._fullScreen ? ["full"] : (node.sheetDetents ?? ["large"])
|
|
1556
|
+
presentedSheets.append(_Element(
|
|
1557
|
+
kind: "sheet",
|
|
1558
|
+
props: .object(["detents": .array(detents.map(_JSON.string))]),
|
|
1559
|
+
children: elements(of: node.children[1]),
|
|
1560
|
+
handler: node.handler,
|
|
1561
|
+
key: AnyHashable(_SheetKey(path: node.path?.text ?? ""))
|
|
1562
|
+
))
|
|
1563
|
+
}
|
|
1564
|
+
} else if traits.contains(.alert), let alert = node.view as? any _AlertPresenting {
|
|
1565
|
+
// Base inline; the actions become a root-level `alert` element whose
|
|
1566
|
+
// buttons also dismiss when tapped.
|
|
1567
|
+
produced = node.children.first.map(elements(of:)) ?? []
|
|
1568
|
+
if node.children.count > 1 {
|
|
1569
|
+
let isPresented = alert._isPresented
|
|
1570
|
+
let buttons = elements(of: node.children[1]).map { _dismissing($0, with: isPresented) }
|
|
1571
|
+
presentedAlerts.append(_Element(
|
|
1572
|
+
kind: "alert",
|
|
1573
|
+
props: .object([
|
|
1574
|
+
"title": .string(alert._alertTitle),
|
|
1575
|
+
"message": alert._alertMessage.map(_JSON.string) ?? .null,
|
|
1576
|
+
"style": .string(alert._alertStyle),
|
|
1577
|
+
]),
|
|
1578
|
+
children: buttons,
|
|
1579
|
+
handler: node.handler,
|
|
1580
|
+
key: AnyHashable(_AlertKey(path: node.path?.text ?? ""))
|
|
1581
|
+
))
|
|
1582
|
+
}
|
|
1583
|
+
} else if traits.contains(.navigationSplit) {
|
|
1584
|
+
// Regular: `navsplit` over the two column stacks. Compact: one
|
|
1585
|
+
// `navstack` whose screens are the sidebar, the detail and what
|
|
1586
|
+
// the detail pushed.
|
|
1587
|
+
let regular = node.environment.horizontalSizeClass == .regular
|
|
1588
|
+
produced = [_Element(
|
|
1589
|
+
kind: regular ? "navsplit" : "navstack",
|
|
1590
|
+
props: .object([:]),
|
|
1591
|
+
children: node.children.flatMap(elements(of:)),
|
|
1592
|
+
handler: nil
|
|
1593
|
+
)]
|
|
1594
|
+
} else if traits.contains(.navigationStack), node.navFlattened {
|
|
1595
|
+
produced = node.children.flatMap(elements(of:))
|
|
1596
|
+
} else if traits.contains(.tabView), node.navFlattened {
|
|
1597
|
+
produced = node.children.flatMap(elements(of:))
|
|
1598
|
+
} else if traits.contains(.tabView) {
|
|
1599
|
+
produced = [_Element(
|
|
1600
|
+
kind: "tabview",
|
|
1601
|
+
props: .object(["selected": .number(Double(node.selectedIndex))]),
|
|
1602
|
+
children: node.children.flatMap(elements(of:)),
|
|
1603
|
+
handler: node.handler
|
|
1604
|
+
)]
|
|
1605
|
+
} else if traits.contains(.picker) {
|
|
1606
|
+
produced = [_Element(
|
|
1607
|
+
kind: "picker",
|
|
1608
|
+
props: .object([
|
|
1609
|
+
"style": .string(node.environment._pickerStyle),
|
|
1610
|
+
"label": .string(node.pickerLabel),
|
|
1611
|
+
"options": .array(node.pickerOptions.map(_JSON.string)),
|
|
1612
|
+
"selected": .number(Double(node.selectedIndex)),
|
|
1613
|
+
]),
|
|
1614
|
+
children: [],
|
|
1615
|
+
handler: node.handler
|
|
1616
|
+
)]
|
|
1617
|
+
} else if traits.contains(.animated), let animated = node.view as? any _AnimatedView {
|
|
1618
|
+
var style: [String: _JSON] = ["animationToken": .number(Double(node.animationToken))]
|
|
1619
|
+
if let animation = animated._animation { style["animation"] = animation._json }
|
|
1620
|
+
produced = [_Element(
|
|
1621
|
+
kind: "styled",
|
|
1622
|
+
props: .object(["style": .object(style)]),
|
|
1623
|
+
children: node.children.flatMap(elements(of:)),
|
|
1624
|
+
handler: nil
|
|
1625
|
+
)]
|
|
1626
|
+
} else if traits.contains(.host), !traits.contains(.group), let hostView = node.view as? any _HostView {
|
|
1627
|
+
let kind = hostView._kind
|
|
1628
|
+
var children = childElements(of: node)
|
|
1629
|
+
if kind == "list" { children = wrapListRows(children, in: node) }
|
|
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) }
|
|
1642
|
+
if node.scrollTargetLayout, case .object(var fields) = props {
|
|
1643
|
+
// Phase 12: this stack's children are the scroll view's snap targets.
|
|
1644
|
+
fields["scrollTarget"] = .bool(true)
|
|
1645
|
+
props = .object(fields)
|
|
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
|
+
}
|
|
1651
|
+
var element = _Element(
|
|
1652
|
+
kind: kind,
|
|
1653
|
+
props: props,
|
|
1654
|
+
children: children,
|
|
1655
|
+
handler: node.handler ?? hostView._handler
|
|
1656
|
+
)
|
|
1657
|
+
if traits.contains(.navigationValueLink), let link = node.view as? any _NavigationValueLinkView, let value = link._value {
|
|
1658
|
+
element.row = _RowMark(linkValue: value, linkHandler: element.handler)
|
|
1659
|
+
}
|
|
1660
|
+
// Wrappers that leave the row's content as their (first) child pass
|
|
1661
|
+
// its row mark up, so `Text("x").tag(1).padding()` stays selectable.
|
|
1662
|
+
let transparent = kind == "styled" || kind == "gesture" || kind == "tapgesture"
|
|
1663
|
+
? children.count == 1 : kind == "swipeactions" || kind == "contextmenu"
|
|
1664
|
+
if transparent, let inner = children.first?.row {
|
|
1665
|
+
element.row = (element.row ?? _RowMark()).merged(with: inner)
|
|
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
|
+
}
|
|
1673
|
+
produced = [element]
|
|
1674
|
+
} else {
|
|
1675
|
+
produced = childElements(of: node)
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
if traits.contains(.tagProviding), let tagged = node.view as? any _TagProviding {
|
|
1679
|
+
let tag = tagged._tag
|
|
1680
|
+
for index in produced.indices where produced[index].row?.tag == nil {
|
|
1681
|
+
produced[index].row = (produced[index].row ?? _RowMark()).merged(with: _RowMark(tag: tag))
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
if traits.contains(.rowEditingDisabling), let disabling = node.view as? any _RowEditingDisabling, disabling._deleteDisabled {
|
|
1685
|
+
for index in produced.indices {
|
|
1686
|
+
produced[index].row = (produced[index].row ?? _RowMark()).merged(with: _RowMark(deleteDisabled: true))
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
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 {
|
|
1703
|
+
// An unselected tab's toolbars do not reach the enclosing screen.
|
|
1704
|
+
var dropped: [_Element] = []
|
|
1705
|
+
produced = produced.map { _hoistBarItems(from: $0, into: &dropped, barFree: &barFree) }
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
if let key = node.key {
|
|
1709
|
+
for index in produced.indices where produced[index].key == nil {
|
|
1710
|
+
produced[index].key = AnyHashable(_ElementKey(base: key, index: index))
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
return produced
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// MARK: State snapshot (hot reload)
|
|
1717
|
+
|
|
1718
|
+
/// The elements of a node's children, in order. The elements a keyed
|
|
1719
|
+
/// group (`ForEach`) produces carry its ids as keys; two keyed groups
|
|
1720
|
+
/// under the same parent may use the same ids (`ForEach(0..<n)` twice in
|
|
1721
|
+
/// a `ZStack`), so each group's keys are qualified by its position among
|
|
1722
|
+
/// the siblings. Without that the element diff matched the two groups'
|
|
1723
|
+
/// elements to each other on alternate passes, swapping their props.
|
|
1724
|
+
private func childElements(of node: _MountedNode) -> [_Element] {
|
|
1725
|
+
let delete = _ViewTraits.of(node.view).contains(.deletableGroup) ? (node.view as? any _DeletableGroup)?._delete : nil
|
|
1726
|
+
return node.children.enumerated().flatMap { position, child -> [_Element] in
|
|
1727
|
+
var items = elements(of: child)
|
|
1728
|
+
// A `ForEach` id is the row's implicit tag; an `onDelete` group's
|
|
1729
|
+
// child at `position` is the row at that offset of its data.
|
|
1730
|
+
if let key = child.key {
|
|
1731
|
+
for index in items.indices where items[index].row?.tag == nil {
|
|
1732
|
+
items[index].row = (items[index].row ?? _RowMark()).merged(with: _RowMark(tag: key))
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
if let delete {
|
|
1736
|
+
for index in items.indices where items[index].row?.delete == nil {
|
|
1737
|
+
items[index].row = (items[index].row ?? _RowMark()).merged(with: _RowMark(delete: { delete(position) }))
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
if child.key == nil, _ViewTraits.of(child.view).contains(.group), let group = child.view as? any _GroupView, group._keys != nil {
|
|
1741
|
+
for index in items.indices {
|
|
1742
|
+
if let key = items[index].key {
|
|
1743
|
+
items[index].key = AnyHashable(_GroupKey(position: position, key: key))
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return items
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/// Wraps a `list`'s rows (also those of its `section`s) that can be selected
|
|
1752
|
+
/// or deleted in `listrow` elements (docs/ops-protocol.md, Phase 8). A row
|
|
1753
|
+
/// is selectable when the list has a selection and the row's `.tag`,
|
|
1754
|
+
/// `ForEach` id or `NavigationLink(value:)` is of the selection's type;
|
|
1755
|
+
/// deletable when its `ForEach` has `onDelete` and the row is not
|
|
1756
|
+
/// `deleteDisabled`. The row's key moves to the wrapper.
|
|
1757
|
+
private func wrapListRows(_ elements: [_Element], in node: _MountedNode) -> [_Element] {
|
|
1758
|
+
let selection = node.environment._listSelection
|
|
1759
|
+
let editMode = node.environment.editMode
|
|
1760
|
+
return elements.map { element in
|
|
1761
|
+
if element.kind == "section" {
|
|
1762
|
+
var section = element
|
|
1763
|
+
section.children = wrapListRows(section.children, in: node)
|
|
1764
|
+
return section
|
|
1765
|
+
}
|
|
1766
|
+
guard let mark = element.row else { return element }
|
|
1767
|
+
let value = mark.linkValue ?? mark.tag
|
|
1768
|
+
let selectable: Bool = if let selection, let value { selection.accepts(value) } else { false }
|
|
1769
|
+
let deletable = mark.delete != nil && !mark.deleteDisabled
|
|
1770
|
+
guard selectable || deletable else { return element }
|
|
1771
|
+
let selected = selectable && selection!.contains(value!)
|
|
1772
|
+
|
|
1773
|
+
var inner = element
|
|
1774
|
+
inner.key = nil
|
|
1775
|
+
inner.row = nil
|
|
1776
|
+
let handler: (_JSON) -> Void = { payload in
|
|
1777
|
+
if case .object(let fields) = payload, case .string("delete")? = fields["type"] {
|
|
1778
|
+
if deletable { mark.delete?() }
|
|
1779
|
+
return
|
|
1780
|
+
}
|
|
1781
|
+
guard selectable, let selection, let value else {
|
|
1782
|
+
mark.linkHandler?(.null)
|
|
1783
|
+
return
|
|
1784
|
+
}
|
|
1785
|
+
let editing = editMode?.wrappedValue.isEditing ?? false
|
|
1786
|
+
if mark.linkValue != nil, !editing {
|
|
1787
|
+
// A value link pushes (or, in a split sidebar, selects) as before.
|
|
1788
|
+
mark.linkHandler?(.null)
|
|
1789
|
+
} else if selection.isMultiple || editing {
|
|
1790
|
+
selection.toggle(value)
|
|
1791
|
+
} else {
|
|
1792
|
+
selection.set(value)
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
return _Element(
|
|
1796
|
+
kind: "listrow",
|
|
1797
|
+
props: .object([
|
|
1798
|
+
"selectable": .bool(selectable),
|
|
1799
|
+
"selected": .bool(selected),
|
|
1800
|
+
"deletable": .bool(deletable),
|
|
1801
|
+
]),
|
|
1802
|
+
children: [inner],
|
|
1803
|
+
handler: handler,
|
|
1804
|
+
key: element.key
|
|
1805
|
+
)
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
/// Primitive `@State` values keyed by view path, for `SB_STATE`.
|
|
1810
|
+
public func snapshot() -> _JSON {
|
|
1811
|
+
var values: [String: _JSON] = [:]
|
|
1812
|
+
func walk(_ node: _MountedNode) {
|
|
1813
|
+
for (index, box) in node.boxes.enumerated() {
|
|
1814
|
+
if let json = _primitiveJSON(box._anyValue) {
|
|
1815
|
+
values["\(node.path?.text ?? "")#\(index)"] = json
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
node.children.forEach(walk)
|
|
1819
|
+
}
|
|
1820
|
+
if let root { walk(root) }
|
|
1821
|
+
return .object(values)
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
private func restoreState(into boxes: [any _AnyStateBox], at path: _ViewPath) {
|
|
1825
|
+
guard !restoredState.isEmpty, !boxes.isEmpty else { return }
|
|
1826
|
+
let text = path.text
|
|
1827
|
+
for (index, box) in boxes.enumerated() {
|
|
1828
|
+
guard let saved = restoredState["\(text)#\(index)"] else { continue }
|
|
1829
|
+
switch (box._anyValue, saved) {
|
|
1830
|
+
case (is Int, .number(let n)) where n == n.rounded(): box._anyValue = Int(n)
|
|
1831
|
+
case (is Double, .number(let n)): box._anyValue = n
|
|
1832
|
+
case (is String, .string(let s)): box._anyValue = s
|
|
1833
|
+
case (is Bool, .bool(let b)): box._anyValue = b
|
|
1834
|
+
default: break
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// MARK: Headless replay
|
|
1840
|
+
|
|
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)
|
|
1857
|
+
continue
|
|
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
|
+
}
|
|
1866
|
+
host.apply([.event(.object(event))])
|
|
1867
|
+
dispatch(event: .object(event))
|
|
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
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
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
|
|
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)
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
/// Views that run an action when first mounted.
|
|
1979
|
+
protocol _OnAppearing: _TraitBearing {
|
|
1980
|
+
var _action: (@MainActor () -> Void)? { get }
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
extension _OnAppearView: _OnAppearing {
|
|
1984
|
+
var _action: (@MainActor () -> Void)? { action }
|
|
1985
|
+
}
|
|
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
|
+
|
|
1996
|
+
func _primitiveJSON(_ value: Any) -> _JSON? {
|
|
1997
|
+
switch value {
|
|
1998
|
+
case let v as Int: .number(Double(v))
|
|
1999
|
+
case let v as Double: .number(v)
|
|
2000
|
+
case let v as String: .string(v)
|
|
2001
|
+
case let v as Bool: .bool(v)
|
|
2002
|
+
default: nil
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
/// The WASI environment (`SB_*` variables the host sets); nil when unset.
|
|
2007
|
+
public func _environment(_ name: String) -> String? {
|
|
2008
|
+
guard let value = getenv(name) else { return nil }
|
|
2009
|
+
return String(cString: value)
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
/// Identity for a sheet element at the root, so re-renders keep its id.
|
|
2013
|
+
struct _SheetKey: Hashable {
|
|
2014
|
+
let path: String
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
/// Identity for an alert element at the root.
|
|
2018
|
+
struct _AlertKey: Hashable {
|
|
2019
|
+
let path: String
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
/// The element with every `button` in its subtree made to also set
|
|
2023
|
+
/// `isPresented` to false when tapped (an alert's action buttons dismiss it).
|
|
2024
|
+
private func _dismissing(_ element: _Element, with isPresented: Binding<Bool>) -> _Element {
|
|
2025
|
+
var copy = element
|
|
2026
|
+
if element.kind == "button" {
|
|
2027
|
+
let action = element.handler
|
|
2028
|
+
copy.handler = { payload in
|
|
2029
|
+
action?(payload)
|
|
2030
|
+
isPresented.wrappedValue = false
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
copy.children = element.children.map { _dismissing($0, with: isPresented) }
|
|
2034
|
+
return copy
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// MARK: - Tab and picker helpers
|
|
2038
|
+
|
|
2039
|
+
/// Flattens structural groups (tuples, optionals, conditionals, ForEach) into
|
|
2040
|
+
/// the views they contribute, keeping ForEach ids as tags when asked.
|
|
2041
|
+
@MainActor
|
|
2042
|
+
func _flattenChildren(_ view: any View, keys: Bool) -> [(view: any View, tag: AnyHashable?)] {
|
|
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),
|
|
2045
|
+
!(view is any _TabItemProviding), !(view is any _TagProviding) {
|
|
2046
|
+
let children = group._children
|
|
2047
|
+
let ids = keys ? group._keys : nil
|
|
2048
|
+
return children.enumerated().flatMap { index, child -> [(view: any View, tag: AnyHashable?)] in
|
|
2049
|
+
let nested = _flattenChildren(child, keys: keys)
|
|
2050
|
+
guard let ids else { return nested }
|
|
2051
|
+
return nested.map { ($0.view, $0.tag ?? ids[index]) }
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
return [(view, _tag(of: view))]
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
@MainActor
|
|
2058
|
+
func _flattenChildren(_ view: any View) -> [any View] {
|
|
2059
|
+
_flattenChildren(view, keys: false).map(\.view)
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
/// The `.tag` applied to a view, looking through other wrappers.
|
|
2063
|
+
@MainActor
|
|
2064
|
+
func _tag(of view: any View) -> AnyHashable? {
|
|
2065
|
+
if let tagged = view as? any _TagProviding { return tagged._tag }
|
|
2066
|
+
if let item = view as? any _TabItemProviding { return _tag(of: item._tabContent) }
|
|
2067
|
+
if let group = view as? any _GroupView, group._children.count == 1, !(view is TupleView<Any>) {
|
|
2068
|
+
return _tag(of: group._children[0])
|
|
2069
|
+
}
|
|
2070
|
+
return nil
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
/// The `.tabItem` label applied to a view, looking through other wrappers.
|
|
2074
|
+
@MainActor
|
|
2075
|
+
func _tabLabel(of view: any View) -> (any View)? {
|
|
2076
|
+
if let item = view as? any _TabItemProviding { return item._tabLabel }
|
|
2077
|
+
if let tagged = view as? any _TagProviding { return _tabLabel(of: tagged._tagged) }
|
|
2078
|
+
if let group = view as? any _GroupView, group._children.count == 1 {
|
|
2079
|
+
return _tabLabel(of: group._children[0])
|
|
2080
|
+
}
|
|
2081
|
+
return nil
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
/// Removes `toolbar` and `searchfield` elements from anywhere under `element`
|
|
2085
|
+
/// (appending them to `bar`) and returns the element without them.
|
|
2086
|
+
private func _hoistBarItems(from element: _Element, into bar: inout [_Element],
|
|
2087
|
+
barFree: inout _BarFreeTokens) -> _Element {
|
|
2088
|
+
if barFree.contains(element.token) { return element }
|
|
2089
|
+
var kept: [_Element] = []
|
|
2090
|
+
var changed = false
|
|
2091
|
+
for child in element.children {
|
|
2092
|
+
if child.kind == "toolbar" || child.kind == "searchfield" {
|
|
2093
|
+
bar.append(child)
|
|
2094
|
+
changed = true
|
|
2095
|
+
} else {
|
|
2096
|
+
let hoisted = _hoistBarItems(from: child, into: &bar, barFree: &barFree)
|
|
2097
|
+
if hoisted.token != child.token { changed = true }
|
|
2098
|
+
kept.append(hoisted)
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
// Unchanged subtrees keep their element (and token), so the reconciler
|
|
2102
|
+
// can skip them; a modified copy is a new element.
|
|
2103
|
+
guard changed else {
|
|
2104
|
+
barFree.insert(element.token)
|
|
2105
|
+
return element
|
|
2106
|
+
}
|
|
2107
|
+
var copy = element
|
|
2108
|
+
copy.children = kept
|
|
2109
|
+
copy.token = _Element.nextToken()
|
|
2110
|
+
return copy
|
|
2111
|
+
}
|
|
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(¤t, &previous)
|
|
2133
|
+
current.removeAll(keepingCapacity: true)
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
/// A picker's options, looking through `Section`s (their headers are dropped).
|
|
2138
|
+
@MainActor
|
|
2139
|
+
func _flattenPickerOptions(_ options: any View) -> [(view: any View, tag: AnyHashable?)] {
|
|
2140
|
+
_flattenChildren(options, keys: true).flatMap { entry -> [(view: any View, tag: AnyHashable?)] in
|
|
2141
|
+
if let section = entry.view as? any _SectionView {
|
|
2142
|
+
return _flattenPickerOptions(section._sectionContent)
|
|
2143
|
+
}
|
|
2144
|
+
return [entry]
|
|
2145
|
+
}
|
|
2146
|
+
}
|