diff --git a/Package.resolved b/Package.resolved index b64c90e55..8f654ef68 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "1cf308da26a892a1962d7acfd0aeae3c95ac506674d47f80a07474eb8476d770", + "originHash" : "4c9f2a8a5476609cccba7d42115419684b57b545e53f9107779b8bbe1569a5fe", "pins" : [ { "identity" : "darwinprivateframeworks", @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", "state" : { "branch" : "main", - "revision" : "5a0e8d14d6fbe728951ece9ded2c6f2a6708f5a6" + "revision" : "be1c53ecb3b0fbeedcddf26ece937ab3c774c03d" } }, { @@ -25,7 +25,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenGraph", "state" : { "branch" : "main", - "revision" : "4aeda45233cb3d8623d819e98aeca4b86a8a3a7d" + "revision" : "a161f230e09b0dbe6db74618b30c137707960fc8" } }, { diff --git a/Sources/OpenSwiftUI/CoreGlue/OpenSwiftUIGlue.swift b/Sources/OpenSwiftUI/CoreGlue/OpenSwiftUIGlue.swift new file mode 100644 index 000000000..6ab01adb2 --- /dev/null +++ b/Sources/OpenSwiftUI/CoreGlue/OpenSwiftUIGlue.swift @@ -0,0 +1,24 @@ +// +// OpenSwiftUIGlue.swift +// OpenSwiftUI +// +// Audited for iOS 18.0 +// Status: Empty + +@_spi(ForOpenSwiftUIOnly) +import OpenSwiftUICore + +#if canImport(Darwin) + +@_cdecl("OpenSwiftUIGlueClass") +func OpenSwiftUIGlueClass() -> CoreGlue.Type { + OpenSwiftUIGlue.self +} + +final class OpenSwiftUIGlue: CoreGlue { + override func makeDefaultLayoutComputer() -> MakeDefaultLayoutComputerResult { + MakeDefaultLayoutComputerResult(value: ViewGraph.current.$defaultLayoutComputer) + } +} + +#endif diff --git a/Sources/OpenSwiftUI/Integration/Hosting/Platform/HostingViewProtocol.swift b/Sources/OpenSwiftUI/Integration/Hosting/Platform/HostingViewProtocol.swift new file mode 100644 index 000000000..baad46c59 --- /dev/null +++ b/Sources/OpenSwiftUI/Integration/Hosting/Platform/HostingViewProtocol.swift @@ -0,0 +1,44 @@ +// +// HostingViewRegistry.swift +// OpenSwiftUI +// +// Audited for iOS 18.0 +// Status: Complete +// ID: 08E507B775941708E73E5FD8531D9361 (SwiftUI) + +@_spi(Private) +public import OpenSwiftUICore + +@_spi(Private) +public protocol HostingViewProtocol: AnyObject { + func preferenceValue(_ key: K.Type) -> K.Value where K: HostPreferenceKey + func convertAnchor(_ anchor: Anchor) -> Value +} + +@_spi(Private) +public class HostingViewRegistry { + public static let shared: HostingViewRegistry = HostingViewRegistry() + + public func forEach(_ body: (any HostingViewProtocol) throws -> Void) rethrows { + try elements.values.forEach { box in + guard let element = box.base as? HostingViewProtocol else { + return + } + try body(element) + } + } + + private var elements: [ObjectIdentifier: WeakBox] = [:] + + func add(_ element: V) where V: HostingViewProtocol { + elements[ObjectIdentifier(element)] = WeakBox(element) + } + + func remove(_ element: V) where V: HostingViewProtocol { + elements.removeValue(forKey: ObjectIdentifier(element)) + } +} + +@_spi(Private) +@available(*, unavailable) +extension HostingViewRegistry: Sendable {} diff --git a/Sources/OpenSwiftUI/Integration/Hosting/Platform/HostingViewTransparentBackgroundReason.swift b/Sources/OpenSwiftUI/Integration/Hosting/Platform/HostingViewTransparentBackgroundReason.swift new file mode 100644 index 000000000..08367d0b0 --- /dev/null +++ b/Sources/OpenSwiftUI/Integration/Hosting/Platform/HostingViewTransparentBackgroundReason.swift @@ -0,0 +1,39 @@ +// +// HostingViewTransparentBackgroundReason.swift +// OpenSwiftUI +// +// Audited for iOS 18.0 +// Status: Complete + +struct HostingViewTransparentBackgroundReason: OptionSet, CustomStringConvertible { + var rawValue: UInt32 + + static var catalystSidebar = HostingViewTransparentBackgroundReason(rawValue: 1 << 0) + static var catalystPresentation = HostingViewTransparentBackgroundReason(rawValue: 1 << 1) + static var legacyPresentationSPI = HostingViewTransparentBackgroundReason(rawValue: 1 << 2) + static var containerBackground = HostingViewTransparentBackgroundReason(rawValue: 1 << 3) + static var listItemBackground = HostingViewTransparentBackgroundReason(rawValue: 1 << 4) + + var description: String { + var description = "" + if contains(.catalystSidebar) { + description.append("catalystSidebar, ") + } + if contains(.catalystPresentation) { + description.append("catalystPresentation, ") + } + if contains(.legacyPresentationSPI) { + description.append("legacyPresentationSPI, ") + } + if contains(.containerBackground) { + description.append("containerBackground, ") + } + if contains(.listItemBackground) { + description.append("listItemBackground, ") + } + if !description.isEmpty { + description.removeLast(2) + } + return "[\(description)]" + } +} diff --git a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/AnyUIHostingView.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/AnyUIHostingView.swift deleted file mode 100644 index 876d1f8e7..000000000 --- a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/AnyUIHostingView.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// AnyUIHostingView.swift -// OpenSwiftUI -// -// Audited for iOS 15.5 -// Status: Complete - -#if os(iOS) - -@_spi(ForOpenSwiftUIOnly) import OpenSwiftUICore - -protocol AnyUIHostingView: AnyObject { - func displayLinkTimer(timestamp: Time, isAsyncThread: Bool) -} -#endif diff --git a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/Controller/UIHostingController.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/Controller/UIHostingController.swift new file mode 100644 index 000000000..f907d2e97 --- /dev/null +++ b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/Controller/UIHostingController.swift @@ -0,0 +1,105 @@ +#if os(iOS) +public import UIKit + +@available(macOS, unavailable) +@available(watchOS, unavailable) +@MainActor +@preconcurrency +open class UIHostingController: UIViewController where Content : View { + var host: _UIHostingView + + override open dynamic var keyCommands: [UIKeyCommand]? { + // TODO + nil + } + + public init(rootView: Content) { + // TODO + host = _UIHostingView(rootView: rootView) + super.init(nibName: nil, bundle: nil) + _commonInit() + } + + public init?(coder: NSCoder, rootView: Content) { + // TODO + host = _UIHostingView(rootView: rootView) + super.init(coder: coder) + _commonInit() + } + + public required init?(coder: NSCoder) { + preconditionFailure("init(coder:) must be implemented in a subclass and call super.init(coder:, rootView:)") + } + + func _commonInit() { + host.viewController = self + // toolbar + // toolbar.addPreferences(to: ViewGraph) + // ... + // IsAppleInternalBuild + } + + open override func loadView() { + view = host + } + + public var rootView: Content { + get { host.rootView } + set { host.rootView = newValue } + } + + public var sizingOptions: UIHostingControllerSizingOptions = [] { + didSet { + sizingOptionsDidChange(from: oldValue) + } + } + + @_spi(Private) + public func setRootView(_ newRootView: Content, transaction: Transaction) { + // TODO + } + + public func sizeThatFits(in size: CGSize) -> CGSize { + host.sizeThatFits(size) + } + + public func _render(seconds: Double) { + host.render(interval: seconds, targetTimestamp: nil) + } + + public func _forEachIdentifiedView(body: (_IdentifiedViewProxy) -> Void) { + host._forEachIdentifiedView(body: body) + } + + @available(*, deprecated, message: "Use UIHostingController/safeAreaRegions or _UIHostingView/safeAreaRegions") + public var _disableSafeArea: Swift.Bool { + get { + host.explicitSafeAreaInsets == .zero + } + set { + host.explicitSafeAreaInsets = newValue ? .zero : nil + } + } + + final public var _rendererConfiguration: _RendererConfiguration { + get { host._rendererConfiguration } + set { host._rendererConfiguration = newValue } + } + + final public var _rendererObject: AnyObject? { + host._rendererObject + } + + func sizingOptionsDidChange(from oldSizingOptions: UIHostingControllerSizingOptions) { + // TODO + } +} + +@available(macOS, unavailable) +extension UIHostingController { + public var safeAreaRegions: SafeAreaRegions { + get { host.safeAreaRegions } + set { host.safeAreaRegions = newValue } + } +} +#endif diff --git a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/Controller/UIHostingControllerSizingOptions.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/Controller/UIHostingControllerSizingOptions.swift new file mode 100644 index 000000000..126c965a1 --- /dev/null +++ b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/Controller/UIHostingControllerSizingOptions.swift @@ -0,0 +1,42 @@ +// +// UIHostingControllerSizingOptions.swift +// OpenSwiftUI +// +// Audited for iOS 18.0 +// Status: Complete + +#if os(iOS) + +/// Options for how a hosting controller tracks its content’s size. +@available(macOS, unavailable) +public struct UIHostingControllerSizingOptions: OptionSet, Sendable { + public let rawValue: Int + public init(rawValue: Int) { + self.rawValue = rawValue + } + + /// The hosting controller tracks its content's ideal size in its + /// preferred content size. + /// + /// Use this option when using a hosting controller with a container view + /// controller that requires up-to-date knowledge of the hosting + /// controller's ideal size. + /// + /// - Note: This option comes with a performance cost because it + /// asks for the ideal size of the content using the + /// ``ProposedViewSize/unspecified`` size proposal. + public static let preferredContentSize: UIHostingControllerSizingOptions = .init(rawValue: 1 << 0) + + /// The hosting controller's view automatically invalidate its intrinsic + /// content size when its ideal size changes. + /// + /// Use this option when the hosting controller's view is being laid out + /// with Auto Layout. + /// + /// - Note: This option comes with a performance cost because it + /// asks for the ideal size of the content using the + /// ``ProposedViewSize/unspecified`` size proposal. + public static let intrinsicContentSize: UIHostingControllerSizingOptions = .init(rawValue: 1 << 1) +} + +#endif diff --git a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingController.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingController.swift deleted file mode 100644 index 654fc8c50..000000000 --- a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingController.swift +++ /dev/null @@ -1,76 +0,0 @@ -#if os(iOS) -public import UIKit - -@available(macOS, unavailable) -@available(watchOS, unavailable) -@MainActor -@preconcurrency -open class UIHostingController : UIViewController where Content : View { - var host: _UIHostingView - - override open dynamic var keyCommands: [UIKeyCommand]? { - // TODO - nil - } - - public init(rootView: Content) { - // TODO - host = _UIHostingView(rootView: rootView) - super.init(nibName: nil, bundle: nil) - _commonInit() - } - - public init?(coder: NSCoder, rootView: Content) { - // TODO - host = _UIHostingView(rootView: rootView) - super.init(coder: coder) - _commonInit() - } - - public required init?(coder: NSCoder) { - preconditionFailure("init(coder:) must be implemented in a subclass and call super.init(coder:, rootView:)") - } - - func _commonInit() { - host.viewController = self - // toolbar - // toolbar.addPreferences(to: ViewGraph) - // ... - // IsAppleInternalBuild - } - - open override func loadView() { - view = host - } - - public var rootView: Content { - get { host.rootView } - _modify { yield &host.rootView } - } - - public func _forEachIdentifiedView(body: (_IdentifiedViewProxy) -> Void) { - host._forEachIdentifiedView(body: body) - } -} - -@available(macOS, unavailable) -extension UIHostingController: _UIHostingViewable where Content == AnyView { - -} - -@available(macOS, unavailable) -public func _makeUIHostingController(_ view: AnyView) -> NSObject & _UIHostingViewable { - UIHostingController(rootView: view) -} - -@available(macOS, unavailable) -public func _makeUIHostingController(_ view: AnyView, tracksContentSize: Bool) -> NSObject & _UIHostingViewable { - let hostingController = UIHostingController(rootView: view) - if tracksContentSize { - // TODO: hostingController.host - // SizeThatFitsObserver - } - return hostingController -} - -#endif diff --git a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingView.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingView.swift deleted file mode 100644 index d3e77ffea..000000000 --- a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingView.swift +++ /dev/null @@ -1,260 +0,0 @@ -// -// UIHostingView.swift -// OpenSwiftUI -// -// Audited for iOS 15.5 -// Status: WIP -// ID: FAF0B683EB49BE9BABC9009857940A1E - -#if os(iOS) -@_spi(ForOpenSwiftUIOnly) -@_spi(Private) -public import OpenSwiftUICore -public import UIKit - -@available(macOS, unavailable) -@available(watchOS, unavailable) -open class _UIHostingView: UIView where Content: View { - private var _rootView: Content - package var viewGraph: ViewGraph - package var currentTimestamp: Time = .zero - package var propertiesNeedingUpdate: ViewRendererHostProperties = [.rootView] // FIXME - package var isRendering: Bool = false - var inheritedEnvironment: EnvironmentValues? - var environmentOverride: EnvironmentValues? - weak var viewController: UIHostingController? - var displayLink: DisplayLink? - var lastRenderTime: Time = .zero - var canAdvanceTimeAutomatically = true - var allowLayoutWhenNotVisible = false - var isEnteringForeground = false - - public init(rootView: Content) { - // TODO - _rootView = rootView - viewGraph = ViewGraph(rootViewType: Content.self, requestedOutputs: []) // Fixme - // TODO - // FIXME - super.init(frame: .zero) - - initializeViewGraph() - // TODO - } - - @available(*, unavailable) - public required init?(coder _: NSCoder) { - preconditionFailure("init(coder:) has not been implemented") - } - - deinit { - updateRemovedState() - NotificationCenter.default.removeObserver(self) - clearDeisplayLink() - clearUpdateTimer() - invalidate() - Update.ensure { - viewGraph.preferenceBridge = nil - viewGraph.invalidate() - } - } - - func setRootView(_ view: Content, transaction: Transaction) { - _rootView = view - let mutation = CustomGraphMutation { [weak self] in - guard let self else { return } - updateRootView() - } - viewGraph.asyncTransaction( - transaction, - mutation: mutation, - style: .deferred, - mayDeferUpdate: true - ) - } - - var rootView: Content { - get { _rootView } - set { - _rootView = newValue - invalidateProperties(.init(rawValue: 1), mayDeferUpdate: true) - } - } - - func makeRootView() -> some View { - _UIHostingView.makeRootView(rootView/*.modifier(EditModeScopeModifier(editMode: .default))*/) - } - - @available(macOS, unavailable) - @available(watchOS, unavailable) - final public func _viewDebugData() -> [_ViewDebug.Data] { - // TODO - [] - } - - open override func layoutSubviews() { - super.layoutSubviews() - guard updatesWillBeVisible || allowLayoutWhenNotVisible else { - return - } - guard canAdvanceTimeAutomatically else { - return - } - Update.locked { - cancelAsyncRendering() - let interval: Double - if let displayLink, displayLink.willRender { - interval = .zero - } else { - interval = renderInterval(timestamp: .systemUptime) / UIAnimationDragCoefficient() - } - render(interval: interval) - allowLayoutWhenNotVisible = false - } - } - - var updatesWillBeVisible: Bool { - guard let window, - let scene = window.windowScene else { - return false - } - let environment = inheritedEnvironment ?? traitCollection.baseEnvironment - switch scene.activationState { - case .unattached, .foregroundActive, .foregroundInactive: - return true - case .background: - fallthrough - @unknown default: - if isEnteringForeground { - return true - } - return environment.scenePhase != .background - } - } - - func cancelAsyncRendering() { - Update.locked { - displayLink?.cancelAsyncRendering() - } - } - - private func renderInterval(timestamp: Time) -> Double { - if lastRenderTime == .zero || lastRenderTime > timestamp { - lastRenderTime = timestamp - 1e-6 - } - let interval = timestamp - lastRenderTime - lastRenderTime = timestamp - return interval - } - - // TODO - func clearDeisplayLink() { - } - - // TODO - func clearUpdateTimer() { - } - - func _forEachIdentifiedView(body: (_IdentifiedViewProxy) -> Void) { - let tree = preferenceValue(_IdentifiedViewsKey.self) - let adjustment = { [weak self](rect: inout CGRect) in - guard let self else { return } - rect = convert(rect, from: nil) - } - tree.forEach { proxy in - var proxy = proxy - proxy.adjustment = adjustment - body(proxy) - } - } -} - -extension _UIHostingView: ViewRendererHost { - package var renderingPhase: OpenSwiftUICore.ViewRenderingPhase { - get { - preconditionFailure("TODO") - } - set(newValue) { - preconditionFailure("TODO") - } - } - - package var externalUpdateCount: Int { - get { - preconditionFailure("TODO") - } - set(newValue) { - preconditionFailure("TODO") - } - } - - package func updateEnvironment() { - preconditionFailure("TODO") - } - - package func updateSize() { - preconditionFailure("TODO") - } - - package func updateSafeArea() { - preconditionFailure("TODO") - } - - package func updateScrollableContainerSize() { - preconditionFailure("TODO") - } - - package func renderDisplayList(_ list: DisplayList, asynchronously: Bool, time: Time, nextTime: Time, targetTimestamp: Time?, version: DisplayList.Version, maxVersion: DisplayList.Version) -> Time { - preconditionFailure("TODO") - } - - package func addImplicitPropertiesNeedingUpdate(to _: inout ViewRendererHostProperties) {} - - package func updateRootView() { - let rootView = makeRootView() - viewGraph.setRootView(rootView) - } - - package func requestUpdate(after: Double) { - // TODO - } - - package func modifyViewInputs(_ inputs: inout _ViewInputs) { - // TODO - } - - package func outputsDidChange(outputs: ViewGraph.Outputs) { - // TODO - } - - package func focusDidChange() { - // TODO - } - - package func rootTransform() -> ViewTransform { - preconditionFailure("TODO") - } - - public func graphDidChange() { - // TODO - } - - public func preferencesDidChange() { - // TODO - } - - func updateRemovedState() { - // TODO - } -} - -extension UITraitCollection { - var baseEnvironment: EnvironmentValues { - // TODO - EnvironmentValues() - } -} - -@_silgen_name("UIAnimationDragCoefficient") -private func UIAnimationDragCoefficient() -> Double - -#endif diff --git a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingViewable.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingViewable.swift index a6e607c24..8f826cd7e 100644 --- a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingViewable.swift +++ b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingViewable.swift @@ -2,22 +2,69 @@ // UIHostingViewable.swift // OpenSwiftUI // -// Audited for iOS 15.5 -// Status: WIP +// Audited for iOS 18.0 +// Status: Complete #if os(iOS) +public import Foundation +import UIKit -import Foundation - +/// Abstract the bits UV needs to know about UIHostingController so it doesn't +/// need to rely on UIHostingController existing in the SDK, which it can't in +/// the watchOS SDK @available(macOS, unavailable) public protocol _UIHostingViewable: AnyObject { -// var rootView: AnyView { get set } -// func _render(seconds: Double) + var rootView: AnyView { get set } + func _render(seconds: Double) func _forEachIdentifiedView(body: (_IdentifiedViewProxy) -> Void) -// func sizeThatFits(in size: CGSize) -> CGSize -// var _disableSafeArea: Bool { get set } -//// var _rendererConfiguration: _RendererConfiguration { get set } -// var _rendererObject: AnyObject? { get } + func sizeThatFits(in size: CGSize) -> CGSize + var _disableSafeArea: Bool { get set } + var _rendererConfiguration: _RendererConfiguration { get set } + var _rendererObject: AnyObject? { get } +} + +@available(macOS, unavailable) +extension UIHostingController: _UIHostingViewable where Content == AnyView {} + +@available(macOS, unavailable) +public func _makeUIHostingController(_ view: AnyView) -> any NSObject & _UIHostingViewable { + UIHostingController(rootView: view) +} + +final class _UISecureHostingController: UIHostingController where Content: View { + override init(rootView: Content) { + super.init(rootView: rootView) + } + + required init?(coder: NSCoder) { + preconditionFailure("init(coder:) has not been implemented") + } + + override var _canShowWhileLocked: Bool { + true + } +} + +@available(macOS, unavailable) +public func _makeUIHostingController(_ view: AnyView, tracksContentSize: Bool, secure: Bool = false) -> NSObject & _UIHostingViewable { + let hostingController: UIHostingController + if secure { + hostingController = _UISecureHostingController(rootView: view) + } else { + hostingController = UIHostingController(rootView: view) + } + if tracksContentSize { + hostingController.sizingOptions = .preferredContentSize + } + return hostingController +} + +@available(iOS, unavailable) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(visionOS, unavailable) +public func _makeWatchKitUIHostingController(_ view: AnyView) -> any NSObject & _UIHostingViewable { + fatalError("TODO") } #endif diff --git a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/AnyUIHostingView.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/AnyUIHostingView.swift new file mode 100644 index 000000000..1ff7b11d6 --- /dev/null +++ b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/AnyUIHostingView.swift @@ -0,0 +1,20 @@ +// +// AnyUIHostingView.swift +// OpenSwiftUI +// +// Audited for iOS 18.0 +// Status: Complete + +#if os(iOS) + +@_spi(ForOpenSwiftUIOnly) import OpenSwiftUICore + +protocol AnyUIHostingView: AnyObject { + var eventBridge: UIKitEventBindingBridge { get set } + func displayLinkTimer(timestamp: Time, targetTimestamp: Time, isAsyncThread: Bool) + var debugName: String? { get } +} + +// FIXME: +class UIKitEventBindingBridge {} +#endif diff --git a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIGraphicsView.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIGraphicsView.swift similarity index 60% rename from Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIGraphicsView.swift rename to Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIGraphicsView.swift index a8c54f44d..0b9c3e378 100644 --- a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIGraphicsView.swift +++ b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIGraphicsView.swift @@ -2,14 +2,22 @@ // UIGraphicsView.swift // OpenSwiftUI // -// Audited for iOS 15.5 +// Audited for iOS 18.0 // Status: Complete #if os(iOS) import OpenSwiftUI_SPI import UIKit -class _UIGraphicsView: UIView { +final class _UIGraphicsView: UIView { + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + override func _shouldAnimateProperty(withKey key: String) -> Bool { if layer.hasBeenCommitted() { super._shouldAnimateProperty(withKey: key) diff --git a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift new file mode 100644 index 000000000..5805cca55 --- /dev/null +++ b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift @@ -0,0 +1,543 @@ +// +// UIHostingView.swift +// OpenSwiftUI +// +// Audited for iOS 18.0 +// Status: WIP +// ID: FAF0B683EB49BE9BABC9009857940A1E (SwiftUI) + +#if os(iOS) +@_spi(ForOpenSwiftUIOnly) +@_spi(Private) +public import OpenSwiftUICore +public import UIKit +import OpenSwiftUI_SPI + +final class UIHostingViewDebugLayer: CALayer { + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override init(layer: Any) { + super.init(layer: layer) + } + + override init() { + super.init() + } + + override var name: String? { + get { + (delegate as? AnyUIHostingView)?.debugName ?? super.name + } + set { + super.name = newValue + } + } +} + +/// A UIView which hosts an OpenSwiftUI View hierarchy. +@available(macOS, unavailable) +open class _UIHostingView: UIView, XcodeViewDebugDataProvider where Content: View { + override dynamic open class var layerClass: AnyClass { + UIHostingViewDebugLayer.self + } + + private var _rootView: Content + + final package let viewGraph: ViewGraph + + final package let renderer = DisplayList.ViewRenderer(platform: .init(definition: UIViewPlatformViewDefinition.self)) + + // final package let eventBindingManager: EventBindingManager + + package var currentTimestamp: Time = .zero + + package var propertiesNeedingUpdate: ViewRendererHostProperties = .all + + package var renderingPhase: ViewRenderingPhase = .none + + package var externalUpdateCount: Int = .zero + + var parentPhase: _GraphInputs.Phase? = nil + + var isRotatingWindow: Bool = false + + var allowUIKitAnimations: Int32 = .zero + + var allowUIKitAnimationsForNextUpdate: Bool = false + + var disabledBackgroundColor: Bool = false + + var allowFrameChanges: Bool = true + + private var transparentBackgroundReasons: HostingViewTransparentBackgroundReason = [] { + didSet { + let oldHadReasons = oldValue != [] + let newHasReasons = transparentBackgroundReasons != [] + if oldHadReasons == newHasReasons { + updateBackgroundColor() + } + } + } + + var explicitSafeAreaInsets: EdgeInsets? = nil { + didSet { + safeAreaInsetsDidChange() + } + } + + var keyboardFrame: CGRect? = nil + + var keyboardSeed: UInt32 = .zero + + // var keyboardTrackingElement: UIHostingKeyboardTrackingElement? = nil + + package var isHiddenForReuse: Bool = false { + didSet { + updateRemovedState() + } + } + + var registeredForGeometryChanges: Bool = false + + @_spi(Private) + public var safeAreaRegions: SafeAreaRegions = .all { + didSet { + safeAreaRegionsDidChange(from: oldValue) + } + } + + var initialInheritedEnvironment: EnvironmentValues? = nil + + var inheritedEnvironment: EnvironmentValues? = nil { + didSet { + invalidateProperties(.environment) + } + } + + package var environmentOverride: EnvironmentValues? = nil { + didSet { + invalidateProperties(.environment) + } + } + + var traitCollectionOverride: UITraitCollection? = nil { + didSet { + guard traitCollectionOverride != oldValue else { + return + } + invalidateProperties(.environment) + } + } + + weak var viewController: UIHostingController? = nil { + didSet { + updateBackgroundColor() + } + } + + var currentEvent: UIEvent? = nil + + // var eventBridge: UIKitEventBindingBridge + + var displayLink: DisplayLink? = nil + + var lastRenderTime: Time = .zero + + var canAdvanceTimeAutomatically = true + + var pendingPreferencesUpdate: Bool = false + + var pendingPostDisappearPreferencesUpdate: Bool = false + + var nextTimerTime: Time? = nil + + var updateTimer: Timer? = nil + + var colorScheme: ColorScheme? = nil { + didSet { + didChangeColorScheme(from: oldValue) + } + } + + // TODO + + var focusedValues: FocusedValues = .init() { + didSet { + invalidateProperties(.focusedValues) + } + } + + // var currentAccessibilityFocusStore: AccessibilityFocusStore = .init() + + private weak var observedWindow: UIWindow? = nil + + private weak var observedScene: UIWindowScene? = nil + + private var _sceneActivationState: UIScene.ActivationState? = nil + + var sceneActivationState: UIScene.ActivationState? { + get { + let selector = Selector(("_windowHostingScene")) + guard let window, + window.responds(to: selector), + (window.perform(selector).takeRetainedValue() as? UIWindowScene) != nil else { + return nil + } + return _sceneActivationState + } + set { + _sceneActivationState = newValue + } + } + + var isEnteringForeground: Bool = false + + var isExitingForeground: Bool = false + + var isCapturingSnapshots: Bool = false + + var updatesWillBeVisible: Bool { + guard let sceneActivationState else { + return false + } + guard !isHiddenForReuse else { + return false + } + return switch sceneActivationState { + case .foregroundActive, .foregroundInactive: true + default: isEnteringForeground || isCapturingSnapshots + } + } + + package var accessibilityEnabled: Bool { + get { + viewGraph.accessibilityEnabled + } + set { + let oldValue = viewGraph.accessibilityEnabled + viewGraph.accessibilityEnabled = newValue + guard oldValue != newValue else { + return + } + invalidateProperties(.environment) + // AccessibilityFocus.changed(from: nil, to: nil, within: self) + } + } + + required public init(rootView: Content) { + _rootView = rootView + Update.begin() + viewGraph = ViewGraph( + rootViewType: ModifiedContent, HitTestBindingModifier>.self, + requestedOutputs: Self.defaultViewGraphOutputs() + ) + // TODO + super.init(frame: .zero) + // TODO + initializeViewGraph() + // TODO + HostingViewRegistry.shared.add(self) + Update.end() + // TODO + } + + @available(*, unavailable) + required dynamic public init?(coder _: NSCoder) { + preconditionFailure("init(coder:) has not been implemented") + } + + deinit { + updateRemovedState() + NotificationCenter.default.removeObserver(self) + clearDisplayLink() + clearUpdateTimer() + invalidate() + Update.ensure { + viewGraph.preferenceBridge = nil + viewGraph.invalidate() + } + HostingViewRegistry.shared.remove(self) + } + + /// The renderer configuration of the hosting view. + final public var _rendererConfiguration: _RendererConfiguration { + get { + Update.locked { renderer.configuration } + } + set { + Update.locked { renderer.configuration = newValue } + } + } + + /// An optional object representing the current renderer. + final public var _rendererObject: AnyObject? { + Update.locked { + renderer.exportedObject(rootView: self) + } + } + + override dynamic open func didMoveToWindow() { + // TODO + } + + override dynamic open func layoutSubviews() { + super.layoutSubviews() + guard window != nil else { + return + } + guard canAdvanceTimeAutomatically else { + return + } + Update.lock() + cancelAsyncRendering() + let interval = if let displayLink, displayLink.willRender { + 0.0 + } else { + renderInterval(timestamp: .systemUptime) / UIAnimationDragCoefficient() + } + render(interval: interval, targetTimestamp: nil) + Update.unlock() + } + + package func modifyViewInputs(_ inputs: inout _ViewInputs) { + // TODO + } + + override dynamic open var frame: CGRect { + get { + super.frame + } + set { + guard allowFrameChanges else { + return + } + let oldValue = super.frame + super.frame = newValue + frameDidChange(oldValue: oldValue) + } + } + + // TODO + + func setRootView(_ view: Content, transaction: Transaction) { + _rootView = view + viewGraph.asyncTransaction(transaction) { [weak self] in + guard let self else { return } + updateRootView() + } + } + + /// The root View of the view hierarchy to display. + var rootView: Content { + get { _rootView } + set { + _rootView = newValue + invalidateProperties(.rootView) + } + } + + var invalidatesIntrinsicContentSizeOnIdealSizeChange: Bool = false { + didSet { + // TODO + } + } + + private lazy var foreignSubviews: NSHashTable? = NSHashTable.weakObjects() + + private var isInsertingRenderedSubview: Bool = false + + /// The UIKit notion of the safe area insets. + open override var safeAreaInsets: UIEdgeInsets { + guard let explicitSafeAreaInsets else { + return super.safeAreaInsets + } + let layoutDirection = Update.ensure { viewGraph.environment.layoutDirection } + return if layoutDirection == .rightToLeft { + UIEdgeInsets(top: explicitSafeAreaInsets.top, left: explicitSafeAreaInsets.trailing, bottom: explicitSafeAreaInsets.bottom, right: explicitSafeAreaInsets.leading) + } else { + UIEdgeInsets(top: explicitSafeAreaInsets.top, left: explicitSafeAreaInsets.leading, bottom: explicitSafeAreaInsets.bottom, right: explicitSafeAreaInsets.trailing) + } + } + + // FIXME + final public func _viewDebugData() -> [_ViewDebug.Data] { + // TODO + [] + } + + // FIXME + func cancelAsyncRendering() { + Update.locked { + displayLink?.cancelAsyncRendering() + } + } + + // FIXME + private func renderInterval(timestamp: Time) -> Double { + if lastRenderTime == .zero || lastRenderTime > timestamp { + lastRenderTime = timestamp - 1e-6 + } + let interval = timestamp - lastRenderTime + lastRenderTime = timestamp + return interval + } + + // TODO + func clearDisplayLink() { + } + + // TODO + func clearUpdateTimer() { + } + + // FIXME + func _forEachIdentifiedView(body: (_IdentifiedViewProxy) -> Void) { + let tree = preferenceValue(_IdentifiedViewsKey.self) + let adjustment = { [weak self](rect: inout CGRect) in + guard let self else { return } + rect = convert(rect, from: nil) + } + tree.forEach { proxy in + var proxy = proxy + proxy.adjustment = adjustment + body(proxy) + } + } + + @_spi(Private) + @available(iOS, deprecated, message: "Use UIHostingController/safeAreaRegions or _UIHostingView/safeAreaRegions") + final public var addsKeyboardToSafeAreaInsets: Bool { + get { safeAreaRegions.contains(.keyboard) } + set { safeAreaRegions.setValue(newValue, for: .keyboard) } + } + + package func makeViewDebugData() -> Data? { + Update.ensure { + _ViewDebug.serializedData(viewGraph.viewDebugData()) + } + } + + static func defaultViewGraphOutputs() -> ViewGraph.Outputs { .defaults } +} + +extension _UIHostingView { + func makeRootView() -> ModifiedContent, HitTestBindingModifier> { + _UIHostingView.makeRootView( + rootView.modifier(EditModeScopeModifier(isActive: viewController != nil)) + ) + } + + var wantsTransparentBackground: Bool { + transparentBackgroundReasons != [] + } + + func setWantsTransparentBackground(for reason: HostingViewTransparentBackgroundReason, _ isEnabled: Bool) { + transparentBackgroundReasons.setValue(isEnabled, for: reason) + } + + func updateRemovedState() { + var removedState: GraphHost.RemovedState = [] + if window == nil { + removedState.insert(.unattached) + } + if isHiddenForReuse { + removedState.insert(.hiddenForReuse) + clearDisplayLink() + } + Update.ensure { + viewGraph.removedState = removedState + } + } + + func safeAreaRegionsDidChange(from oldSafeAreaRegions: SafeAreaRegions) { + guard safeAreaRegions != oldSafeAreaRegions else { + return + } + invalidateProperties([.safeArea, .scrollableContainerSize]) + } + + func updateBackgroundColor() { + guard let viewController else { + return + } + // TODO + } + + func didChangeColorScheme(from oldColorScheme: ColorScheme?) { + // TODO + } + + private func frameDidChange(oldValue: CGRect) { + // TODO + } +} + +extension _UIHostingView: ViewRendererHost { + package func updateEnvironment() { + // preconditionFailure("TODO") + } + + package func updateSize() { + // preconditionFailure("TODO") + } + + package func updateSafeArea() { + // preconditionFailure("TODO") + } + + package func updateScrollableContainerSize() { + // preconditionFailure("TODO") + } + + package func renderDisplayList(_ list: DisplayList, asynchronously: Bool, time: Time, nextTime: Time, targetTimestamp: Time?, version: DisplayList.Version, maxVersion: DisplayList.Version) -> Time { + // preconditionFailure("TODO") + return .infinity + } + + package func updateRootView() { + let rootView = makeRootView() + viewGraph.setRootView(rootView) + } + + package func requestUpdate(after: Double) { + // TODO + } + + package func outputsDidChange(outputs: ViewGraph.Outputs) { + // TODO + } + + package func focusDidChange() { + // TODO + } + + package func rootTransform() -> ViewTransform { + preconditionFailure("TODO") + } + + public func graphDidChange() { + // TODO + } + + public func preferencesDidChange() { + // TODO + } +} + +extension UITraitCollection { + var baseEnvironment: EnvironmentValues { + // TODO + EnvironmentValues() + } +} + +@_spi(Private) +extension _UIHostingView: HostingViewProtocol { + public func convertAnchor(_ anchor: Anchor) -> Value { + anchor.convert(to: viewGraph.transform) + } +} + +#endif diff --git a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIInheritedView.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIInheritedView.swift new file mode 100644 index 000000000..4ce35aef6 --- /dev/null +++ b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIInheritedView.swift @@ -0,0 +1,35 @@ +// +// UIInheritedView.swift +// OpenSwiftUI +// +// Audited for iOS 18.0 +// Status: Complete + +#if os(iOS) +import OpenSwiftUI_SPI +import UIKit + +final class _UIInheritedView: UIView { + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard !UIViewIgnoresTouchEvents(self) else { + return nil + } + for subview in subviews.reversed() { + let convertedPoint = convert(point, to: subview) + let result = subview.hitTest(convertedPoint, with: event) + if let result { + return result + } + } + return nil + } +} +#endif diff --git a/Sources/OpenSwiftUI/Integration/Render/UIKit/UIViewPlatformViewDefinition.swift b/Sources/OpenSwiftUI/Integration/Render/UIKit/UIViewPlatformViewDefinition.swift new file mode 100644 index 000000000..77ff1e2ec --- /dev/null +++ b/Sources/OpenSwiftUI/Integration/Render/UIKit/UIViewPlatformViewDefinition.swift @@ -0,0 +1,24 @@ +// +// UIViewPlatformViewDefinition.swift +// OpenSwiftUI +// +// Audited for iOS 18.0 +// Status: WIP + +#if os(iOS) +@_spi(DisplayList_ViewSystem) import OpenSwiftUICore +import UIKit + +// TODO +final class UIViewPlatformViewDefinition: PlatformViewDefinition, @unchecked Sendable { + override final class var system: PlatformViewDefinition.System { .uiView } + + override class func makeView(kind: PlatformViewDefinition.ViewKind) -> AnyObject { + preconditionFailure("TODO") + } + + override class func makeLayerView(type: CALayer.Type, kind: PlatformViewDefinition.ViewKind) -> AnyObject { + preconditionFailure("TODO") + } +} +#endif diff --git a/Sources/OpenSwiftUI/Render/UIViewPlatformViewDefinition.swift b/Sources/OpenSwiftUI/Render/UIViewPlatformViewDefinition.swift deleted file mode 100644 index 504008b3c..000000000 --- a/Sources/OpenSwiftUI/Render/UIViewPlatformViewDefinition.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// UIViewPlatformViewDefinition.swift -// OpenSwiftUI -// -// Audited for iOS 18.0 -// Status: WIP - -@_spi(DisplayList_ViewSystem) import OpenSwiftUICore - -final class UIViewPlatformViewDefinition: PlatformViewDefinition, @unchecked Sendable { - override final class var system: PlatformViewDefinition.System { .uiView } - // TODO -} diff --git a/Sources/OpenSwiftUI/View/List/EditMode.swift b/Sources/OpenSwiftUI/View/List/EditMode.swift new file mode 100644 index 000000000..f3097c124 --- /dev/null +++ b/Sources/OpenSwiftUI/View/List/EditMode.swift @@ -0,0 +1,130 @@ +// +// EditMode.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete +// ID: D7D98064D8079914AC08939D4AA110C8 (SwiftUI) + +public import OpenSwiftUICore +import OpenGraphShims + +// MARK: - EditMode + +/// A mode that indicates whether the user can edit a view's content. +/// +/// You receive an optional binding to the edit mode state when you +/// read the ``EnvironmentValues/editMode`` environment value. The binding +/// contains an `EditMode` value that indicates whether edit mode is active, +/// and that you can use to change the mode. To learn how to read an environment +/// value, see ``EnvironmentValues``. +/// +/// Certain built-in views automatically alter their appearance and behavior +/// in edit mode. For example, a ``List`` with a ``ForEach`` that's +/// configured with the ``DynamicViewContent/onDelete(perform:)`` or +/// ``DynamicViewContent/onMove(perform:)`` modifier provides controls to +/// delete or move list items while in edit mode. On devices without an attached +/// keyboard and mouse or trackpad, people can make multiple selections in lists +/// only when edit mode is active. +/// +/// You can also customize your own views to react to edit mode. +/// The following example replaces a read-only ``Text`` view with +/// an editable ``TextField``, checking for edit mode by +/// testing the wrapped value's ``EditMode/isEditing`` property: +/// +/// @Environment(\.editMode) private var editMode +/// @State private var name = "Maria Ruiz" +/// +/// var body: some View { +/// Form { +/// if editMode?.wrappedValue.isEditing == true { +/// TextField("Name", text: $name) +/// } else { +/// Text(name) +/// } +/// } +/// .animation(nil, value: editMode?.wrappedValue) +/// .toolbar { // Assumes embedding this view in a NavigationView. +/// EditButton() +/// } +/// } +/// +/// You can set the edit mode through the binding, or you can +/// rely on an ``EditButton`` to do that for you, as the example above +/// demonstrates. The button activates edit mode when the user +/// taps it, and disables the mode when the user taps again. +@available(macOS, unavailable) +@available(watchOS, unavailable) +public enum EditMode: Sendable { + + /// The user can't edit the view content. + /// + /// The ``isEditing`` property is `false` in this state. + case inactive + + /// The view is in a temporary edit mode. + /// + /// The use of this state varies by platform and for different + /// controls. As an example, OpenSwiftUI might engage temporary edit mode + /// over the duration of a swipe gesture. + /// + /// The ``isEditing`` property is `true` in this state. + case transient + + /// The user can edit the view content. + /// + /// The ``isEditing`` property is `true` in this state. + case active + + /// Indicates whether a view is being edited. + /// + /// This property returns `true` if the mode is something other than + /// inactive. + public var isEditing: Bool { self != .inactive } +} + +// MARK: - EditModeKey + +@available(macOS, unavailable) +@available(watchOS, unavailable) +private struct EditModeKey: EnvironmentKey { + static var defaultValue: Binding? { .constant(.inactive) } +} + +extension EnvironmentValues { + @available(macOS, unavailable) + @available(watchOS, unavailable) + public var editMode: Binding? { + get { self[EditModeKey.self] } + set { self[EditModeKey.self] = newValue } + } +} + +// MARK: - EditModeScopeModifier + +@available(macOS, unavailable) +@available(watchOS, unavailable) +struct EditModeScopeModifier: ViewModifier { + var isActive: Bool + + @State private var editMode: EditMode = .inactive + + func body(content: Content) -> some View { + content.modifier( + TransformModifier(isActive: isActive, editMode: $editMode) + ) + } + + private struct TransformModifier: EnvironmentModifier, PrimitiveViewModifier { + var isActive: Bool + var editMode: Binding + + static func makeEnvironment(modifier: Attribute, environment: inout EnvironmentValues) { + let value = modifier.value + guard value.isActive else { + return + } + environment.editMode = value.editMode + } + } +} diff --git a/Sources/OpenSwiftUICore/View/CoreGlue/CoreGlue.swift b/Sources/OpenSwiftUICore/CoreGlue/CoreGlue.swift similarity index 82% rename from Sources/OpenSwiftUICore/View/CoreGlue/CoreGlue.swift rename to Sources/OpenSwiftUICore/CoreGlue/CoreGlue.swift index a6300de63..e30928570 100644 --- a/Sources/OpenSwiftUICore/View/CoreGlue/CoreGlue.swift +++ b/Sources/OpenSwiftUICore/CoreGlue/CoreGlue.swift @@ -9,15 +9,15 @@ public import Foundation package import OpenGraphShims +import OpenSwiftUI_SPI @_spi(ForOpenSwiftUIOnly) @objc(OpenSwiftUICoreGlue) open class CoreGlue: NSObject { - package static var shared: CoreGlue = CoreGlue() // FIXME - + package static var shared: CoreGlue = _initializeCoreGlue() as! CoreGlue open func makeDefaultLayoutComputer() -> MakeDefaultLayoutComputerResult { - preconditionFailure("TODO") + preconditionFailure("") } } diff --git a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKey.swift b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKey.swift index 411931150..d8d1dd3e0 100644 --- a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKey.swift +++ b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKey.swift @@ -7,9 +7,67 @@ import OpenGraphShims +/// A key for accessing values in the environment. +/// +/// You can create custom environment values by extending the +/// ``EnvironmentValues`` structure with new properties. +/// First declare a new environment key type and specify a value for the +/// required ``defaultValue`` property: +/// +/// private struct MyEnvironmentKey: EnvironmentKey { +/// static let defaultValue: String = "Default value" +/// } +/// +/// The Swift compiler automatically infers the associated ``Value`` type as the +/// type you specify for the default value. Then use the key to define a new +/// environment value property: +/// +/// extension EnvironmentValues { +/// var myCustomValue: String { +/// get { self[MyEnvironmentKey.self] } +/// set { self[MyEnvironmentKey.self] = newValue } +/// } +/// } +/// +/// Clients of your environment value never use the key directly. +/// Instead, they use the key path of your custom environment value property. +/// To set the environment value for a view and all its subviews, add the +/// ``View/environment(_:_:)`` view modifier to that view: +/// +/// MyView() +/// .environment(\.myCustomValue, "Another string") +/// +/// As a convenience, you can also define a dedicated view modifier to +/// apply this environment value: +/// +/// extension View { +/// func myCustomValue(_ myCustomValue: String) -> some View { +/// environment(\.myCustomValue, myCustomValue) +/// } +/// } +/// +/// This improves clarity at the call site: +/// +/// MyView() +/// .myCustomValue("Another string") +/// +/// To read the value from inside `MyView` or one of its descendants, use the +/// ``Environment`` property wrapper: +/// +/// struct MyView: View { +/// @Environment(\.myCustomValue) var customValue: String +/// +/// var body: some View { +/// Text(customValue) // Displays "Another string". +/// } +/// } +/// public protocol EnvironmentKey { + /// The associated type representing the type of the environment key's + /// value. associatedtype Value + /// The default value for the environment key. static var defaultValue: Value { get } static func _valuesEqual(_ lhs: Self.Value, _ rhs: Self.Value) -> Bool diff --git a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKeyTransformModifier.swift b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKeyTransformModifier.swift index b1392dee8..46eaeb086 100644 --- a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKeyTransformModifier.swift +++ b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKeyTransformModifier.swift @@ -1,16 +1,25 @@ // // EnvironmentKeyTransformModifier.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for iOS 15.5 -// Status: Blocked by syncMainIfReferences -// ID: 1DBD4F024EFF0E73A70DB6DD05D5B548 +// Audited for iOS 18.0 +// Status: Blocked by Observation +// ID: 1DBD4F024EFF0E73A70DB6DD05D5B548 (SwiftUI) +// ID: E370275CDB55AC7AD9ACF0420859A9E8 (SwiftUICore) -import OpenGraphShims +package import OpenGraphShims +// MARK: - EnvironmentKeyTransformModifier + +/// A view modifier that transforms the existing value of an +/// environment key. @frozen -public struct _EnvironmentKeyTransformModifier: PrimitiveViewModifier, _GraphInputsModifier { +public struct _EnvironmentKeyTransformModifier: ViewModifier, _GraphInputsModifier, PrimitiveViewModifier { + /// The environment key path to transform. public var keyPath: WritableKeyPath + + /// A function to map the original value of the environment key to + /// its new value. public var transform: (inout Value) -> Void @inlinable @@ -18,44 +27,29 @@ public struct _EnvironmentKeyTransformModifier: PrimitiveViewModifier, _G self.keyPath = keyPath self.transform = transform } - + public static func _makeInputs(modifier: _GraphValue, inputs: inout _GraphInputs) { let childEnvironment = ChildEnvironment( modifier: modifier.value, environment: inputs.cachedEnvironment.wrappedValue.environment, + oldValue: nil, oldKeyPath: nil ) - let attribute = Attribute(childEnvironment) - inputs.environment = attribute + inputs.environment = Attribute(childEnvironment) } } -extension View { - /// Transforms the environment value of the specified key path with the - /// given function. - @inlinable - public func transformEnvironment( - _ keyPath: WritableKeyPath, - transform: @escaping (inout V) -> Void - ) -> some View { - modifier(_EnvironmentKeyTransformModifier( - keyPath: keyPath, - transform: transform - )) - } -} - -private struct ChildEnvironment: StatefulRule, AsyncAttribute { - @Attribute - private var modifier: _EnvironmentKeyTransformModifier - private var _environment: Attribute +private struct ChildEnvironment: StatefulRule, AsyncAttribute, CustomStringConvertible { + @Attribute private var modifier: _EnvironmentKeyTransformModifier + @Attribute private var environment: EnvironmentValues private var oldValue: Value? private var oldKeyPath: WritableKeyPath? - init(modifier: Attribute<_EnvironmentKeyTransformModifier>, - environment: Attribute, - oldValue: Value? = nil, - oldKeyPath: WritableKeyPath? + init( + modifier: Attribute<_EnvironmentKeyTransformModifier>, + environment: Attribute, + oldValue: Value?, + oldKeyPath: WritableKeyPath? ) { _modifier = modifier _environment = environment @@ -69,11 +63,13 @@ private struct ChildEnvironment: StatefulRule, AsyncAttribute { typealias Value = EnvironmentValues + // FIXME mutating func updateValue() { var (environment, environmentChanged) = _environment.changedValue() let keyPath = modifier.keyPath var newValue = environment[keyPath: keyPath] - _modifier.syncMainIfReferences { modifier in + $modifier.syncMainIfReferences { modifier in + // TODO: Observation modifier.transform(&newValue) } guard !environmentChanged, @@ -89,3 +85,48 @@ private struct ChildEnvironment: StatefulRule, AsyncAttribute { } } } + +@available(*, unavailable) +extension _EnvironmentKeyTransformModifier: Sendable {} + +extension View { + /// Transforms the environment value of the specified key path with the + /// given function. + @inlinable + nonisolated public func transformEnvironment( + _ keyPath: WritableKeyPath, + transform: @escaping (inout V) -> Void + ) -> some View { + modifier(_EnvironmentKeyTransformModifier( + keyPath: keyPath, + transform: transform + )) + } +} + +// MARK: - EnvironmentModifier + +package protocol EnvironmentModifier: _GraphInputsModifier { + static func makeEnvironment(modifier: Attribute, environment: inout EnvironmentValues) +} + +extension EnvironmentModifier { + package static func _makeInputs(modifier: _GraphValue, inputs: inout _GraphInputs) { + let updateEnviroment = UpdateEnvironment( + modifier: modifier.value, + environment: inputs.cachedEnvironment.wrappedValue.environment + ) + inputs.environment = Attribute(updateEnviroment) + } +} + +private struct UpdateEnvironment: Rule where Modifier: EnvironmentModifier { + @Attribute var modifier: Modifier + @Attribute var environment: EnvironmentValues + + var value: EnvironmentValues { + var environment = environment + Modifier.makeEnvironment(modifier: $modifier, environment: &environment) + return environment + } +} diff --git a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentModifier.swift b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentModifier.swift deleted file mode 100644 index 5d21a3abf..000000000 --- a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentModifier.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// EnvironmentModifier.swift -// OpenSwiftUICore -// -// Audited for iOS 18.0 -// Status: TODO - -package import OpenGraphShims - -package protocol EnvironmentModifier: _GraphInputsModifier { - static func makeEnvironment(modifier: Attribute, environment: inout EnvironmentValues) -} - -extension EnvironmentModifier { - package static func _makeInputs(modifier: _GraphValue, inputs: inout _GraphInputs) { - preconditionFailure("TODO") - } -} diff --git a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentValues.swift b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentValues.swift index e8c9f94be..b37bf5656 100644 --- a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentValues.swift +++ b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentValues.swift @@ -1,10 +1,11 @@ // // EnvironmentValues.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for iOS 15.5 -// Status: Complete -// ID: 83E729E7BD00420AB79EFD8DF557072A +// Audited for iOS 18.0 +// Status: TODO +// ID: 83E729E7BD00420AB79EFD8DF557072A (SwiftUI) +// ID: 0CBA6217BE011883F496E97230B6CF8F (SwiftUICore) /// A collection of environment values propagated through a view hierarchy. /// @@ -35,7 +36,7 @@ /// OpenSwiftUI provides dedicated view modifiers for setting some values, which /// typically makes your code easier to read. For example, rather than setting /// the ``EnvironmentValues/lineLimit`` value directly, as in the previous -/// example, you should instead use the ``View/lineLimit(_:)-513mb`` modifier: +/// example, you should instead use the ``View/lineLimit(_:)`` modifier: /// /// MyView() /// .lineLimit(2) @@ -53,21 +54,12 @@ /// .preferredColorScheme(.dark) /// } /// -/// Create custom environment values by defining a type that -/// conforms to the ``EnvironmentKey`` protocol, and then extending the -/// environment values structure with a new property. Use your key to get and -/// set the value, and provide a dedicated modifier for clients to use when -/// setting the value: -/// -/// private struct MyEnvironmentKey: EnvironmentKey { -/// static let defaultValue: String = "Default value" -/// } +/// Create a custom environment value by declaring a new property +/// in an extension to the environment values structure and applying +/// the ``Entry()`` macro to the variable declaration: /// /// extension EnvironmentValues { -/// var myCustomValue: String { -/// get { self[MyEnvironmentKey.self] } -/// set { self[MyEnvironmentKey.self] = newValue } -/// } +/// @Entry var myCustomValue: String = "Default value" /// } /// /// extension View { @@ -80,6 +72,10 @@ /// with the ``Environment`` property wrapper, and setting it with the /// `myCustomValue` view modifier. public struct EnvironmentValues: CustomStringConvertible { + private var _plist: PropertyList + + private let tracker: PropertyList.Tracker? + /// Creates an environment values instance. /// /// You don't typically create an instance of ``EnvironmentValues`` @@ -91,6 +87,26 @@ public struct EnvironmentValues: CustomStringConvertible { public init() { _plist = PropertyList() tracker = nil + // TODO: CoreGlue.shared + } + + package init(_ plist: PropertyList) { + _plist = plist + tracker = nil + } + + package init(_ plist: PropertyList, tracker: PropertyList.Tracker) { + // FIXME + self._plist = plist + self.tracker = tracker + } + + // FIXME + package var plist: PropertyList { + get { _plist } + set { + _plist = newValue + } } /// Accesses the environment value associated with a custom key. @@ -146,25 +162,17 @@ public struct EnvironmentValues: CustomStringConvertible { /// A string that represents the contents of the environment values /// instance. public var description: String { _plist.description } - - private var _plist: PropertyList - private let tracker: PropertyList.Tracker? - - // FIXME - var plist: PropertyList { _plist } - - init(plist: PropertyList) { - _plist = plist - tracker = nil - } } +@available(*, unavailable) +extension EnvironmentValues: Sendable {} + private struct EnvironmentPropertyKey: PropertyKey { static var defaultValue: EnvKey.Value { EnvKey.defaultValue } } private struct DerivedEnvironmentPropertyKey: DerivedPropertyKey { static func value(in plist: PropertyList) -> some Equatable { - EnvKey.value(in: EnvironmentValues(plist: plist)) + EnvKey.value(in: EnvironmentValues(plist)) } } diff --git a/Sources/OpenSwiftUICore/Data/Time.swift b/Sources/OpenSwiftUICore/Data/Time.swift index 536958088..0d786399e 100644 --- a/Sources/OpenSwiftUICore/Data/Time.swift +++ b/Sources/OpenSwiftUICore/Data/Time.swift @@ -6,7 +6,7 @@ // Status: Complete #if canImport(QuartzCore) -import QuartzCore +public import QuartzCore #endif @_spi(ForOpenSwiftUIOnly) @@ -22,6 +22,7 @@ public struct Time: Equatable, Hashable, Comparable { public static let infinity: Time = Time(seconds: .infinity) #if canImport(QuartzCore) + @inlinable public static var systemUptime: Time { Time(seconds: CACurrentMediaTime()) } diff --git a/Sources/OpenSwiftUICore/Data/Update.swift b/Sources/OpenSwiftUICore/Data/Update.swift index 880b1dfb8..f7ec9f606 100644 --- a/Sources/OpenSwiftUICore/Data/Update.swift +++ b/Sources/OpenSwiftUICore/Data/Update.swift @@ -72,7 +72,7 @@ package enum Update { Signpost.viewHost.traceEvent( type: .begin, object: trackHost, - "", // TODO: For os_log use + "", [ 0, UInt(bitPattern: Unmanaged.passUnretained(trackHost).toOpaque()), @@ -89,7 +89,7 @@ package enum Update { Signpost.viewHost.traceEvent( type: .end, object: trackHost, - "", // TODO: For os_log use + "", [ 0, UInt(bitPattern: Unmanaged.passUnretained(trackHost).toOpaque()), diff --git a/Sources/OpenSwiftUICore/Graph/GraphHost.swift b/Sources/OpenSwiftUICore/Graph/GraphHost.swift index d31c29c58..e4cf7b90f 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphHost.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphHost.swift @@ -206,8 +206,7 @@ open class GraphHost: CustomReflectable { package final func setNeedsUpdate(mayDeferUpdate: Bool) { self.mayDeferUpdate = self.mayDeferUpdate && mayDeferUpdate - // Blocked by OGGraphSetNeedsUpdate - // data.graph?.setNeedsUpdate() + data.graph?.setNeedsUpdate() } // MARK: - GraphHost.ConstantID diff --git a/Sources/OpenSwiftUICore/Graph/GraphInputs.swift b/Sources/OpenSwiftUICore/Graph/GraphInputs.swift index e92d5f8b2..25f11c9e1 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphInputs.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphInputs.swift @@ -4,6 +4,7 @@ // // Audited for iOS 18.0 // Status: WIP +// ID: 9FF97745734808976F608CE0DC13C39C (SwiftUICore) package import OpenGraphShims @@ -296,6 +297,21 @@ extension _GraphInputs { } } +private struct MergedEnvironment: Rule, AsyncAttribute { + @WeakAttribute private var lhs: EnvironmentValues? + @Attribute private var rhs: EnvironmentValues + + var value: EnvironmentValues { + let rhs = rhs + guard let lhs else { + return rhs + } + preconditionFailure("TODO") + // rhs.plist.merge(lhs.plist) + // Tracker.invalidateAllValues(from: SwiftUI.PropertyList, to: SwiftUI.PropertyList) + } +} + // FIXME: TO BE REMOVED extension _GraphInputs { @@ -303,9 +319,8 @@ extension _GraphInputs { @inline(__always) package func detechedEnvironmentInputs() -> Self { -// var newInputs = self -// newInputs.cachedEnvironment = MutableBox(cachedEnvironment.wrappedValue) -// return newInputs - preconditionFailure("TO BE REMOVED") + var newInputs = self + newInputs.cachedEnvironment = MutableBox(cachedEnvironment.wrappedValue) + return newInputs } } diff --git a/Sources/OpenSwiftUICore/Graphic/Color/CoreColor.swift b/Sources/OpenSwiftUICore/Graphic/Color/CoreColor.swift index c262c117a..577d49052 100644 --- a/Sources/OpenSwiftUICore/Graphic/Color/CoreColor.swift +++ b/Sources/OpenSwiftUICore/Graphic/Color/CoreColor.swift @@ -41,7 +41,7 @@ extension CoreColor { } package static func platformColor(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) -> NSObject? { - CorePlatformColorForRGBA(system: .defaults, red: red, green: green, blue: blue, alpha: alpha) + CorePlatformColorForRGBA(system: .defaults, red: red, green: green, blue: blue, alpha: alpha) as? NSObject } } diff --git a/Sources/OpenSwiftUICore/Layout/Geometry/Anchor.swift b/Sources/OpenSwiftUICore/Layout/Geometry/Anchor.swift index de74e03f3..74947cc16 100644 --- a/Sources/OpenSwiftUICore/Layout/Geometry/Anchor.swift +++ b/Sources/OpenSwiftUICore/Layout/Geometry/Anchor.swift @@ -7,4 +7,8 @@ package struct AnchorGeometry {} -@frozen public struct Anchor {} +@frozen public struct Anchor { + package func convert(to transform: ViewTransform) -> Value { + preconditionFailure("TODO") + } +} diff --git a/Sources/OpenSwiftUICore/Layout/SafeArea/SafeAreaInsetsModifier.swift b/Sources/OpenSwiftUICore/Layout/SafeArea/SafeAreaInsetsModifier.swift index 6cf0f8020..a638c3aee 100644 --- a/Sources/OpenSwiftUICore/Layout/SafeArea/SafeAreaInsetsModifier.swift +++ b/Sources/OpenSwiftUICore/Layout/SafeArea/SafeAreaInsetsModifier.swift @@ -22,7 +22,8 @@ package struct _SafeAreaInsetsModifier: /* MultiViewModifier, */ PrimitiveViewMo } nonisolated package static func _makeView(modifier: _GraphValue<_SafeAreaInsetsModifier>, inputs: _ViewInputs, body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs) -> _ViewOutputs { - preconditionFailure("TODO") + // preconditionFailure("TODO") + return body(_Graph(), inputs) } } diff --git a/Sources/OpenSwiftUICore/Log/Signpost.swift b/Sources/OpenSwiftUICore/Log/Signpost.swift index 5deb06f37..1fb80c4ed 100644 --- a/Sources/OpenSwiftUICore/Log/Signpost.swift +++ b/Sources/OpenSwiftUICore/Log/Signpost.swift @@ -193,7 +193,7 @@ package struct Signpost { let id = OSSignpostID.makeExclusiveID(object) let args = args() switch style { - case let .kdebug(code): + case .kdebug: _primitive(type, log: _signpostLog, signpostID: id, message, args) case let .os_log(name): os_signpost(type, log: _signpostLog, name: name, signpostID: id, message, args) diff --git a/Sources/OpenSwiftUICore/Util/EnvironmentHelper.swift b/Sources/OpenSwiftUICore/Util/EnvironmentHelper.swift index 3d2309dc8..ef4198246 100644 --- a/Sources/OpenSwiftUICore/Util/EnvironmentHelper.swift +++ b/Sources/OpenSwiftUICore/Util/EnvironmentHelper.swift @@ -28,6 +28,9 @@ package enum EnvironmentHelper { @_transparent package static func bool(for key: String) -> Bool { - int32(for: key) != 0 + guard let value = int32(for: key) else { + return false + } + return value != 0 } } diff --git a/Sources/OpenSwiftUICore/Util/Tracing.swift b/Sources/OpenSwiftUICore/Util/Tracing.swift index 2c730df71..f66b2b9bf 100644 --- a/Sources/OpenSwiftUICore/Util/Tracing.swift +++ b/Sources/OpenSwiftUICore/Util/Tracing.swift @@ -59,7 +59,7 @@ package struct Tracing { package func traceBody(_ v: any Any.Type, body: () -> Body) -> Body { Signpost.bodyInvoke.traceInterval( object: nil, - "", // TODO: For os_log use + "", [ Metadata(v).description, Tracing.libraryName(defining: v) @@ -76,7 +76,7 @@ package func traceRuleBody(_ v: any Any.Type, body: () -> Body) -> Body { Signpost.bodyInvoke.traceEvent( type: .end, object: nil, - "", // TODO: For os_log use + "", [ current.rawValue, 1, @@ -87,7 +87,7 @@ package func traceRuleBody(_ v: any Any.Type, body: () -> Body) -> Body { } return Signpost.bodyInvoke.traceInterval( object: nil, - "", // TODO: For os_log use + "", [ Metadata(v).description, Tracing.libraryName(defining: v) diff --git a/Sources/OpenSwiftUICore/View/Graph/EmptyViewRendererHost.swift b/Sources/OpenSwiftUICore/View/Graph/EmptyViewRendererHost.swift new file mode 100644 index 000000000..278270451 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Graph/EmptyViewRendererHost.swift @@ -0,0 +1,31 @@ +// +// EmptyViewRendererHost.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +final package class EmptyViewRendererHost: ViewRendererHost { + package let viewGraph: ViewGraph + package var propertiesNeedingUpdate: ViewRendererHostProperties = [] + package var renderingPhase: ViewRenderingPhase = .none + package var externalUpdateCount: Int = .zero + package var currentTimestamp: Time = .zero + package init(environment: EnvironmentValues = EnvironmentValues()) { + Update.begin() + viewGraph = ViewGraph(rootViewType: EmptyView.self, requestedOutputs: []) + viewGraph.setEnvironment(environment) + initializeViewGraph() + Update.end() + } + package func requestUpdate(after delay: Double) {} + package func updateRootView() {} + package func updateEnvironment() {} + package func updateSize() {} + package func updateSafeArea() {} + package func updateScrollableContainerSize() {} + package func renderDisplayList(_ list: DisplayList, asynchronously: Bool, time: Time, nextTime: Time, targetTimestamp: Time?, version: DisplayList.Version, maxVersion: DisplayList.Version) -> Time { + .infinity + } + package func forEachIdentifiedView(body: (_IdentifiedViewProxy) -> Void) {} +} diff --git a/Sources/OpenSwiftUICore/View/Graph/ViewGraph.swift b/Sources/OpenSwiftUICore/View/Graph/ViewGraph.swift index 658b0f52c..d993e9957 100644 --- a/Sources/OpenSwiftUICore/View/Graph/ViewGraph.swift +++ b/Sources/OpenSwiftUICore/View/Graph/ViewGraph.swift @@ -455,7 +455,39 @@ extension ViewGraph { } } -// TODO +//package struct SizeThatFitsMeasurer: ViewGraphGeometryMeasurer { +// package static func measure(given proposal: _ProposedSize, in graph: ViewGraph) -> CGSize +// package static let invalidValue: CGSize +// package typealias Proposal = _ProposedSize +// package typealias Size = CGSize +//} + +//package typealias SizeThatFitsObservers = ViewGraphGeometryObservers +extension ViewGraph { + package func sizeThatFits(_ proposal: _ProposedSize) -> CGSize { + preconditionFailure("TODO") + } + + package func explicitAlignment(of guide: VerticalAlignment, at size: CGSize) -> CGFloat? { + preconditionFailure("TODO") + } + + package func explicitAlignment(of guide: HorizontalAlignment, at size: CGSize) -> CGFloat? { + preconditionFailure("TODO") + } + + package func alignment(of guide: VerticalAlignment, at size: CGSize) -> CGFloat { + preconditionFailure("TODO") + } + + package func alignment(of guide: HorizontalAlignment, at size: CGSize) -> CGFloat { + preconditionFailure("TODO") + } + + package func viewDebugData() -> [_ViewDebug.Data] { + preconditionFailure("TODO") + } +} extension ViewGraph { package func invalidatePreferenceBridge() { diff --git a/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift b/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift index 9f51ef6a1..9ff6cd1ba 100644 --- a/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift +++ b/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift @@ -61,7 +61,6 @@ package protocol ViewRendererHost: ViewGraphDelegate { func updateAccessibilityFocus() func updateAccessibilityFocusStore() func updateAccessibilityEnvironment() - func requestUpdate(after delay: Double) func renderDisplayList(_ list: DisplayList, asynchronously: Bool, time: Time, nextTime: Time, targetTimestamp: Time?, version: DisplayList.Version, maxVersion: DisplayList.Version) -> Time func didRender() } @@ -72,96 +71,192 @@ extension ViewRendererHost { } package func initializeViewGraph() { - // viewGraph.delegate = self - // TODO: Signpost related + viewGraph.delegate = self + #if canImport(Darwin) + Signpost.viewHost.traceEvent( + type: .event, + object: self, + "", + [ + viewGraph.graph.counter(for: ._4), // FIXME: UInt + UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque()), + ] + ) + #endif } package func invalidate() { - // viewGraph.delegate = nil - // TODO: Signpost.viewHost - preconditionFailure("TODO") + viewGraph.delegate = nil + #if canImport(Darwin) + Signpost.viewHost.traceEvent( + type: .event, + object: self, + "", + [ + viewGraph.graph.counter(for: ._4), // FIXME: UInt + UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque()), + ] + ) + #endif } -// package static func makeRootView(_ view: V) -> ModifiedContent where V: View - package static func makeRootView(_ view: V) -> some View { - view/*.modifier(HitTestBindingModifier())*/ + package static func makeRootView(_ view: V) -> ModifiedContent where V: View { + view.modifier(HitTestBindingModifier()) } @_spi(ForOpenSwiftUIOnly) public func updateViewGraph(body: (ViewGraph) -> T) -> T { - // FIXME - Update.dispatchImmediately { - OGGraph.withoutUpdate { - updateGraph() - return body(viewGraph) - } + Update.begin() + defer { Update.end() } + return Graph.withoutUpdate { + updateGraph() + return body(viewGraph) } } @_spi(ForOpenSwiftUIOnly) public func graphDidChange() { - preconditionFailure("TODO") + Update.locked { + if !isRendering { + requestUpdate(after: .zero) + } + } } package func didRender() {} @_spi(ForOpenSwiftUIOnly) - public func preferencesDidChange() { - preconditionFailure("TODO") - } + public func preferencesDidChange() {} package func invalidateProperties(_ props: ViewRendererHostProperties, mayDeferUpdate: Bool = true) { - // FIXME -// Update.locked { -// guard !propertiesNeedingUpdate.contains(properties) else { -// return -// } -// propertiesNeedingUpdate.insert(properties) -// viewGraph.setNeedsUpdate(mayDeferUpdate: mayDeferUpdate) -// requestUpdate(after: .zero) -// } + Update.locked { + guard !propertiesNeedingUpdate.contains(props) else { + return + } + propertiesNeedingUpdate.insert(props) + viewGraph.setNeedsUpdate(mayDeferUpdate: mayDeferUpdate) + requestUpdate(after: .zero) + } } package func updateGraph() { - // FIXME let properties = propertiesNeedingUpdate - // addImplicitPropertiesNeedingUpdate(to: &properties) guard !properties.isEmpty else { return } Update.syncMain { - func update(_ property: ViewRendererHostProperties, body: () -> Void) { - if properties.contains(property) { - propertiesNeedingUpdate.remove(property) - } - body() + if properties.contains(.rootView) { + propertiesNeedingUpdate.remove(.rootView) + updateRootView() + } + if properties.contains(.environment) { + propertiesNeedingUpdate.remove(.rootView) + updateEnvironment() + } + if properties.contains(.focusedValues) { + propertiesNeedingUpdate.remove(.focusedValues) + updateFocusedValues() + } + if properties.contains(.transform) { + propertiesNeedingUpdate.remove(.transform) + updateTransform() + } + if properties.contains(.size) { + propertiesNeedingUpdate.remove(.size) + updateSize() + } + if properties.contains(.safeArea) { + propertiesNeedingUpdate.remove(.safeArea) + updateSafeArea() + } + if properties.contains(.scrollableContainerSize) { + propertiesNeedingUpdate.remove(.scrollableContainerSize) + updateScrollableContainerSize() + } + if properties.contains(.focusStore) { + propertiesNeedingUpdate.remove(.focusStore) + updateFocusStore() + } + if properties.contains(.accessibilityFocusStore) { + propertiesNeedingUpdate.remove(.accessibilityFocusStore) + updateAccessibilityFocusStore() + } + if properties.contains(.focusedItem) { + propertiesNeedingUpdate.remove(.focusedItem) + updateFocusedItem() + } + if properties.contains(.accessibilityFocus) { + propertiesNeedingUpdate.remove(.accessibilityFocus) + updateAccessibilityFocus() } - update(.rootView) { updateRootView() } - // TODO: } } package func updateTransform() { - preconditionFailure("TODO") + // Blocked by ValueState + // viewGraph.$rootTransform.valueState + // preconditionFailure("TODO") } package func render(interval: Double = 0, updateDisplayList: Bool = true, targetTimestamp: Time? = nil) { - // FIXME - Update.dispatchImmediately { - guard !isRendering else { - return + Update.begin() + defer { Update.end() } + guard !isRendering else { + return + } + Signpost.render.traceInterval( + object: self, + nil + ) { + let viewGraph = viewGraph + currentTimestamp += interval + let time = currentTimestamp + viewGraph.flushTransactions() + Graph.withoutUpdate { + updateGraph() } - let update = { [self] in - currentTimestamp += interval - let time = currentTimestamp - viewGraph.flushTransactions() - // Signpost.renderUpdate - // TODO - viewGraph.updateOutputs(at: time) + renderingPhase = .rendering + + var displayList: DisplayList = .init() + var version: DisplayList.Version = .init() + Signpost.renderUpdate.traceInterval( + object: self, + nil + ) { + var isFirst = true + repeat { + var shouldContinue = isFirst + Update.dispatchActions() + viewGraph.updateOutputs(at: time) + Update.dispatchActions() + viewGraph.flushTransactions() + if updateDisplayList { + (displayList, version) = viewGraph.rootDisplayList ?? (.init(), .init()) + } + isFirst = false + if !Update.canDispatch { + shouldContinue = shouldContinue && viewGraph.globalSubgraph.isDirty(1) + } + guard shouldContinue else { + break + } + } while(true) } - if Signpost.render.isEnabled { - // TODO: Signpost related - update() - } else { - update() + var nextTime = viewGraph.nextUpdate.views.time + if updateDisplayList { + let maxVersion = DisplayList.Version(forUpdate: ()) + nextTime = renderDisplayList( + displayList, + asynchronously:false, + time: time, + nextTime: nextTime, + targetTimestamp: targetTimestamp, + version: version, + maxVersion: maxVersion + ) + } + renderingPhase = .none + if nextTime.seconds.isFinite { + var delay = max(nextTime.seconds, time.seconds) - time.seconds + requestUpdate(after: max(delay, 1e-6)) } } } @@ -171,7 +266,11 @@ extension ViewRendererHost { } package func advanceTimeForTest(interval: Double) { - preconditionFailure("TODO") + guard interval >= 0 else { + preconditionFailure("Test render timestamps must monotonically increase.") + } + let advancedTime = currentTimestamp + interval + currentTimestamp = advancedTime == currentTimestamp ? Time(seconds: nextafter(advancedTime.seconds, Double.infinity)) : advancedTime } @_spi(Private) @@ -181,23 +280,33 @@ extension ViewRendererHost { } } - package func idealSize() -> CGSize { preconditionFailure("TODO") } + package func idealSize() -> CGSize { + sizeThatFits(.unspecified) + } package func sizeThatFits(_ proposal: _ProposedSize) -> CGSize { - preconditionFailure("TODO") + updateViewGraph { graph in + // FIXME: + // graph.sizeThatFits(proposal, layoutComputer: layoutComputer, insets: rootViewInsets) + CGSize.zero + } } package func explicitAlignment(of guide: HorizontalAlignment, at size: CGSize) -> CGFloat? { preconditionFailure("TODO") } + package func explicitAlignment(of guide: VerticalAlignment, at size: CGSize) -> CGFloat? { preconditionFailure("TODO") } + package func alignment(of guide: HorizontalAlignment, at size: CGSize) -> CGFloat { preconditionFailure("TODO") } + package func alignment(of guide: VerticalAlignment, at size: CGSize) -> CGFloat { preconditionFailure("TODO") } + package var centersRootView: Bool { get { viewGraph.centersRootView } set { viewGraph.centersRootView = newValue } @@ -208,8 +317,12 @@ extension ViewRendererHost { // } package var isRootHost: Bool { - preconditionFailure("TODO") + guard let bridge = viewGraph.preferenceBridge else { + return true + } + return bridge.viewGraph == nil } + private var enclosingHosts: [ViewRendererHost] { preconditionFailure("TODO") } package func performExternalUpdate(_ update: () -> Void) { preconditionFailure("TODO") } package func updateFocusedItem() {} @@ -222,7 +335,7 @@ extension ViewRendererHost { // MARK: - ViewRendererHost + Gesture [TODO] -//package let hostingViewCoordinateSpace: CoordinateSpace.ID +package let hostingViewCoordinateSpace: CoordinateSpace.ID = .init() //extension ViewRendererHost { // package var nextGestureUpdateTime: Time { @@ -290,30 +403,3 @@ extension ViewRendererHost { // viewGraph.graph.archiveJSON(name: name) } } - -// MARK: - EmptyViewRendererHost - -final package class EmptyViewRendererHost: ViewRendererHost { - package let viewGraph: ViewGraph - package var propertiesNeedingUpdate: ViewRendererHostProperties = [] - package var renderingPhase: ViewRenderingPhase = .none - package var externalUpdateCount: Int = .zero - package var currentTimestamp: Time = .zero - package init(environment: EnvironmentValues = EnvironmentValues()) { - Update.begin() - viewGraph = ViewGraph(rootViewType: EmptyView.self, requestedOutputs: []) - viewGraph.setEnvironment(environment) - initializeViewGraph() - Update.end() - } - package func requestUpdate(after delay: Double) {} - package func updateRootView() {} - package func updateEnvironment() {} - package func updateSize() {} - package func updateSafeArea() {} - package func updateScrollableContainerSize() {} - package func renderDisplayList(_ list: DisplayList, asynchronously: Bool, time: Time, nextTime: Time, targetTimestamp: Time?, version: DisplayList.Version, maxVersion: DisplayList.Version) -> Time { - .infinity - } - package func forEachIdentifiedView(body: (_IdentifiedViewProxy) -> Void) {} -} diff --git a/Sources/OpenSwiftUICore/View/Responder/ViewRespondersKey.swift b/Sources/OpenSwiftUICore/View/Responder/ViewRespondersKey.swift index 9d7e06d42..905214a09 100644 --- a/Sources/OpenSwiftUICore/View/Responder/ViewRespondersKey.swift +++ b/Sources/OpenSwiftUICore/View/Responder/ViewRespondersKey.swift @@ -20,3 +20,16 @@ package struct ViewRespondersKey: PreferenceKey { @_spi(ForOpenSwiftUIOnly) open class ViewResponder/*: ResponderNode, CustomStringConvertible, CustomRecursiveStringConvertible*/ { } + +package struct HitTestBindingModifier: ViewModifier, /*MultiViewModifier,*/ PrimitiveViewModifier { + nonisolated package static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + // preconditionFailure("TODO") + return body(_Graph(), inputs) + } + + package typealias Body = Never +} diff --git a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/OpenSwiftUICoreColor.h b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/OpenSwiftUICoreColor.h index aa036b55c..441875bef 100644 --- a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/OpenSwiftUICoreColor.h +++ b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/OpenSwiftUICoreColor.h @@ -25,7 +25,7 @@ OPENSWIFTUI_EXPORT BOOL OpenSwiftUICoreColorPlatformColorGetComponents(OpenSwiftUICoreSystem system, id color, CGFloat *red, CGFloat *green, CGFloat *blue, CGFloat *alpha) OPENSWIFTUI_SWIFT_NAME(CoreColorPlatformColorGetComponents(system:color:red:green:blue:alpha:)); OPENSWIFTUI_EXPORT -NSObject * _Nullable OpenSwiftUICorePlatformColorForRGBA(OpenSwiftUICoreSystem system, CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha) OPENSWIFTUI_SWIFT_NAME(CorePlatformColorForRGBA(system:red:green:blue:alpha:)); +id _Nullable OpenSwiftUICorePlatformColorForRGBA(OpenSwiftUICoreSystem system, CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha) OPENSWIFTUI_SWIFT_NAME(CorePlatformColorForRGBA(system:red:green:blue:alpha:)); OPENSWIFTUI_EXPORT Class _Nullable OpenSwiftUICoreColorGetKitColorClass(OpenSwiftUICoreSystem system) OPENSWIFTUI_SWIFT_NAME(CoreColorGetKitColorClass(system:)); diff --git a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/OpenSwiftUICoreColor.m b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/OpenSwiftUICoreColor.m index 6fe61e251..d2b20d3e2 100644 --- a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/OpenSwiftUICoreColor.m +++ b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/OpenSwiftUICoreColor.m @@ -44,7 +44,7 @@ BOOL OpenSwiftUICoreColorPlatformColorGetComponents(OpenSwiftUICoreSystem system #endif } -NSObject *OpenSwiftUICorePlatformColorForRGBA(OpenSwiftUICoreSystem system, CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha) { +id OpenSwiftUICorePlatformColorForRGBA(OpenSwiftUICoreSystem system, CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha) { Class colorClass = OpenSwiftUICoreColorClass(system); if (!colorClass) { return nil; diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIKitAppKit_Private.h b/Sources/OpenSwiftUI_SPI/Shims/UIKitAppKit_Private.h index 513b4fd06..a3c4b8707 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/UIKitAppKit_Private.h +++ b/Sources/OpenSwiftUI_SPI/Shims/UIKitAppKit_Private.h @@ -41,6 +41,16 @@ OPENSWIFTUI_ASSUME_NONNULL_BEGIN - (BOOL)_shouldAnimatePropertyWithKey:(NSString *)key; @end +@interface UIViewController (OpenSwiftUI_SPI) +@property (nonatomic, readonly) BOOL _canShowWhileLocked; +@end + +OPENSWIFTUI_EXPORT +bool UIViewIgnoresTouchEvents(UIView *view); + +OPENSWIFTUI_EXPORT +double UIAnimationDragCoefficient(void); + OPENSWIFTUI_ASSUME_NONNULL_END #elif __has_include() diff --git a/Sources/OpenSwiftUI_SPI/Util/CoreGlue.h b/Sources/OpenSwiftUI_SPI/Util/CoreGlue.h new file mode 100644 index 000000000..1f619fbf9 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Util/CoreGlue.h @@ -0,0 +1,19 @@ +// +// CoreGlue.h +// OpenSwiftUI_SPI +// +// Audited for iOS 15.5 +// Status: Complete + +#ifndef CoreGlue_h +#define CoreGlue_h + +#include "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +id _initializeCoreGlue(); + +#endif + +#endif /* CoreGlue_h */ diff --git a/Sources/OpenSwiftUI_SPI/Util/CoreGlue.m b/Sources/OpenSwiftUI_SPI/Util/CoreGlue.m new file mode 100644 index 000000000..d45a71d03 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Util/CoreGlue.m @@ -0,0 +1,46 @@ +// +// CoreGlue.m +// OpenSwiftUI_SPI +// +// Audited for iOS 18.0 +// Status: Complete + +#include "CoreGlue.h" +#include + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +void abort_report_np(const char*, ...); + +void* OpenSwiftUILibrary(); +void * getSwiftUIGlueClassSymbolLoc(); + +id _initializeCoreGlue() { + void *location = getSwiftUIGlueClassSymbolLoc(); + Class (*classFunc)(void) = (Class (*)(void))location; + Class glueClass = classFunc(); + return [[glueClass alloc] init]; +} + +void* OpenSwiftUILibrary() { + // Since we are staticlly linking OpenSwiftUI and OpenSwiftUICore into the final binary, + // we can just use dlopen(NULL, RTLD_LAZY) to get the current macho binary handle + return dlopen(NULL, RTLD_LAZY); +} + +void *getSwiftUIGlueClassSymbolLoc() { + static void *ptr; + if (ptr == NULL) { + @try { + ptr = dlsym(OpenSwiftUILibrary(), "OpenSwiftUIGlueClass"); + } @finally { + } + } + if (ptr == NULL) { + const char *error = dlerror(); + abort_report_np("%s", error); + } + return ptr; +} + +#endif diff --git a/Sources/OpenSwiftUI_SPI/Util/LockedPointer.h b/Sources/OpenSwiftUI_SPI/Util/LockedPointer.h index b0f2d90b3..af9f54d9e 100644 --- a/Sources/OpenSwiftUI_SPI/Util/LockedPointer.h +++ b/Sources/OpenSwiftUI_SPI/Util/LockedPointer.h @@ -1,6 +1,6 @@ // // LockedPointer.h -// OpenSwiftUI +// OpenSwiftUI_SPI // // Audited for iOS 15.5 // Status: Complete