Skip to content

Commit 4968d13

Browse files
committed
Add ComposableView for custom platform composables
1 parent 1a214af commit 4968d13

1 file changed

Lines changed: 62 additions & 0 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//
2+
// ComposableView.swift
3+
// AndroidSwiftUICore
4+
//
5+
// The library's escape hatch for custom platform UI — the counterpart to
6+
// UIKit's `UIViewRepresentable`. Because a `@Composable` (or an Android
7+
// `View`) can never be authored in Swift, the seam is a *named registry*:
8+
// Kotlin registers `name → composable` factories once at startup, and this
9+
// view references one by name, forwarding typed props and an optional slot of
10+
// SwiftUI child content.
11+
//
12+
// Kotlin side:
13+
//
14+
// ComposableRegistry.register("RatingBar") { props, _ ->
15+
// AndroidView(factory = { RatingBar(it) }, update = {
16+
// it.rating = (props["rating"] as? JsonPrimitive)?.floatOrNull ?: 0f
17+
// })
18+
// }
19+
//
20+
// Swift side:
21+
//
22+
// ComposableView("RatingBar", props: ["rating": 3.5, "max": 5])
23+
//
24+
// A factory that wraps an Android-only `View` (maps, WebView, an ad SDK) uses
25+
// Compose's own `AndroidView` inside the registered composable — so arbitrary
26+
// custom Android views compose into a SwiftUI tree. An unregistered name
27+
// renders a visible diagnostic rather than failing silently.
28+
//
29+
30+
public struct ComposableView<Content: View>: View {
31+
32+
internal let name: String
33+
internal let props: [String: PropValue]
34+
internal let content: Content
35+
36+
public init(_ name: String, props: [String: PropValue] = [:], @ViewBuilder content: () -> Content) {
37+
self.name = name
38+
self.props = props
39+
self.content = content()
40+
}
41+
42+
public typealias Body = Never
43+
}
44+
45+
public extension ComposableView where Content == EmptyView {
46+
init(_ name: String, props: [String: PropValue] = [:]) {
47+
self.init(name, props: props) { EmptyView() }
48+
}
49+
}
50+
51+
extension ComposableView: PrimitiveView {
52+
public func _render(in context: ResolveContext) -> RenderNode {
53+
var props = self.props
54+
props["name"] = .string(name) // reserved: identifies the registered factory
55+
return RenderNode(
56+
type: "Composable",
57+
id: context.path,
58+
props: props,
59+
children: Evaluator.resolveChildren(content, context.descending("content"))
60+
)
61+
}
62+
}

0 commit comments

Comments
 (0)