Merge pull request #913 from morluto/fix/uikit-runtime-connections
fix(artifacts): project UIKit runtime outlet and event connections
This commit is contained in:
@@ -215,15 +215,17 @@ const projectNibArchive = (archive: NibArchiveDocument) => {
|
||||
const object = byId.get(objectId);
|
||||
if (object === undefined) return null;
|
||||
const storedClass = object.class_name.replace(/\0+$/u, "");
|
||||
return storedClass === "NSClassSwapper"
|
||||
? (referencedString(object.values.NSClassName) ?? storedClass)
|
||||
if (storedClass === "NSClassSwapper")
|
||||
return referencedString(object.values.NSClassName) ?? storedClass;
|
||||
return storedClass === "UIClassSwapper"
|
||||
? (referencedString(object.values.UIClassName) ?? storedClass)
|
||||
: storedClass;
|
||||
};
|
||||
const objects = Object.fromEntries(
|
||||
archive.objects
|
||||
.filter((object) => {
|
||||
const name = runtimeClass(object.id) ?? "";
|
||||
return !/^(?:NSObject|NSIBObjectData|NSString|NSMutableString|NSNumber|NSArray|NSMutableArray|NSSet|NSMutableSet|NSDictionary|NSMutableDictionary|NSApplication|NSNib.*Connector)$/u.test(
|
||||
return !/^(?:NSObject|NSIBObjectData|NSString|NSMutableString|NSNumber|NSArray|NSMutableArray|NSSet|NSMutableSet|NSDictionary|NSMutableDictionary|NSApplication|NSNib.*Connector|UIRuntime\w*Connection)$/u.test(
|
||||
name,
|
||||
);
|
||||
})
|
||||
@@ -269,26 +271,34 @@ const projectNibArchive = (archive: NibArchiveDocument) => {
|
||||
};
|
||||
for (const object of archive.objects) {
|
||||
const className = object.class_name.replace(/\0+$/u, "");
|
||||
const source = reference(object.values.NSSource);
|
||||
const destination = reference(object.values.NSDestination);
|
||||
if (className.includes("Connector") && source !== null) {
|
||||
const label = referencedString(object.values.NSLabel);
|
||||
const type = className.includes("Outlet")
|
||||
? "outlet"
|
||||
: className.includes("Control")
|
||||
? "action"
|
||||
: className;
|
||||
// NSSource is the outlet owner or the sending control; NSDestination is
|
||||
// the outlet value or the action target, and a nil target is the first
|
||||
// responder.
|
||||
(connections[String(source)] ??= []).push({
|
||||
type,
|
||||
"destination-id": destination === null ? null : String(destination),
|
||||
label,
|
||||
source_id: String(source),
|
||||
archive_object_id: String(object.id),
|
||||
});
|
||||
}
|
||||
// AppKit connectors use NS-prefixed keys; UIKit's runtime outlet and
|
||||
// event connections use UI-prefixed keys with the same roles.
|
||||
const uiKit = /^UIRuntime\w*Connection$/u.test(className);
|
||||
if (!uiKit && !className.includes("Connector")) continue;
|
||||
const values = object.values;
|
||||
const source = reference(uiKit ? values.UISource : values.NSSource);
|
||||
if (source === null) continue;
|
||||
const destination = reference(
|
||||
uiKit ? values.UIDestination : values.NSDestination,
|
||||
);
|
||||
const label = referencedString(uiKit ? values.UILabel : values.NSLabel);
|
||||
const type = className.includes("Outlet")
|
||||
? "outlet"
|
||||
: className.includes("Control") || className.includes("Event")
|
||||
? "action"
|
||||
: className;
|
||||
const eventMask = values.UIEventMask;
|
||||
// The source is the outlet owner or the sending control; the destination
|
||||
// is the outlet value or the action target, and a nil target is the
|
||||
// first responder.
|
||||
(connections[String(source)] ??= []).push({
|
||||
type,
|
||||
"destination-id": destination === null ? null : String(destination),
|
||||
label,
|
||||
source_id: String(source),
|
||||
archive_object_id: String(object.id),
|
||||
...(typeof eventMask === "number" ? { ui_event_mask: eventMask } : {}),
|
||||
});
|
||||
}
|
||||
let hierarchyOmitted = 0;
|
||||
const hierarchyFor = (
|
||||
|
||||
@@ -394,7 +394,11 @@ const classifyObject = (
|
||||
const value = className?.toLowerCase() ?? "";
|
||||
if (value.includes("constraint")) return "constraint";
|
||||
if (value.includes("layoutguide")) return "layout_guide";
|
||||
if (value.includes("placeholder") || value.includes("firstresponder"))
|
||||
if (
|
||||
value.includes("placeholder") ||
|
||||
value.includes("firstresponder") ||
|
||||
value === "uiproxyobject"
|
||||
)
|
||||
return "placeholder";
|
||||
if (
|
||||
value.includes("image") ||
|
||||
|
||||
@@ -340,3 +340,92 @@ describe("bounded Interface Builder archive decoding", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("compiled UIKit NIB connections", () => {
|
||||
it("projects UIKit runtime outlet and event connections", async () => {
|
||||
const root = await createTestTempDirectory("rea-ib-test-");
|
||||
const bundle = join(root, "Example.app");
|
||||
await mkdir(bundle, { recursive: true });
|
||||
await writeFile(
|
||||
join(bundle, "Cell.nib"),
|
||||
encodeNibArchiveFixture({
|
||||
classes: [
|
||||
"UIProxyObject",
|
||||
"UIClassSwapper",
|
||||
"UIButton",
|
||||
"UIRuntimeEventConnection",
|
||||
"UIRuntimeOutletConnection",
|
||||
"NSString",
|
||||
],
|
||||
objects: [
|
||||
{ classIndex: 0, values: { UIProxiedObjectIdentifier: { ref: 5 } } },
|
||||
{ classIndex: 1, values: { UIClassName: { ref: 6 } } },
|
||||
{ classIndex: 2, values: {} },
|
||||
{
|
||||
classIndex: 3,
|
||||
values: {
|
||||
UISource: { ref: 2 },
|
||||
UIDestination: { ref: 1 },
|
||||
UILabel: { ref: 7 },
|
||||
UIEventMask: 64,
|
||||
},
|
||||
},
|
||||
{
|
||||
classIndex: 4,
|
||||
values: {
|
||||
UISource: { ref: 1 },
|
||||
UIDestination: { ref: 2 },
|
||||
UILabel: { ref: 8 },
|
||||
},
|
||||
},
|
||||
{ classIndex: 5, values: { "NS.bytes": "IBFilesOwner" } },
|
||||
{ classIndex: 5, values: { "NS.bytes": "BuildCell" } },
|
||||
{ classIndex: 5, values: { "NS.bytes": "buildTapped:" } },
|
||||
{ classIndex: 5, values: { "NS.bytes": "buildButton" } },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const analysis = await analyzeInterfaceBuilderBundle({
|
||||
bundlePath: bundle,
|
||||
targetSha256: "e".repeat(64),
|
||||
});
|
||||
const byId = new Map(analysis.graph.nodes.map((node) => [node.id, node]));
|
||||
const named = (id: string | null) =>
|
||||
id === null ? null : (byId.get(id)?.name ?? null);
|
||||
const action = analysis.graph.nodes.find(({ kind }) => kind === "action");
|
||||
expect(action).toMatchObject({
|
||||
name: "buildTapped:",
|
||||
attributes: { ui_event_mask: 64 },
|
||||
});
|
||||
const actionEdges = analysis.graph.edges.filter(
|
||||
({ relation }) => relation === "target_action",
|
||||
);
|
||||
expect(
|
||||
actionEdges
|
||||
.filter(({ to }) => to === action?.id)
|
||||
.map(({ from }) => named(from)),
|
||||
).toEqual(["UIButton"]);
|
||||
expect(
|
||||
actionEdges
|
||||
.filter(({ from }) => from === action?.id)
|
||||
.map(({ to }) => named(to)),
|
||||
).toEqual(expect.arrayContaining(["BuildCell"]));
|
||||
const outlet = analysis.graph.nodes.find(({ kind }) => kind === "outlet");
|
||||
expect(outlet?.name).toBe("buildButton");
|
||||
expect(
|
||||
analysis.graph.edges
|
||||
.filter(
|
||||
({ relation, from }) =>
|
||||
relation === "outlet_to" && from === outlet?.id,
|
||||
)
|
||||
.map(({ to }) => named(to)),
|
||||
).toEqual(["UIButton"]);
|
||||
expect(
|
||||
analysis.graph.nodes.find(({ name }) => name === "IBFilesOwner")?.kind,
|
||||
).toBe("placeholder");
|
||||
expect(analysis.graph.nodes.map(({ name }) => name)).not.toContain(
|
||||
"UIRuntimeEventConnection",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user