Skip to content

Commit 65a2842

Browse files
authored
Merge pull request #1 from nova286/codex/swiftui-inspector
Add experimental SwiftUI inspector mode
2 parents 95e30fd + a05a946 commit 65a2842

13 files changed

Lines changed: 305 additions & 22 deletions
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
name: Build Lookin Desktop
2+
3+
on:
4+
push:
5+
pull_request:
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
build:
13+
name: Release build
14+
runs-on: macos-15
15+
timeout-minutes: 30
16+
17+
steps:
18+
- name: Check out repository
19+
uses: actions/checkout@v7
20+
21+
- name: Set up Ruby
22+
uses: ruby/setup-ruby@v1
23+
with:
24+
ruby-version: "3.3"
25+
26+
- name: Install CocoaPods
27+
run: gem install cocoapods -v 1.17.0 --no-document
28+
29+
- name: Resolve dependencies
30+
run: pod install
31+
32+
- name: Build universal Release app
33+
run: |
34+
set -euo pipefail
35+
xcodebuild \
36+
-workspace Lookin.xcworkspace \
37+
-scheme LookinClient \
38+
-configuration Release \
39+
-destination 'generic/platform=macOS' \
40+
-derivedDataPath "$RUNNER_TEMP/LookinDerivedData" \
41+
CODE_SIGNING_ALLOWED=NO \
42+
ARCHS='arm64 x86_64' \
43+
ONLY_ACTIVE_ARCH=NO \
44+
build
45+
46+
- name: Package app
47+
if: github.event_name != 'pull_request'
48+
run: |
49+
set -euo pipefail
50+
51+
app_path="$RUNNER_TEMP/LookinDerivedData/Build/Products/Release/Lookin.app"
52+
test -d "$app_path"
53+
54+
codesign --force --deep --sign - "$app_path"
55+
codesign --verify --deep --strict "$app_path"
56+
lipo "$app_path/Contents/MacOS/Lookin" -verify_arch arm64 x86_64
57+
58+
ref_label="${GITHUB_REF_NAME//\//-}"
59+
artifact_name="Lookin-SwiftUI-Inspector-${ref_label}-${GITHUB_SHA:0:7}"
60+
artifact_dir="$GITHUB_WORKSPACE/dist"
61+
archive_path="$artifact_dir/${artifact_name}.zip"
62+
63+
mkdir -p "$artifact_dir"
64+
ditto -c -k --sequesterRsrc --keepParent "$app_path" "$archive_path"
65+
cd "$artifact_dir"
66+
shasum -a 256 "${artifact_name}.zip" > "${artifact_name}.zip.sha256"
67+
68+
- name: Upload packaged app
69+
if: github.event_name != 'pull_request'
70+
uses: actions/upload-artifact@v7
71+
with:
72+
name: lookin-desktop-${{ github.run_id }}
73+
path: dist/*
74+
if-no-files-found: error
75+
retention-days: 14
76+
77+
release:
78+
name: Publish GitHub Release
79+
if: startsWith(github.ref, 'refs/tags/v')
80+
needs: build
81+
runs-on: ubuntu-latest
82+
permissions:
83+
contents: write
84+
85+
steps:
86+
- name: Download packaged app
87+
uses: actions/download-artifact@v8
88+
with:
89+
name: lookin-desktop-${{ github.run_id }}
90+
path: dist
91+
92+
- name: Create or update release
93+
env:
94+
GH_TOKEN: ${{ github.token }}
95+
run: |
96+
set -euo pipefail
97+
98+
archive_path="$(find dist -type f -name '*.zip' -print -quit)"
99+
test -n "$archive_path"
100+
checksum_path="${archive_path}.sha256"
101+
test -f "$checksum_path"
102+
103+
if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
104+
gh release upload "$GITHUB_REF_NAME" \
105+
"$archive_path" \
106+
"$checksum_path" \
107+
--clobber \
108+
--repo "$GITHUB_REPOSITORY"
109+
else
110+
gh release create "$GITHUB_REF_NAME" \
111+
"$archive_path" \
112+
"$checksum_path" \
113+
--generate-notes \
114+
--title "Lookin SwiftUI Inspector $GITHUB_REF_NAME" \
115+
--repo "$GITHUB_REPOSITORY"
116+
fi

LookinClient/Dashboard/LKDashboardViewController.m

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#import "LKDashboardSearchMethodsView.h"
2424
#import "LKDashboardSearchMethodsDataSource.h"
2525
#import "LookinCustomAttrModification.h"
26+
#import "LookinCustomDisplayItemInfo.h"
2627
#import "LKDashboardTextControlEditingFlag.h"
2728
@import AppCenter;
2829
@import AppCenterAnalytics;
@@ -247,11 +248,38 @@ - (RACSignal *)modifyCustomAttribute:(LookinAttribute *)attribute newValue:(id)n
247248
return nil;
248249
}
249250

251+
BOOL shouldRefreshSwiftUIHierarchy = self.staticDataSource &&
252+
[attribute.targetDisplayItem.customInfo.semanticKind hasPrefix:@"swiftui-"];
250253
@weakify(self);
251254
[[[LKAppsManager sharedInstance].inspectingApp submitCustomModification:modification] subscribeNext:^(id ret) {
252255
NSLog(@"custom modification - succ");
253256
attribute.value = newValue;
254-
[subscriber sendNext:nil];
257+
258+
if (!shouldRefreshSwiftUIHierarchy) {
259+
[subscriber sendNext:nil];
260+
return;
261+
}
262+
263+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.15 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
264+
@strongify(self);
265+
LKInspectableApp *app = [LKAppsManager sharedInstance].inspectingApp;
266+
if (!self || !app) {
267+
AlertError(LookinErr_NoConnect, self.view.window);
268+
[subscriber sendError:LookinErr_NoConnect];
269+
return;
270+
}
271+
272+
[[[app fetchHierarchyData] deliverOnMainThread] subscribeNext:^(LookinHierarchyInfo *info) {
273+
LKDashboardTextControlEditingFlag.sharedInstance.shouldIgnoreTextEditingChangeEvent = YES;
274+
[self.staticDataSource reloadWithHierarchyInfo:info keepState:YES];
275+
LKDashboardTextControlEditingFlag.sharedInstance.shouldIgnoreTextEditingChangeEvent = NO;
276+
[subscriber sendNext:nil];
277+
278+
} error:^(NSError *error) {
279+
AlertError(error, self.view.window);
280+
[subscriber sendError:error];
281+
}];
282+
});
255283

256284
} error:^(NSError * _Nullable error) {
257285
@strongify(self);

LookinClient/Hierarchy/LKDanceUIAttrMaker.m

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,13 @@ + (NSString *)getClassFromSource:(NSString *)json {
5555
NSError *error;
5656
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:&error];
5757
if (error) {
58-
NSAssert(NO, @"");
5958
return nil;
6059
}
6160
if (![dict isKindOfClass:[NSDictionary class]]) {
62-
NSAssert(NO, @"");
6361
return nil;
6462
}
6563
NSString *type = dict[@"type"];
66-
if (!type) {
67-
NSAssert(NO, @"");
64+
if (![type isKindOfClass:[NSString class]] || type.length == 0) {
6865
return nil;
6966
}
7067
return type;

LookinClient/Hierarchy/LKHierarchyDataSource.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ typedef NS_ENUM(NSUInteger, LKHierarchyDataSourceState) {
2525
如果 keepState 为 YES,则会尽量维持刷新之前的折叠状态和选中态
2626
*/
2727
- (void)reloadWithHierarchyInfo:(LookinHierarchyInfo *)info keepState:(BOOL)keepState;
28+
/// Subclass hook for presenting an alternate root hierarchy without mutating rawHierarchyInfo.
29+
- (NSArray<LookinDisplayItem *> *)hierarchyRootItemsForInfo:(LookinHierarchyInfo *)info;
2830
@property(nonatomic, strong, readonly) RACSubject *willReloadHierarchyInfo;
2931
@property(nonatomic, strong, readonly) RACSubject *didReloadHierarchyInfo;
3032

