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