diff --git a/Sources/OpenSwiftUICore/Runtime/ConditionalMetadata.swift b/Sources/OpenSwiftUICore/Runtime/ConditionalMetadata.swift index 735afbe2b..9baa6182c 100644 --- a/Sources/OpenSwiftUICore/Runtime/ConditionalMetadata.swift +++ b/Sources/OpenSwiftUICore/Runtime/ConditionalMetadata.swift @@ -76,7 +76,16 @@ package struct ConditionalTypeDescriptor

where P: ConditionalProtocolDescript if descriptor == conditionalTypeDescriptor { let falseDescriptor = Self.descriptor(type: metadata.genericType(at: 1)) let trueDescriptor = Self.descriptor(type: metadata.genericType(at: 0)) - storage = .either(type, f: falseDescriptor, t: trueDescriptor) + // FIXME: How to get _ConditionalContent.Storage type more easily + typealias Accessor = @convention(c) (UInt, Metadata, Metadata) -> Metadata + let nominal = Metadata(_ConditionalContent.Storage.self).nominalDescriptor! + let accessorRelativePointer = nominal.advanced(by: 12) + let accessor = unsafeBitCast( + accessorRelativePointer.advanced(by:Int(accessorRelativePointer.assumingMemoryBound(to: Int32.self).pointee)), + to: Accessor.self + ) + let type = accessor(0, Metadata(metadata.genericType(at: 0)), Metadata(metadata.genericType(at: 1))) + storage = .either(type.type, f: falseDescriptor, t: trueDescriptor) count = falseDescriptor.count + trueDescriptor.count } else if descriptor == optionalTypeDescriptor { let wrappedDescriptor = Self.descriptor(type: metadata.genericType(at: 0)) @@ -150,6 +159,29 @@ extension Optional { } } +// MARK: - ConditionalContent + ConditionalMetadata + +extension _ConditionalContent { + static func makeConditionalMetadata

(_ protocolDescriptor: P.Type) -> ConditionalMetadata

where P: ConditionalProtocolDescriptor { + let descriptor: ConditionalTypeDescriptor

+ if let result = P.fetchConditionalType(key: ObjectIdentifier(Self.self)) { + descriptor = result + } else { + descriptor = { + let falseDescriptor = ConditionalTypeDescriptor

.descriptor(type: FalseContent.self) + let trueDescriptor = ConditionalTypeDescriptor

.descriptor(type: TrueContent.self) + return ConditionalTypeDescriptor( + storage: .either(Storage.self, f: falseDescriptor, t: trueDescriptor), + count: falseDescriptor.count + trueDescriptor.count + ) + + }() + P.insertConditionalType(key: ObjectIdentifier(Self.self), value: descriptor) + } + return ConditionalMetadata(descriptor) + } +} + // MARK: ConditionalMetadata + ViewDescriptor extension ConditionalMetadata where P == ViewDescriptor { diff --git a/Sources/OpenSwiftUICore/View/ConditionalContent.swift b/Sources/OpenSwiftUICore/View/ConditionalContent.swift index 38397d06b..4bf810eee 100644 --- a/Sources/OpenSwiftUICore/View/ConditionalContent.swift +++ b/Sources/OpenSwiftUICore/View/ConditionalContent.swift @@ -1,64 +1,247 @@ // // ConditionalContent.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for iOS 15.5 -// Status: WIP -// ID: 1A625ACC143FD8524C590782FD8F4F8C +// Audited for iOS 18.0 +// Status: Complete +// ID: 1A625ACC143FD8524C590782FD8F4F8C (SwiftUI) -import OpenGraphShims +package import OpenGraphShims + +// MARK: - ConditionalContent /// View content that shows one of two possible children. @frozen public struct _ConditionalContent { - @usableFromInline @frozen - enum Storage { + public enum Storage { case trueContent(TrueContent) case falseContent(FalseContent) } - @usableFromInline - let storage: _ConditionalContent.Storage + public let storage: Storage } +@available(*, unavailable) +extension _ConditionalContent.Storage: Sendable {} + +@available(*, unavailable) +extension _ConditionalContent: Sendable {} + +extension _ConditionalContent { + /// Creates a conditional content. + /// + /// You don't use this initializer directly. OpenSwiftUI creates a + /// _ConditionalContent on your behalf when using conditional + /// statements in a variety of result builders. + @available(*, deprecated, message: "Do not use this.") + @_alwaysEmitIntoClient + public init(_storage: Storage) { + self.storage = _storage + } + + @_alwaysEmitIntoClient + package init(__storage: Storage) { + self.storage = __storage + } +} + +// MARK: - ConditionalContent + View + extension _ConditionalContent: View, PrimitiveView where TrueContent: View, FalseContent: View { @usableFromInline init(storage: Storage) { self.storage = storage } - public static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs { + nonisolated public static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs { if _SemanticFeature_v2.isEnabled { - makeImplicitRoot(view: view, inputs: inputs) + return makeImplicitRoot(view: view, inputs: inputs) } else { - AnyView._makeView( - view: _GraphValue(ChildView(content: view.value)), - inputs: inputs - ) + let metadata = makeConditionalMetadata(ViewDescriptor.self) + return makeDynamicView(metadata: metadata, view: view, inputs: inputs) } } - // public static func _makeViewList(view: _GraphValue<_ConditionalContent>, inputs: _ViewListInputs) -> _ViewListOutputs - // public static func _viewListCount(inputs: _ViewListCountInputs) -> Swift.Int? - - private struct ChildView: Rule, AsyncAttribute { - @Attribute var content: _ConditionalContent + nonisolated public static func _makeViewList(view: _GraphValue, inputs: _ViewListInputs) -> _ViewListOutputs { + let metadata = makeConditionalMetadata(ViewDescriptor.self) + return makeDynamicViewList(metadata: metadata, view: view, inputs: inputs) + } + + nonisolated public static func _viewListCount(inputs: _ViewListCountInputs) -> Int? { + guard let trueCount = TrueContent._viewListCount(inputs: inputs), + trueCount == FalseContent._viewListCount(inputs: inputs) else { + return nil + } + return trueCount + } +} + +// MARK: - ConditionalContent + DynamicView + +extension _ConditionalContent: DynamicView where TrueContent: View, FalseContent: View { + package static var canTransition: Bool { + true + } + + package func childInfo(metadata: Metadata) -> (any Any.Type, ID?) { + withUnsafePointer(to: self) { ptr in + metadata.childInfo(ptr: ptr, emptyType: EmptyView.self) + } + } + + package func makeChildView(metadata: Metadata, view: Attribute, inputs: _ViewInputs) -> _ViewOutputs { + withUnsafePointer(to: self) { ptr in + metadata.makeView(ptr: ptr, view: view, inputs: inputs) + } + } + + package func makeChildViewList(metadata: Metadata, view: Attribute, inputs: _ViewListInputs) -> _ViewListOutputs { + withUnsafePointer(to: self) { ptr in + metadata.makeViewList(ptr: ptr, view: view, inputs: inputs) + } + } + + package typealias ID = UniqueID + + package typealias Metadata = ConditionalMetadata +} - let ids: (UniqueID, UniqueID) +extension _ConditionalContent { + // MARK: - ConditionalContent + Info - init(content: Attribute<_ConditionalContent>) { - _content = content - ids = (UniqueID(), UniqueID()) + package struct Info { + var content: _ConditionalContent + var subgraph: Subgraph + + init(content: _ConditionalContent, subgraph: Subgraph) { + self.content = content + self.subgraph = subgraph } - var value: AnyView { + func matches(_ other: _ConditionalContent) -> Bool { switch content.storage { - case .trueContent(let view): - AnyView(view) - case .falseContent(let view): - AnyView(view) + case .trueContent: + switch other.storage { + case .trueContent: true + case .falseContent: false + } + case .falseContent: + switch other.storage { + case .trueContent: false + case .falseContent: true + } } } } + + // MARK: - ConditionalContent + Container + + package struct Container: StatefulRule, AsyncAttribute + where TrueContent == Provider.TrueContent, + FalseContent == Provider.FalseContent, + Provider: ConditionalContentProvider { + @Attribute var content: _ConditionalContent + let provider: Provider + let parentSubgraph: Subgraph + + package init(content: Attribute<_ConditionalContent>, provider: Provider) { + self._content = content + self.provider = provider + self.parentSubgraph = Subgraph.current! + } + + package typealias Value = Info + + package mutating func updateValue() { + let content = content + guard hasValue, value.matches(content) else { + if hasValue { + eraseInfo(value) + } + value = makeInfo(content) + return + } + var info = value + info.content = content + value = info + } + + func makeInfo(_ content: _ConditionalContent) -> Info { + let current = AnyAttribute.current! + let graph = parentSubgraph.graph + let newSubgraph = Subgraph(graph: graph) + parentSubgraph.addChild(newSubgraph) + return newSubgraph.apply { + let inputs = provider.makeChildInputs() + let outputs: Provider.Outputs + switch content.storage { + case let .trueContent(trueContent): + let trueChild = TrueChild(info: .init(identifier: current)) + let trueChildAttribute = Attribute(trueChild) + trueChildAttribute.value = trueContent + outputs = provider.makeTrueOutputs(child: trueChildAttribute, inputs: inputs) + case let .falseContent(falseContent): + let falseChild = FalseChild(info: .init(identifier: current)) + let falseChildAttribute = Attribute(falseChild) + falseChildAttribute.value = falseContent + outputs = provider.makeFalseOutputs(child: falseChildAttribute, inputs: inputs) + } + provider.attachOutputs(to: outputs) + return Info(content: content, subgraph: newSubgraph) + } + } + + func eraseInfo(_ info: Info) { + let subgraph = info.subgraph + subgraph.willInvalidate(isInserted: true) + subgraph.invalidate() + } + } + + // MARK: - ConditionalContent + TrueChild + + package struct TrueChild: StatefulRule, AsyncAttribute { + @Attribute var info: Info + + package typealias Value = TrueContent + + package mutating func updateValue() { + guard case let .trueContent(content) = info.content.storage else { + return + } + value = content + } + } + + // MARK: - ConditionalContent + FalseChild + + package struct FalseChild: StatefulRule, AsyncAttribute { + @Attribute var info: Info + + package typealias Value = FalseContent + + package mutating func updateValue() { + guard case let .falseContent(content) = info.content.storage else { + return + } + value = content + } + } +} + +// MARK: - ConditionalContentProvider + +package protocol ConditionalContentProvider { + associatedtype TrueContent + associatedtype FalseContent + associatedtype Inputs + associatedtype Outputs + var inputs: Inputs { get } + var outputs: Outputs { get } + func detachOutputs() + func attachOutputs(to: Outputs) + func makeChildInputs() -> Inputs + func makeTrueOutputs(child: Attribute, inputs: Inputs) -> Outputs + func makeFalseOutputs(child: Attribute, inputs: Inputs) -> Outputs } diff --git a/Sources/OpenSwiftUICore/View/ViewBuilder.swift b/Sources/OpenSwiftUICore/View/ViewBuilder.swift index 94585e750..91075f92f 100644 --- a/Sources/OpenSwiftUICore/View/ViewBuilder.swift +++ b/Sources/OpenSwiftUICore/View/ViewBuilder.swift @@ -2,16 +2,41 @@ // ViewBuilder.swift // OpenSwiftUICore // -// Audited for iOS 18.0 +// Audited for iOS 18.2 // Status: Complete +/// A custom parameter attribute that constructs views from closures. +/// +/// You typically use ``ViewBuilder`` as a parameter attribute for child +/// view-producing closure parameters, allowing those closures to provide +/// multiple child views. For example, the following `contextMenu` function +/// accepts a closure that produces one or more views via the view builder. +/// +/// func contextMenu( +/// @ViewBuilder menuItems: () -> MenuItems +/// ) -> some View +/// +/// Clients of this function can use multiple-statement closures to provide +/// several child views, as shown in the following example: +/// +/// myView.contextMenu { +/// Text("Cut") +/// Text("Copy") +/// Text("Paste") +/// if isSymbol { +/// Text("Jump to Definition") +/// } +/// } +/// @resultBuilder public struct ViewBuilder { + /// Builds an expression within the builder. @_alwaysEmitIntoClient public static func buildExpression(_ content: Content) -> Content where Content: View { content } - + + /// Rejects incompatible expressions within the builder. @available(*, unavailable, message: "this expression does not conform to 'View'") @_disfavoredOverload @_alwaysEmitIntoClient @@ -19,11 +44,16 @@ public struct ViewBuilder { fatalError() } + /// Builds an empty view from a block containing no statements. @_alwaysEmitIntoClient public static func buildBlock() -> EmptyView { EmptyView() } + /// Passes a single view written as a child view through unmodified. + /// + /// An example of a single view written as a child view is + /// `{ Text("Hello") }`. @_alwaysEmitIntoClient public static func buildBlock(_ content: Content) -> Content where Content: View { content @@ -40,16 +70,22 @@ public struct ViewBuilder { extension ViewBuilder: Sendable {} extension ViewBuilder { + /// Produces an optional view for conditional statements in multi-statement + /// closures that's only visible when the condition evaluates to true. @_alwaysEmitIntoClient public static func buildIf(_ content: Content?) -> Content? where Content: View { content } + /// Produces content for a conditional statement in a multi-statement closure + /// when the condition is true. @_alwaysEmitIntoClient public static func buildEither(first: TrueContent) -> _ConditionalContent where TrueContent: View, FalseContent: View { .init(storage: .trueContent(first)) } + /// Produces content for a conditional statement in a multi-statement closure + /// when the condition is false. @_alwaysEmitIntoClient public static func buildEither(second: FalseContent) -> _ConditionalContent where TrueContent: View, FalseContent: View { .init(storage: .falseContent(second)) @@ -57,8 +93,10 @@ extension ViewBuilder { } extension ViewBuilder { + /// Processes view content for a conditional compiler-control + /// statement that performs an availability check. @_alwaysEmitIntoClient - public static func buildLimitedAvailability(_ content: some View) -> AnyView { + public static func buildLimitedAvailability(_ content: Content) -> AnyView where Content: View { .init(content) } } diff --git a/Tests/OpenSwiftUICoreTests/Runtime/ConditionalMetadataTests.swift b/Tests/OpenSwiftUICoreTests/Runtime/ConditionalMetadataTests.swift index 556c7885c..f0316a98d 100644 --- a/Tests/OpenSwiftUICoreTests/Runtime/ConditionalMetadataTests.swift +++ b/Tests/OpenSwiftUICoreTests/Runtime/ConditionalMetadataTests.swift @@ -20,6 +20,7 @@ extension TestProtocolDescriptor: ConditionalProtocolDescriptor { @Suite(.enabled(if: swiftToolchainSupported), .serialized) struct ConditionalMetadataTests { + struct EmptyP: TestProtocol {} struct P1: TestProtocol {} struct P2: TestProtocol {} @@ -53,7 +54,7 @@ struct ConditionalMetadataTests { } @Test - func conditionalTypeDescriptorCaching() throws { + func conditionalTypeDescriptorCaching() { struct P3: TestProtocol {} let firstMetadata = Optional.makeConditionalMetadata(TestProtocolDescriptor.self) @@ -62,4 +63,45 @@ struct ConditionalMetadataTests { let secondMetadata = Optional.makeConditionalMetadata(TestProtocolDescriptor.self) #expect(firstMetadata.ids != secondMetadata.ids) } + + #if OPENSWIFTUI_SUPPORT_2024_API + @Test + func childInfo() throws { + // optional + let optionalMetadata = Optional.makeConditionalMetadata(TestProtocolDescriptor.self) + let value: P1? = P1() + withUnsafePointer(to: value) { ptr in + let (type, id) = optionalMetadata.childInfo(ptr: ptr, emptyType: EmptyP.self) + #expect(type == P1.self) + #expect(id != nil) + } + let nilP: P1? = nil + withUnsafePointer(to: nilP) { ptr in + let (type, id) = optionalMetadata.childInfo(ptr: ptr, emptyType: EmptyP.self) + #expect(type == EmptyP.self) + #expect(id != nil) + } + + // either + let eitherMetadata = ConditionalMetadata(ConditionalTypeDescriptor(EitherType.self)) + let trueValue = EitherType(__storage: .trueContent(P1())) + withUnsafePointer(to: trueValue) { ptr in + let (type, id) = eitherMetadata.childInfo(ptr: ptr, emptyType: EmptyP.self) + #expect(type == P1.self) + #expect(id != nil) + } + let falseValue = EitherType(__storage: .falseContent(P2())) + withUnsafePointer(to: falseValue) { ptr in + let (type, id) = eitherMetadata.childInfo(ptr: ptr, emptyType: EmptyP.self) + #expect(type == P2.self) + #expect(id != nil) + } + let otherValue = 0 + withUnsafePointer(to: otherValue) { ptr in + let (type, id) = eitherMetadata.childInfo(ptr: ptr, emptyType: EmptyP.self) + #expect(type == P1.self) + #expect(id != nil) + } + } + #endif }