LookinClient/Hierarchy/LKHierarchyDataSource.m

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ - (void)reloadWithHierarchyInfo:(LookinHierarchyInfo *)info keepState:(BOOL)keep
144144
[self _setUpColors];
145145

146146
// 根据 subitems 属性打平为二维数组,同时给每个 item 设置 indentLevel
147-
self.rawFlatItems = [LookinDisplayItem flatItemsFromHierarchicalItems:info.displayItems];
147+
NSArray<LookinDisplayItem *> *hierarchyRootItems = [self hierarchyRootItemsForInfo:info];
148+
self.rawFlatItems = [LookinDisplayItem flatItemsFromHierarchicalItems:hierarchyRootItems];
148149
NSArray<LookinDisplayItem *> *flatItems = self.rawFlatItems.copy;
149150

150151
// 设置 preferToBeCollapsed 属性
@@ -242,6 +243,10 @@ - (void)reloadWithHierarchyInfo:(LookinHierarchyInfo *)info keepState:(BOOL)keep
242243
[self.didReloadHierarchyInfo sendNext:nil];
243244
}
244245

246+
- (NSArray<LookinDisplayItem *> *)hierarchyRootItemsForInfo:(LookinHierarchyInfo *)info {
247+
return info.displayItems;
248+
}
249+
245250
- (NSInteger)numberOfRows {
246251
return self.displayingFlatItems.count;
247252
}
@@ -358,7 +363,7 @@ - (void)adjustExpansionByIndex:(NSInteger)index referenceDict:(NSDictionary<NSNu
358363

359364
if (selectedItem) {
360365
__block LookinDisplayItem *preferedSelectedItem = nil;
361-
LookinDisplayItem *keyWindowRootItem = [self.rawHierarchyInfo.displayItems lookin_firstFiltered:^BOOL(LookinDisplayItem *obj) {
366+
LookinDisplayItem *keyWindowRootItem = [[self hierarchyRootItemsForInfo:self.rawHierarchyInfo] lookin_firstFiltered:^BOOL(LookinDisplayItem *obj) {
362367
return obj.representedAsKeyWindow;
363368
}];
364369
[[LookinDisplayItem flatItemsFromHierarchicalItems:@[keyWindowRootItem]] enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(LookinDisplayItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
@@ -371,13 +376,14 @@ - (void)adjustExpansionByIndex:(NSInteger)index referenceDict:(NSDictionary<NSNu
371376
}
372377

373378
} else {
374-
LookinDisplayItem *keyWindowItem = [self.rawHierarchyInfo.displayItems lookin_firstFiltered:^BOOL(LookinDisplayItem *windowItem) {
379+
NSArray<LookinDisplayItem *> *hierarchyRootItems = [self hierarchyRootItemsForInfo:self.rawHierarchyInfo];
380+
LookinDisplayItem *keyWindowItem = [hierarchyRootItems lookin_firstFiltered:^BOOL(LookinDisplayItem *windowItem) {
375381
return windowItem.representedAsKeyWindow;
376382
}];
377383
if (!keyWindowItem) {
378-
keyWindowItem = self.rawHierarchyInfo.displayItems.firstObject;
384+
keyWindowItem = hierarchyRootItems.firstObject;
379385
}
380-
[self.rawHierarchyInfo.displayItems enumerateObjectsUsingBlock:^(LookinDisplayItem * _Nonnull windowItem, NSUInteger idx, BOOL * _Nonnull stop) {
386+
[hierarchyRootItems enumerateObjectsUsingBlock:^(LookinDisplayItem * _Nonnull windowItem, NSUInteger idx, BOOL * _Nonnull stop) {
381387
if (windowItem == keyWindowItem) {
382388
return;
383389
}

LookinClient/Static/LKStaticHierarchyDataSource.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
+ (instancetype)sharedInstance;
1717

1818
@property(nonatomic, strong, readonly) LookinAppInfo *appInfo;
19+
@property(nonatomic, assign, readonly) BOOL swiftUISemanticHierarchyAvailable;
20+
@property(nonatomic, assign) BOOL showsSwiftUISemanticHierarchy;
1921

2022
#pragma mark - Signal
2123

LookinClient/Static/LKStaticHierarchyDataSource.m

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ @interface LKStaticHierarchyDataSource ()
2828

2929
@property(nonatomic, assign) BOOL shouldIgnoreFastModeAutoUpdate;
3030
@property(nonatomic, assign) BOOL isUsingDanceUI;
31+
@property(nonatomic, assign, readwrite) BOOL swiftUISemanticHierarchyAvailable;
32+
@property(nonatomic, strong) LookinDisplayItem *swiftUISemanticRoot;
3133

3234
@end
3335

@@ -56,7 +58,55 @@ - (instancetype)init {
5658
#pragma mark - Public
5759

5860
- (void)reloadWithHierarchyInfo:(LookinHierarchyInfo *)info keepState:(BOOL)keepState {
61+
NSString *selectedSemanticIdentifier = nil;
62+
NSMutableDictionary<NSString *, NSNumber *> *semanticExpansionState = nil;
63+
if (keepState && self.showsSwiftUISemanticHierarchy) {
64+
selectedSemanticIdentifier = self.selectedItem.customInfo.semanticIdentifier;
65+
semanticExpansionState = [NSMutableDictionary dictionary];
66+
[self.flatItems enumerateObjectsUsingBlock:^(LookinDisplayItem *item, NSUInteger idx, BOOL *stop) {
67+
NSString *identifier = item.customInfo.semanticIdentifier;
68+
if (identifier.length > 0) {
69+
semanticExpansionState[identifier] = @(item.isExpanded);
70+
}
71+
}];
72+
}
73+
74+
NSArray<LookinDisplayItem *> *allItems = [LookinDisplayItem flatItemsFromHierarchicalItems:info.displayItems];
75+
LookinDisplayItem *semanticRoot = [allItems lookin_firstFiltered:^BOOL(LookinDisplayItem *item) {
76+
return [item.customInfo.semanticKind isEqualToString:@"swiftui-root"];
77+
}];
78+
BOOL wasAvailable = self.swiftUISemanticHierarchyAvailable;
79+
self.swiftUISemanticRoot = semanticRoot.copy;
80+
self.swiftUISemanticHierarchyAvailable = (semanticRoot != nil);
81+
if (!wasAvailable && self.swiftUISemanticHierarchyAvailable) {
82+
_showsSwiftUISemanticHierarchy = YES;
83+
} else if (!self.swiftUISemanticHierarchyAvailable) {
84+
_showsSwiftUISemanticHierarchy = NO;
85+
}
86+
5987
[super reloadWithHierarchyInfo:info keepState:keepState];
88+
89+
if (semanticExpansionState.count > 0) {
90+
[self.flatItems enumerateObjectsUsingBlock:^(LookinDisplayItem *item, NSUInteger idx, BOOL *stop) {
91+
NSString *identifier = item.customInfo.semanticIdentifier;
92+
if (identifier.length == 0) {
93+
return;
94+
}
95+
NSNumber *wasExpanded = semanticExpansionState[identifier];
96+
if (wasExpanded) {
97+
item.isExpanded = wasExpanded.boolValue;
98+
}
99+
}];
100+
[self buildDisplayingFlatItems];
101+
}
102+
if (selectedSemanticIdentifier.length > 0) {
103+
LookinDisplayItem *matchingItem = [self.flatItems lookin_firstFiltered:^BOOL(LookinDisplayItem *item) {
104+
return [item.customInfo.semanticIdentifier isEqualToString:selectedSemanticIdentifier];
105+
}];
106+
if (matchingItem) {
107+
self.selectedItem = matchingItem;
108+
}
109+
}
60110

61111
_appInfo = info.appInfo;
62112

@@ -81,6 +131,24 @@ - (void)reloadWithHierarchyInfo:(LookinHierarchyInfo *)info keepState:(BOOL)keep
81131
}
82132
}
83133

134+
- (NSArray<LookinDisplayItem *> *)hierarchyRootItemsForInfo:(LookinHierarchyInfo *)info {
135+
if (self.showsSwiftUISemanticHierarchy && self.swiftUISemanticRoot) {
136+
return @[self.swiftUISemanticRoot];
137+
}
138+
return [super hierarchyRootItemsForInfo:info];
139+
}
140+
141+
- (void)setShowsSwiftUISemanticHierarchy:(BOOL)showsSwiftUISemanticHierarchy {
142+
BOOL nextValue = showsSwiftUISemanticHierarchy && self.swiftUISemanticHierarchyAvailable;
143+
if (_showsSwiftUISemanticHierarchy == nextValue) {
144+
return;
145+
}
146+
_showsSwiftUISemanticHierarchy = nextValue;
147+
if (self.rawHierarchyInfo) {
148+
[self reloadWithHierarchyInfo:self.rawHierarchyInfo keepState:NO];
149+
}
150+
}
151+
84152
- (void)modifyWithDisplayItemDetail:(LookinDisplayItemDetail *)detail {
85153
if (!detail) {
86154
return;
@@ -155,7 +223,7 @@ - (void)modifyWithDisplayItemDetail:(LookinDisplayItemDetail *)detail {
155223

156224
displayItem.subitems = detail.subitems;
157225
// 根据 subitems 属性打平为二维数组,同时给每个 item 设置 indentLevel
158-
self.rawFlatItems = [LookinDisplayItem flatItemsFromHierarchicalItems:self.rawHierarchyInfo.displayItems];
226+
self.rawFlatItems = [LookinDisplayItem flatItemsFromHierarchicalItems:[self hierarchyRootItemsForInfo:self.rawHierarchyInfo]];
159227
self.flatItems = self.rawFlatItems;
160228
[self.didReloadHierarchyInfo sendNext:nil];
161229

LookinClient/Static/LKStaticWindowController.m

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ - (void)popupAllInspectableAppsWithSource:(MenuPopoverAppsListControllerEventSou
158158
}
159159

160160
- (NSArray<NSToolbarItemIdentifier> *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar {
161-
NSMutableArray *ret = @[LKToolBarIdentifier_Reload, LKToolBarIdentifier_FastMode, LKToolBarIdentifier_App, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Dimension, LKToolBarIdentifier_Rotation, LKToolBarIdentifier_Setting, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Scale, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Measure, LKToolBarIdentifier_Console].mutableCopy;
161+
NSMutableArray *ret = @[LKToolBarIdentifier_Reload, LKToolBarIdentifier_FastMode, LKToolBarIdentifier_App, LKToolBarIdentifier_SwiftUI, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Dimension, LKToolBarIdentifier_Rotation, LKToolBarIdentifier_Setting, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Scale, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Measure, LKToolBarIdentifier_Console].mutableCopy;
162162
if ([[[LKMessageManager sharedInstance] queryMessages] count] > 0) {
163163
[ret addObject:LKToolBarIdentifier_Message];
164164
[MSACAnalytics trackEvent:@"Show Notification"];
@@ -202,11 +202,27 @@ - (nullable NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(
202202
} else if ([item.itemIdentifier isEqualToString:LKToolBarIdentifier_FastMode]) {
203203
item.target = self;
204204
item.action = @selector(handleFastMode);
205+
} else if ([item.itemIdentifier isEqualToString:LKToolBarIdentifier_SwiftUI]) {
206+
NSButton *button = (NSButton *)item.view;
207+
button.target = self;
208+
button.action = @selector(_handleSwiftUIHierarchy:);
209+
LKStaticHierarchyDataSource *dataSource = [LKStaticHierarchyDataSource sharedInstance];
210+
[RACObserve(dataSource, swiftUISemanticHierarchyAvailable) subscribeNext:^(NSNumber *available) {
211+
button.enabled = available.boolValue;
212+
}];
213+
[RACObserve(dataSource, showsSwiftUISemanticHierarchy) subscribeNext:^(NSNumber *shows) {
214+
button.state = shows.boolValue ? NSControlStateValueOn : NSControlStateValueOff;
215+
}];
205216
}
206217
}
207218
return item;
208219
}
209220

221+
- (void)_handleSwiftUIHierarchy:(NSButton *)sender {
222+
LKStaticHierarchyDataSource *dataSource = [LKStaticHierarchyDataSource sharedInstance];
223+
dataSource.showsSwiftUISemanticHierarchy = (sender.state == NSControlStateValueOn);
224+
}
225+
210226
#pragma mark - Event Handler
211227

212228
- (void)_handleInspectingAppDidEnd:(id)obj {

LookinClient/Toolbar/LKWindowToolbarHelper.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ extern NSToolbarItemIdentifier const LKToolBarIdentifier_Remove;
2121
extern NSToolbarItemIdentifier const LKToolBarIdentifier_Measure;
2222
extern NSToolbarItemIdentifier const LKToolBarIdentifier_Message;
2323
extern NSToolbarItemIdentifier const LKToolBarIdentifier_FastMode;
24+
extern NSToolbarItemIdentifier const LKToolBarIdentifier_SwiftUI;
2425

2526
@class LKPreferenceManager, LookinAppInfo;
2627

0 commit comments

Comments
 (0)