Skip to content

Commit 83c8aff

Browse files
committed
feat(swift-interop): host SwiftUI views
A bridged struct that conforms to SwiftUI.View gains a synthesized hostingController() member. It returns a UIHostingController showing a copy of the view, as an ordinary NativeScript UIViewController that can go into a NativeScript layout. The thunk runs on the main actor and refuses other threads. The compiler verifies it like every other thunk, and the unit imports SwiftUI and UIKit when it hosts views.
1 parent 0d6c43d commit 83c8aff

17 files changed

Lines changed: 74026 additions & 658 deletions

File tree

‎TestRunner/app/tests/SwiftInteropTests.js‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,21 @@ describe("ns:swift", function () {
755755
});
756756
});
757757

758+
describe("SwiftUI views", function () {
759+
it("hosts a view in a UIHostingController", function () {
760+
var badge = new SwiftDemo.Badge("Inbox", 3);
761+
var controller = badge.hostingController();
762+
expect(controller instanceof UIViewController).toBe(true);
763+
expect(controller.view instanceof UIView).toBe(true);
764+
var size = controller.view.sizeThatFits(CGSizeMake(1000, 1000));
765+
expect(size.width).toBeGreaterThan(0);
766+
expect(size.height).toBeGreaterThan(0);
767+
// The struct is copied in: later changes do not reach the host.
768+
badge.count = 4;
769+
expect(badge.hostingController()).not.toBe(controller);
770+
});
771+
});
772+
758773
describe("Objective-C objects", function () {
759774
it("passes NativeScript's Objective-C wrappers", function () {
760775
var view = UIView.new();

‎docs/swift-interop.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,24 @@ Protocols with associated types, `Self` requirements, static or `async`
205205
requirements, initializers, or more than one refined module protocol stay
206206
type-only.
207207

208+
## SwiftUI views
209+
210+
A bridged struct that conforms to SwiftUI's `View` gains
211+
`hostingController()`, which returns a `UIHostingController` showing a copy
212+
of the view, as an ordinary NativeScript `UIViewController` (main thread
213+
only):
214+
215+
```ts
216+
const controller = new Badge("Inbox", 3).hostingController();
217+
```
218+
219+
To place it in a NativeScript layout, return `controller.view` from a
220+
custom view's `createNativeView()` and add the controller as a child of the
221+
page's view controller (`addChildViewController`, then
222+
`didMoveToParentViewController`). The view is copied in: to change what it
223+
shows, create a new controller, or drive it from an `@Observable` model the
224+
view reads.
225+
208226
## Asynchronous sequences
209227

210228
A function or property returning `AsyncStream<T>`, `AsyncThrowingStream<T, E>`

‎metadata-generator/TestFixtures/SwiftDemo/Interop/SwiftDemo+NativeScript.swift‎

Lines changed: 434 additions & 358 deletions
Large diffs are not rendered by default.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// A SwiftUI view JavaScript hosts in a UIHostingController.
2+
3+
import SwiftUI
4+
5+
public struct Badge: View {
6+
public var title: String
7+
public var count: Int
8+
9+
public init(title: String, count: Int) {
10+
self.title = title
11+
self.count = count
12+
}
13+
14+
public var body: some View {
15+
HStack {
16+
Text(title)
17+
Text("\(count)").bold()
18+
}
19+
.padding()
20+
}
21+
}

‎metadata-generator/TestFixtures/SwiftDemo/symbolgraphs/SwiftDemo.symbols.json‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎metadata-generator/src/Swift/SwiftSource.cpp‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#include <set>
77
#include <tuple>
88

9+
#include "SwiftSurface.h"
10+
911
namespace Swift {
1012

1113
std::string declaringModuleName(const Module& module, const Symbol& symbol) {
@@ -220,6 +222,16 @@ std::string swiftImports(const Module& module,
220222
collectObjCClasses(capability->result, classes);
221223
}
222224
std::set<std::string> frameworks;
225+
for (const auto& entry : module.symbols) {
226+
const Capability* capability = classification.find(entry.first);
227+
if (capability && capability->tier == Tier::SwiftGenerated &&
228+
entry.second.kind == SymbolKind::Struct &&
229+
isSwiftUIView(entry.second)) {
230+
// hostingController() (SwiftSurface).
231+
frameworks.insert("SwiftUI");
232+
frameworks.insert("UIKit");
233+
}
234+
}
223235
for (const auto& name : classes) {
224236
const char* framework = frameworkOfObjCClass(name);
225237
if (framework && llvm::StringRef(framework) != "Foundation" &&

‎metadata-generator/src/Swift/SwiftSurface.cpp‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include "SwiftSurface.h"
22

3+
#include <algorithm>
4+
35
namespace Swift {
46

57
namespace {
@@ -42,6 +44,11 @@ SwiftSurface::MemberKind memberKind(SymbolKind kind) {
4244

4345
} // namespace
4446

47+
bool isSwiftUIView(const Symbol& symbol) {
48+
return std::find(symbol.conformances.begin(), symbol.conformances.end(),
49+
"s:7SwiftUI4ViewP") != symbol.conformances.end();
50+
}
51+
4552
int SwiftSurface::indexOf(const std::string& symbolId) const {
4653
auto it = typeIndex.find(symbolId);
4754
return it == typeIndex.end() ? -1 : it->second;
@@ -233,6 +240,53 @@ SwiftSurface describeSurface(const Module& module,
233240
member.signature += ">" + signatureCode(surface, member.result);
234241
surface.members.push_back(std::move(member));
235242
}
243+
244+
for (const auto& type : surface.types) {
245+
const Symbol& view = *type.symbol;
246+
if (type.kind != SwiftSurface::kStruct || !isSwiftUIView(view)) {
247+
continue;
248+
}
249+
bool taken = false;
250+
for (const auto& member : surface.members) {
251+
taken |= member.owner == type.index &&
252+
member.capability->jsName == "hostingController";
253+
}
254+
if (taken) {
255+
continue;
256+
}
257+
Symbol symbol;
258+
symbol.preciseId = view.preciseId;
259+
symbol.kind = SymbolKind::InstanceMethod;
260+
symbol.name = "hostingController()";
261+
symbol.baseName = "hostingController";
262+
symbol.pathComponents = view.pathComponents;
263+
symbol.pathComponents.push_back(symbol.name);
264+
symbol.parentId = view.preciseId;
265+
symbol.module = view.module;
266+
symbol.docComment =
267+
"A UIHostingController showing this view (main thread only).";
268+
surface.syntheticSymbols.push_back(std::move(symbol));
269+
270+
Capability capability;
271+
capability.tier = Tier::SwiftGenerated;
272+
capability.jsName = "hostingController";
273+
capability.isMainActor = true;
274+
capability.result.bridge = Bridge::ObjCObject;
275+
capability.result.objcClass = "UIViewController";
276+
surface.syntheticCapabilities.push_back(std::move(capability));
277+
278+
SwiftSurface::Member member;
279+
member.symbol = &surface.syntheticSymbols.back();
280+
member.capability = &surface.syntheticCapabilities.back();
281+
member.owner = type.index;
282+
member.module = type.module;
283+
member.kind = SwiftSurface::kInstanceMethod;
284+
member.flags = SwiftSurface::kMainActor;
285+
member.result = member.capability->result;
286+
member.signature = ">" + signatureCode(surface, member.result);
287+
member.hostsView = true;
288+
surface.members.push_back(std::move(member));
289+
}
236290
return surface;
237291
}
238292

‎metadata-generator/src/Swift/SwiftSurface.h‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include <deque>
34
#include <map>
45
#include <string>
56
#include <vector>
@@ -67,6 +68,9 @@ struct SwiftSurface {
6768
// The value the member produces: the owner for initializers and enum
6869
// cases, the property type for properties.
6970
BridgedType result;
71+
// `hostingController()`, added to SwiftUI views: a UIHostingController
72+
// showing the view.
73+
bool hostsView = false;
7074
};
7175

7276
// A tagged enum's cases in registration order, which is the case index
@@ -83,8 +87,16 @@ struct SwiftSurface {
8387
int indexOf(const std::string& symbolId) const;
8488

8589
std::map<std::string, int> typeIndex;
90+
91+
// Declarations of members the surface adds (hostsView), which Member
92+
// points into.
93+
std::deque<Symbol> syntheticSymbols;
94+
std::deque<Capability> syntheticCapabilities;
8695
};
8796

97+
// Whether `symbol` conforms to SwiftUI.View.
98+
bool isSwiftUIView(const Symbol& symbol);
99+
88100
SwiftSurface describeSurface(const Module& module,
89101
const Classification& classification);
90102

‎metadata-generator/src/Swift/ThunkEmitter.cpp‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,9 @@ class Emitter {
430430
int flags = member.flags;
431431
const BridgedType& result = member.result;
432432

433+
if (member.hostsView) {
434+
return emitHosting(member);
435+
}
433436
if (member.kind == SwiftSurface::kEnumCase) {
434437
// Cases are values (names, or objects JS builds); nothing to call.
435438
addRegistration(member, "nil", "nil");
@@ -527,6 +530,31 @@ class Emitter {
527530
return code;
528531
}
529532

533+
// `view.hostingController()`: the boxed view in a UIHostingController,
534+
// created on the main actor.
535+
std::string emitHosting(const SwiftSurface::Member& member) {
536+
const Symbol& view = *_module.find(member.symbol->parentId);
537+
int index = _thunkCount++;
538+
std::string code =
539+
"// " + view.preciseId + " (hosting)\n" + thunkHeader(index);
540+
std::string availability = availabilityCondition(_module, view);
541+
if (!availability.empty()) {
542+
code +=
543+
" guard #available(" + availability +
544+
") else { return rt.throwMessage(" +
545+
quoted(view.name + " requires " +
546+
llvm::StringRef(availability).rsplit(',').first.trim().str()) +
547+
") }\n";
548+
}
549+
std::string receiver = bindReceiver(view, code);
550+
code += onMainActor(
551+
*member.symbol,
552+
" rt.returnObjC(UIHostingController(rootView: " + receiver + "))\n");
553+
code += "}\n\n";
554+
addRegistration(member, "__nsThunk" + std::to_string(index), "nil");
555+
return code;
556+
}
557+
530558
void addRegistration(const SwiftSurface::Member& member,
531559
const std::string& thunk, const std::string& setter) {
532560
_registrations.push_back(

‎metadata-generator/tests/swift/SymbolGraphLoaderTests.cpp‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ TEST(TopLevelDeclarationsInSourceOrder) {
7575
for (const auto& id : demo().topLevelIds) {
7676
names.push_back(demo().find(id)->name);
7777
}
78-
std::vector<std::string> expected = {"Greeter",
78+
std::vector<std::string> expected = {"Badge",
79+
"Greeter",
7980
"Demo",
8081
"Point",
8182
"Flavor",

0 commit comments

Comments
 (0)