diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..7de78ae --- /dev/null +++ b/Package.resolved @@ -0,0 +1,25 @@ +{ + "object": { + "pins": [ + { + "package": "AssociatedTypeRequirementsKit", + "repositoryURL": "https://github.com/nerdsupremacist/AssociatedTypeRequirementsKit.git", + "state": { + "branch": null, + "revision": "5c0b95758d2d65fdb98977ee1af5648d9d192a32", + "version": "0.2.0" + } + }, + { + "package": "CoreGraphicsShim", + "repositoryURL": "https://github.com/Cosmo/CoreGraphicsShim.git", + "state": { + "branch": "master", + "revision": "d2aec4b41d4f92371198b9a382f35749254f0a80", + "version": null + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift index 7ec0d9c..9285b17 100644 --- a/Package.swift +++ b/Package.swift @@ -18,13 +18,15 @@ let package = Package( // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), .package(url: "https://github.com/Cosmo/CoreGraphicsShim.git", .branch("master")), + .package(url: "https://github.com/nerdsupremacist/AssociatedTypeRequirementsKit.git", from: "0.2.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "OpenSwiftUI", - dependencies: ["CoreGraphicsShim"]), + dependencies: ["CoreGraphicsShim", "AssociatedTypeRequirementsKit"]), + .testTarget( name: "OpenSwiftUITests", dependencies: ["OpenSwiftUI"]), diff --git a/README.md b/README.md index f3fb993..a2c9faa 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ Xcode 11.2 or higher is required. | Status | Name | Notes | | --- | --- | --- | -| ⚠️ | `struct AnyView` | `init?(_fromValue value: Any)` missing. | +| ✅ | `struct AnyView` | | | ✅ | `struct TupleView` | | ### Drawing and Animation diff --git a/Sources/OpenSwiftUI/Associated Types/ViewAssociatedTypeRequirementsVisitor.swift b/Sources/OpenSwiftUI/Associated Types/ViewAssociatedTypeRequirementsVisitor.swift new file mode 100644 index 0000000..c27cc71 --- /dev/null +++ b/Sources/OpenSwiftUI/Associated Types/ViewAssociatedTypeRequirementsVisitor.swift @@ -0,0 +1,20 @@ +import AssociatedTypeRequirementsVisitor + +/** + **For internal use only.** Use to be able to call a function on a view where you don't know the concrete type at Compile Time. + Implement `callAsFunction` and a version that can take `Any` will be included as an extension. + + Useful for occassions where your intuition is to cast `Any` to `View`, but Swift will stop you due to the associated type requirement. + + ```swift + guard let view = view as? View else { return } + // Do something + ``` + */ +protocol ViewAssociatedTypeRequirementsVisitor: AssociatedTypeRequirementsVisitor { + associatedtype Visitor = ViewAssociatedTypeRequirementsVisitor + associatedtype Input = View + associatedtype Output + + func callAsFunction(_ value: T) -> Output +} diff --git a/Sources/OpenSwiftUI/Views/AnyView+initFromValue.swift b/Sources/OpenSwiftUI/Views/AnyView+initFromValue.swift new file mode 100644 index 0000000..a3de9cf --- /dev/null +++ b/Sources/OpenSwiftUI/Views/AnyView+initFromValue.swift @@ -0,0 +1,18 @@ +import Foundation + +extension AnyView { + public init?(_fromValue value: Any) { + guard let view = ViewTypeEraser.shared(value) else { return nil } + self = view + } +} + +private struct ViewTypeEraser: ViewAssociatedTypeRequirementsVisitor { + func callAsFunction(_ value: T) -> AnyView { + return AnyView(value) + } +} + +extension ViewTypeEraser { + static let shared = ViewTypeEraser() +} diff --git a/Sources/OpenSwiftUI/Views/AnyView.swift b/Sources/OpenSwiftUI/Views/AnyView.swift index f6e60ee..7421646 100644 --- a/Sources/OpenSwiftUI/Views/AnyView.swift +++ b/Sources/OpenSwiftUI/Views/AnyView.swift @@ -17,10 +17,6 @@ public struct AnyView: View { _storage = AnyViewStorage(view) } - public init?(_fromValue value: Any) { - fatalError() - } - public typealias Body = Never public var body: Never { fatalError() diff --git a/Tests/OpenSwiftUITests/OpenSwiftUITests.swift b/Tests/OpenSwiftUITests/OpenSwiftUITests.swift index 0658af0..876b954 100644 --- a/Tests/OpenSwiftUITests/OpenSwiftUITests.swift +++ b/Tests/OpenSwiftUITests/OpenSwiftUITests.swift @@ -8,15 +8,35 @@ final class OpenSwiftUITests: XCTestCase { Text("World") } - let body = HStack { - if true { - Text("Hello") - } - Text("World") + } + + func testAnyViewFromValueWithInDoesNotYieldView() { + let anyView = AnyView(_fromValue: 42) + XCTAssertNil(anyView) + } + + func testAnyViewFromValueWithTextYieldsAnyView() { + let expectedText = "Hello" + let value: Any = Text(expectedText) + let anyView = AnyView(_fromValue: value) + XCTAssertNotNil(anyView) + + guard let storage = anyView?._storage as? AnyViewStorage else { + XCTFail("View storage is not an AnyViewStorage of Text") + return + } + + switch storage._view._storage { + case .verbatim(let string): + XCTAssertEqual(string, expectedText) + case .anyTextStorage(let storage): + XCTAssertEqual(storage.storage, expectedText) } } static var allTests = [ ("testExample", testExample), + ("testAnyViewFromValueWithInDoesNotYieldView", testAnyViewFromValueWithInDoesNotYieldView), + ("testAnyViewFromValueWithTextYieldsAnyView", testAnyViewFromValueWithTextYieldsAnyView), ] }