From 809538fad244a5e4c48660f34c0cf634c863682d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=CC=81o=20Natan?= Date: Wed, 2 Aug 2023 03:19:00 +0300 Subject: [PATCH] Added support for macOS AppKit apps Restructured project Added an AppKit demo project --- .../LNViewHierarchyDumper/Info.plist | 2 +- .../LNViewHierarchyDumper+LibraryDebug.h | 16 + .../LNViewHierarchyDumper+LibraryDebug.m | 85 ++ .../LNViewHierarchyDumper+LibrarySupport.h | 59 ++ .../LNViewHierarchyDumper+LibrarySupport.m | 212 +++++ .../LNViewHierarchyDumper+PhaseSupport_iOS.h | 26 + .../LNViewHierarchyDumper+PhaseSupport_iOS.m | 536 +++++++++++ ...LNViewHierarchyDumper+PhaseSupport_macOS.h | 24 + ...LNViewHierarchyDumper+PhaseSupport_macOS.m | 462 ++++++++++ .../LNViewHierarchyDumper-Private.h | 22 + .../LNViewHierarchyDumper.m | 855 +---------------- .../LNViewHierarchyDumper/NSData+GZIP.h | 19 + .../LNViewHierarchyDumper/NSData+GZIP.m | 61 ++ Package.swift | 3 +- README.md | 6 +- .../AppDelegate.swift | 30 + ...AppKitViewHierarchyDumpTester.entitlements | 5 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 58 ++ .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/Main.storyboard | 861 ++++++++++++++++++ .../ViewController.swift | 82 ++ .../project.pbxproj | 180 +++- .../AppKitViewHierarchyDumpTester.xcscheme | 77 ++ 24 files changed, 2867 insertions(+), 831 deletions(-) create mode 100644 LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+LibraryDebug.h create mode 100644 LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+LibraryDebug.m create mode 100644 LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+LibrarySupport.h create mode 100644 LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+LibrarySupport.m create mode 100644 LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+PhaseSupport_iOS.h create mode 100644 LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+PhaseSupport_iOS.m create mode 100644 LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+PhaseSupport_macOS.h create mode 100644 LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+PhaseSupport_macOS.m create mode 100644 LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper-Private.h create mode 100644 LNViewHierarchyDumper/LNViewHierarchyDumper/NSData+GZIP.h create mode 100644 LNViewHierarchyDumper/LNViewHierarchyDumper/NSData+GZIP.m create mode 100644 ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/AppDelegate.swift create mode 100644 ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/AppKitViewHierarchyDumpTester.entitlements create mode 100644 ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/Assets.xcassets/Contents.json create mode 100644 ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/Base.lproj/Main.storyboard create mode 100644 ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/ViewController.swift create mode 100644 ViewHierarchyDumpTester/ViewHierarchyDumpTester.xcodeproj/xcshareddata/xcschemes/AppKitViewHierarchyDumpTester.xcscheme diff --git a/LNViewHierarchyDumper/LNViewHierarchyDumper/Info.plist b/LNViewHierarchyDumper/LNViewHierarchyDumper/Info.plist index 6561f9f..3014121 100644 --- a/LNViewHierarchyDumper/LNViewHierarchyDumper/Info.plist +++ b/LNViewHierarchyDumper/LNViewHierarchyDumper/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.2.1 + 1.2 CFBundleVersion 1 diff --git a/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+LibraryDebug.h b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+LibraryDebug.h new file mode 100644 index 0000000..f1c4788 --- /dev/null +++ b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+LibraryDebug.h @@ -0,0 +1,16 @@ +// +// LNViewHierarchyDumper+LibraryDebug.h +// +// +// Created by Leo Natan on 02/08/2023. +// + +#import "LNViewHierarchyDumper-Private.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface LNViewHierarchyDumper (LibraryDebug) + +@end + +NS_ASSUME_NONNULL_END diff --git a/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+LibraryDebug.m b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+LibraryDebug.m new file mode 100644 index 0000000..e8d6d72 --- /dev/null +++ b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+LibraryDebug.m @@ -0,0 +1,85 @@ +// +// LNViewHierarchyDumper+LibraryDebug.m +// +// +// Created by Leo Natan on 02/08/2023. +// + +#import "LNViewHierarchyDumper+LibraryDebug.h" + +//#if DEBUG +//@import ObjectiveC; +//@import Darwin; +//@import MachO.dyld; +//#endif + +@implementation LNViewHierarchyDumper (LibraryDebug) + +//#if DEBUG +//static void func(const struct mach_header* mh, intptr_t vmaddr_slide) +//{ +// Dl_info DlInfo; +// dladdr(mh, &DlInfo); +// const char* image_name = DlInfo.dli_fname; +// +// NSLog(@"%s", image_name); +//} +// +// +//static NSUInteger requestCounter = 0; +//static NSUInteger responseCounter = 0; +// +//__attribute__((constructor)) +//static void zzz(void) +//{ +// //We are looking for: +// // DebugHierarchyFoundation.framework +// // libViewDebuggerSupport.dylib +// _dyld_register_func_for_add_image(func); +// +// //The following code will dump requests made by Xcode View Hierarchy Inspector (and this framework) to ~/Desktop/Request_x.json +// { +// Class cls = NSClassFromString(@"DebugHierarchyRequest"); +// SEL sel = NSSelectorFromString(@"requestWithBase64Data:error:"); +// Method m = class_getClassMethod(cls, sel); +// id(*orig)(id, SEL, NSString*, NSError**) = (void*)method_getImplementation(m); +// +// method_setImplementation(m, imp_implementationWithBlock(^(id _self, NSString* request, NSError** error) { +// NSData* b64Data = [[NSData alloc] initWithBase64EncodedString:request options:0]; +// NSString* homePath = NSHomeDirectory(); +//#if TARGET_OS_SIMULATOR +// homePath = [homePath substringToIndex:[homePath rangeOfString:@"/Library"].location]; +//#endif +// NSLog(@"%@", homePath); +// NSURL* outputURL = [[NSURL fileURLWithPath:homePath] URLByAppendingPathComponent:[NSString stringWithFormat:@"Desktop/Request_%@.json", @(requestCounter++)]]; +// [b64Data writeToURL:outputURL atomically:YES]; +// +// return orig(_self, sel, request, error); +// })); +// } +// +// { +// Class cls = NSClassFromString(@"DebugHierarchyTargetHub"); +// SEL sel = NSSelectorFromString(@"performRequest:error:"); +// Method m = class_getInstanceMethod(cls, sel); +// NSData*(*orig)(id, SEL, id, NSError**) = (void*)method_getImplementation(m); +// method_setImplementation(m, imp_implementationWithBlock(^(id _self, id request, NSError** error) { +// NSData* rv = orig(_self, sel, request, error); +// +// if(rv != nil) +// { +// NSString* homePath = NSHomeDirectory(); +//#if TARGET_OS_SIMULATOR +// homePath = [homePath substringToIndex:[homePath rangeOfString:@"/Library"].location]; +//#endif +// NSURL* outputURL = [[NSURL fileURLWithPath:homePath] URLByAppendingPathComponent:[NSString stringWithFormat:@"Desktop/Response_%@.json", @(responseCounter++)]]; +// [rv writeToURL:outputURL atomically:YES]; +// } +// +// return rv; +// })); +// } +//} +//#endif + +@end diff --git a/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+LibrarySupport.h b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+LibrarySupport.h new file mode 100644 index 0000000..b7ebba1 --- /dev/null +++ b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+LibrarySupport.h @@ -0,0 +1,59 @@ +// +// LNViewHierarchyDumper+LibrarySupport.h +// ViewHierarchyDumpTester +// +// Created by Leo Natan on 02/08/2023. +// + +#import "LNViewHierarchyDumper.h" + +NS_ASSUME_NONNULL_BEGIN + +#if TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST + +@interface NSTask : NSObject @end + +@interface NSTask () + +@property (nullable, copy) NSURL* executableURL; +@property (nullable, copy) NSArray* arguments; +@property (nullable, copy) NSDictionary* environment; +@property (nullable, retain) id standardOutput; +@property (nullable, retain) id standardError; + +@property(readonly) int terminationStatus; + +@property(copy, nonnull) void (^terminationHandler)(NSTask* _Nonnull task); + +- (BOOL)launch; + +@end + +#endif + +@interface NSObject () + +//DBGTargetHub ++ (id)sharedHub; +- (NSData*)performRequestWithRequestInBase64:(NSString*)arg1; + +//DebugHierarchyTargetHub +- (nullable id)performRequest:(id /*DBGTargetHub*/)arg1 error:(NSError**)error; + +//DebugHierarchyRequest ++ (nullable id)requestWithBase64Data:(NSString*)arg1 error:(NSError**)arg2; + +//NSTask +- (BOOL)launchAndReturnError:(out NSError **_Nullable)error; + +@end + +@interface LNViewHierarchyDumper (LibrarySupport) + ++ (NSURL*)_xcodeURLOrError:(out NSError* __strong * _Nullable)outError; ++ (nullable NSURL*)_debugHierarchyFoundationFrameworkURL:(NSURL*)runtimeURL error:(out NSError** _Nullable)error; ++ (nullable NSURL*)_libViewDebuggerSupportURL:(NSURL*)runtimeURL error:(out NSError** _Nullable)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+LibrarySupport.m b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+LibrarySupport.m new file mode 100644 index 0000000..687664c --- /dev/null +++ b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+LibrarySupport.m @@ -0,0 +1,212 @@ +// +// LNViewHierarchyDumper+LibrarySupport.m +// ViewHierarchyDumpTester +// +// Created by Leo Natan on 02/08/2023. +// + +#import "LNViewHierarchyDumper+LibrarySupport.h" + +@implementation LNViewHierarchyDumper (LibrarySupport) + ++ (BOOL)_isMacSandboxed +{ + return NSProcessInfo.processInfo.environment[@"APP_SANDBOX_CONTAINER_ID"].length > 0; +} + ++ (NSURL*)_xcodeURLOrError:(out NSError* __strong * _Nullable)outError +{ + static NSURL* rv = nil; + static NSError* error = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + if(self._isMacSandboxed) + { + rv = [NSURL fileURLWithPath:@"/Applications/Xcode.app/Contents"]; + if([rv checkResourceIsReachableAndReturnError:NULL] == NO) + { + rv = [NSURL fileURLWithPath:@"/Applications/Xcode-beta.app/Contents"]; + if([rv checkResourceIsReachableAndReturnError:NULL] == NO) + { + rv = NULL; + error = [NSError errorWithDomain:@"LNViewHierarchyDumperDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: @"App is sandboxed and cannot find Xcode in\n/Applications/Xcode.app\nor\n/Applications/Xcode-beta.app"}]; + return; + } + } + } + else + { + NSTask* whichXcodeTask = [NSTask new]; + [whichXcodeTask setValue:[NSURL fileURLWithPath:@"/usr/bin/xcode-select"] forKey:@"executableURL"]; + whichXcodeTask.arguments = @[@"-p"]; + whichXcodeTask.environment = @{}; + + NSPipe* outPipe = [NSPipe pipe]; + NSMutableData* outData = [NSMutableData new]; + whichXcodeTask.standardOutput = outPipe; + outPipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle * _Nonnull fileHandle) { + [outData appendData:fileHandle.availableData]; + }; + + NSPipe* errPipe = [NSPipe pipe]; + NSMutableData* errData = [NSMutableData new]; + whichXcodeTask.standardError = errPipe; + errPipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle * _Nonnull fileHandle) { + [errData appendData:fileHandle.availableData]; + }; + + dispatch_semaphore_t waitForTermination = dispatch_semaphore_create(0); + + [whichXcodeTask setValue:^(NSTask* _Nonnull task) { + outPipe.fileHandleForReading.readabilityHandler = nil; + errPipe.fileHandleForReading.readabilityHandler = nil; + + dispatch_semaphore_signal(waitForTermination); + } forKey:@"terminationHandler"]; + + NSError* taskError; + [(id)whichXcodeTask launchAndReturnError:&taskError]; + + if(taskError == nil) + { + dispatch_semaphore_wait(waitForTermination, DISPATCH_TIME_FOREVER); + } + else + { + error = taskError; + return; + } + + if(whichXcodeTask.terminationStatus != 0) + { + NSString* errString = [[NSString alloc] initWithData:errData encoding:NSUTF8StringEncoding]; + error = [NSError errorWithDomain:@"LNViewHierarchyDumperDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: errString}]; + return; + } + + NSString* xcodePath = [[[NSString alloc] initWithData:outData encoding:NSUTF8StringEncoding] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; + + if(xcodePath.length == 0) + { + error = [NSError errorWithDomain:@"LNViewHierarchyDumperDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Unable to find the current active developer directory"}]; + + return; + } + + rv = [[[NSURL fileURLWithPath:xcodePath] URLByAppendingPathComponent:@".."] URLByStandardizingPath]; + } + }); + if(outError != NULL) + { + *outError = error; + } + return rv; +} + ++ (NSURL*)_debugHierarchyFoundationFrameworkURL_iOS:(NSURL*)runtimeURL +{ + NSString* rv = nil; + if(@available(iOS 17, *)) + { + rv = @"System/Library/PrivateFrameworks/DebugHierarchyFoundation.framework"; + } + else + { + rv = @"Developer/Library/PrivateFrameworks/DebugHierarchyFoundation.framework"; + } +#if TARGET_OS_SIMULATOR + return [runtimeURL URLByAppendingPathComponent:rv]; +#else + return [NSURL fileURLWithPath:[NSString stringWithFormat:@"/%@", rv]]; +#endif +} + ++ (NSURL*)_debugHierarchyFoundationFrameworkURL_macOS:(NSURL*)runtimeURL isCatalyst:(BOOL)isCatalyst +{ + return [runtimeURL URLByAppendingPathComponent:@"SharedFrameworks/DebugHierarchyFoundation.framework"]; +} + ++ (nullable NSURL*)_debugHierarchyFoundationFrameworkURL:(NSURL*)runtimeURL error:(out NSError** _Nullable)error +{ +#if TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST + if (@available(iOS 14.0, *)) + { + if(NSProcessInfo.processInfo.isiOSAppOnMac) + { + NSError* xcodeError = nil; + NSURL* xcodeURL = [self _xcodeURLOrError:&xcodeError]; + if(xcodeError) + { + if(error) { *error = xcodeError; } + return nil; + } + + return [self _debugHierarchyFoundationFrameworkURL_macOS:xcodeURL isCatalyst:YES]; + } + } + return [self _debugHierarchyFoundationFrameworkURL_iOS:runtimeURL]; +#else +#if TARGET_OS_MACCATALYST + BOOL isCatalyst = YES; +#else + BOOL isCatalyst = NO; +#endif + return [self _debugHierarchyFoundationFrameworkURL_macOS:runtimeURL isCatalyst:isCatalyst]; +#endif +} + ++ (NSURL*)_libViewDebuggerSupportURL_iOS:(NSURL*)runtimeURL +{ + NSString* rv = nil; + if(@available(iOS 17, *)) + { + rv = @"usr/lib/libViewDebuggerSupport.dylib"; + } + else + { + rv = @"Developer/Library/PrivateFrameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib"; + } +#if TARGET_OS_SIMULATOR + return [runtimeURL URLByAppendingPathComponent:rv]; +#else + return [NSURL fileURLWithPath:[NSString stringWithFormat:@"/%@", rv]]; +#endif +} + ++ (NSURL*)_libViewDebuggerSupportURL_macOS:(NSURL*)runtimeURL isCatalyst:(BOOL)isCatalyst +{ + NSString* libName = isCatalyst ? @"libViewDebuggerSupport_macCatalyst.dylib" : @"libViewDebuggerSupport.dylib"; + return [runtimeURL URLByAppendingPathComponent:[NSString stringWithFormat:@"Developer/Platforms/MacOSX.platform/Developer/Library/Debugger/%@", libName]]; +} + ++ (nullable NSURL*)_libViewDebuggerSupportURL:(NSURL*)runtimeURL error:(out NSError** _Nullable)error +{ +#if TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST + if (@available(iOS 14.0, *)) + { + if(NSProcessInfo.processInfo.isiOSAppOnMac) + { + NSError* xcodeError = nil; + NSURL* xcodeURL = [self _xcodeURLOrError:&xcodeError]; + if(xcodeError) + { + if(error) { *error = xcodeError; } + return nil; + } + + return [self _libViewDebuggerSupportURL_macOS:xcodeURL isCatalyst:YES]; + } + } + return [self _libViewDebuggerSupportURL_iOS:runtimeURL]; +#else +#if TARGET_OS_MACCATALYST + BOOL isCatalyst = YES; +#else + BOOL isCatalyst = NO; +#endif + return [self _libViewDebuggerSupportURL_macOS:runtimeURL isCatalyst:isCatalyst]; +#endif +} + + +@end diff --git a/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+PhaseSupport_iOS.h b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+PhaseSupport_iOS.h new file mode 100644 index 0000000..c91f946 --- /dev/null +++ b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+PhaseSupport_iOS.h @@ -0,0 +1,26 @@ +// +// LNViewHierarchyDumper+PhaseSupport_iOS.h +// +// +// Created by Leo Natan on 02/08/2023. +// + +#import "LNViewHierarchyDumper-Private.h" + +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST + +NS_ASSUME_NONNULL_BEGIN + +NSArray* LNExtractPhoneOSPhase1ForPhase2ResponseObjects(NSDictionary* phase1Response); +NSArray* LNExtractPhoneOSPhase1ForPhase3ResponseObjects(NSDictionary* phase1Response); +NSArray* LNExtractPhoneOSPhase1ForPhase4ResponseObjects(NSDictionary* phase1Response); + +@interface LNViewHierarchyDumper (PhaseSupport_iOS) + +- (BOOL)_startPhasesWithHub:(id /*DebugHierarchyTargetHub*/)sharedHub outputURL:(NSURL*)outputURL error:(NSError**)error; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+PhaseSupport_iOS.m b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+PhaseSupport_iOS.m new file mode 100644 index 0000000..2c45412 --- /dev/null +++ b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+PhaseSupport_iOS.m @@ -0,0 +1,536 @@ +// +// LNViewHierarchyDumper+PhaseSupport_iOS.m +// +// +// Created by Leo Natan on 02/08/2023. +// + +#import "LNViewHierarchyDumper-Private.h" +#import "LNViewHierarchyDumper+PhaseSupport_iOS.h" + +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST + +NSArray* LNExtractPhoneOSPhase0ForPhase1ResponseObjects(NSDictionary* phase1Response) +{ + NSArray* topLevelGroupsCALayers = phase1Response[@"topLevelGroups"][@"com.apple.QuartzCore.CALayer"][@"debugHierarchyObjects"]; + + return [topLevelGroupsCALayers valueForKey:@"objectID"]; +} + +/* +NSArray* LNExtractPhoneOSPhase0ForPhase2ResponseObjects(NSDictionary* phase1Response) +{ + NSArray* topLevelGroupsCALayers = phase1Response[@"topLevelGroups"][@"com.apple.QuartzCore.CALayer"][@"debugHierarchyObjects"]; + + return [topLevelGroupsCALayers valueForKey:@"objectID"]; +} + +NSArray* LNExtractPhoneOSPhase0ForPhase3ResponseObjects(NSDictionary* phase1Response) +{ + NSArray* topLevelGroupsCALayers = phase1Response[@"topLevelGroups"][@"com.apple.QuartzCore.CALayer"][@"debugHierarchyObjects"]; + + return [topLevelGroupsCALayers valueForKey:@"objectID"]; +} + */ + +@implementation LNViewHierarchyDumper (PhaseSupport_iOS) + +- (BOOL)_startPhasesWithHub:(id /*DebugHierarchyTargetHub*/)sharedHub outputURL:(NSURL*)outputURL error:(NSError**)error +{ + NSURL* requestResponses = [outputURL URLByAppendingPathComponent:@"RequestResponses" isDirectory:YES]; + RETURN_IF_FALSE([NSFileManager.defaultManager createDirectoryAtURL:requestResponses withIntermediateDirectories:YES attributes:nil error:error]); + + NSDictionary* phase0Dictionary = @{ + @"DBGHierarchyRequestName": @"Initial request", + @"DBGHierarchyRequestInitiatorVersionKey": @3, + @"DBGHierarchyRequestPriority": @0, + @"DBGHierarchyObjectDiscovery": @1, + @"DBGHierarchyRequestActions": @[ + @{ + @"actionClass": @"DebugHierarchyResetAction" + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"dbgFormattedDisplayName" + ], + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": NSNull.null, + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @0 + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"anchorPoint", + @"anchorPointZ", + @"bounds", + @"contentsScale", + @"doubleSided", + @"frame", + @"geometryFlipped", + @"masksToBounds", + @"name", + @"opacity", + @"opaque", + @"position", + @"sublayerTransform", + @"transform", + @"zPosition", + @"dbg_ListID", + @"optimizationOpportunities" + ], + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": @[ + @"CALayer" + ], + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @0 + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"constant", + @"firstAttribute", + @"firstItem", + @"identifier", + @"multiplier", + @"priority", + @"relation", + @"secondAttribute", + @"secondItem" + ], + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": @[ + @"NSLayoutConstraint" + ], + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @0 + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"anchorPoint", + @"frame", + @"hidden", + @"position", + @"untransformedSize", + @"visualRepresentationOffset", + @"xScale", + @"yScale", + @"zPosition", + @"zRotation" + ], + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": @[ + @"SKNode" + ], + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @0 + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"scaleMode", + @"size" + ], + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": @[ + @"SKScene" + ], + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @0 + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @1, + @"propertyNames": NSNull.null, + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @3, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": @[ + @"SKView" + ], + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyActionLegacyV1", + @"propertyNamesAreExclusive": @0 + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @1, + @"propertyNames": NSNull.null, + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @3, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": @[ + @"SKScene" + ], + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyActionLegacyV1", + @"propertyNamesAreExclusive": @0 + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @1, + @"propertyNames": NSNull.null, + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @3, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": @[ + @"SKNode" + ], + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyActionLegacyV1", + @"propertyNamesAreExclusive": @0 + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"displayCornerRadius", + @"nativeBounds", + @"nativeDisplayBounds", + @"nativeScale", + @"scale", + @"dbgScreenShape", + @"dbgScreenShapeIsRectangular" + ], + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": @[ + @"UIScreen" + ], + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @0 + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"keyWindow", + @"statusBarOrientation" + ], + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": @[ + @"UIApplication" + ], + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @0 + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"title", + @"activationState" + ], + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": @[ + @"UIScene" + ], + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @0 + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"title", + @"internal", + @"visible" + ], + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": @[ + @"UIWindow" + ], + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @0 + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"title" + ], + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": @[ + @"UIViewController" + ], + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @0 + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"alpha", + @"ambiguityStatusMask", + @"bounds", + @"dbgViewForFirstBaselineLayout", + @"dbgViewForLastBaselineLayout", + @"firstBaselineOffsetFromTop", + @"frame", + @"hasAmbiguousLayout", + @"hidden", + @"horizontalAffectingConstraints", + @"lastBaselineOffsetFromBottom", + @"layoutMargins", + @"verticalAffectingConstraints", + @"dbgSubviewHierarchy" + ], + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": @[ + @"UIView" + ], + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @0 + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"layoutFrame", + @"identifier" + ], + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": @[ + @"UILayoutGuide" + ], + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @0 + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"dbg_holdsSymbolImage" + ], + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": @[ + @"UIImageView" + ], + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @0 + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"axis" + ], + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": @[ + @"UIStackView" + ], + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @0 + } + ], + @"DBGHierarchyRequestIdentifier": NSUUID.UUID.UUIDString, + @"DBGHierarchyRequestTransportCompression": @YES + }; + + NSDictionary* phase0Response = [self _executeRequestPhaseWithRequest:phase0Dictionary hub:sharedHub outputURL:requestResponses phaseCount:0 error:error]; + RETURN_IF_NIL(phase0Response); + + NSArray* objects = LNExtractPhoneOSPhase0ForPhase1ResponseObjects(phase0Response); + RETURN_IF_NIL(objects); + + NSDictionary* phase1Dictionary = @{ + @"DBGHierarchyRequestName": @"Fetch encoded layers", + @"DBGHierarchyRequestInitiatorVersionKey": @3, + @"DBGHierarchyRequestPriority": @0, + @"DBGHierarchyObjectDiscovery": @2, + @"DBGHierarchyRequestActions": @[ + @{ + @"objectIdentifiers": objects, + @"options": @0, + @"propertyNames": @[ + @"encodedPresentationLayer" + ], + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": NSNull.null, + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @0 + } + ], + @"DBGHierarchyRequestIdentifier": NSUUID.UUID.UUIDString, + @"DBGHierarchyRequestTransportCompression": @YES + }; + + RETURN_IF_NIL([self _executeRequestPhaseWithRequest:phase1Dictionary hub:sharedHub outputURL:requestResponses phaseCount:1 error:error]); + + NSDictionary* phase2Dictionary = @{ + @"DBGHierarchyRequestName": @"Fetch remaining lazy properties", + @"DBGHierarchyRequestInitiatorVersionKey": @3, + @"DBGHierarchyRequestPriority": @0, + @"DBGHierarchyObjectDiscovery": @2, + @"DBGHierarchyRequestActions": @[ + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"encodedPresentationLayer", + @"encodedPresentationScene", + @"dbgFormattedDisplayName", + @"snapshotImage", + @"snapshotImageRenderedUsingDrawHierarchyInRect", + @"visualRepresentation", + @"dbgSubviewHierarchy" + ], + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": NSNull.null, + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @1 + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"encodedPresentationLayer", + @"encodedPresentationScene", + @"dbgFormattedDisplayName", + @"snapshotImage", + @"snapshotImageRenderedUsingDrawHierarchyInRect", + @"visualRepresentation", + @"dbgSubviewHierarchy" + ], + @"exactTypesAreExclusive": @0, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @0, + @"visibility": @15, + @"types": NSNull.null, + @"objectIdentifiersAreExclusive": @0, + @"actionClass": @"DebugHierarchyPropertyActionLegacyV1", + @"propertyNamesAreExclusive": @1 + } + ], + @"DBGHierarchyRequestIdentifier": NSUUID.UUID.UUIDString, + @"DBGHierarchyRequestTransportCompression": @YES + }; + + RETURN_IF_NIL([self _executeRequestPhaseWithRequest:phase1Dictionary hub:sharedHub outputURL:requestResponses phaseCount:2 error:error]); + + NSDictionary* cleanupPhaseDictionary = @{ + @"DBGHierarchyRequestName": @"Cleanup", + @"DBGHierarchyRequestInitiatorVersionKey": @3, + @"DBGHierarchyRequestPriority": @0, + @"DBGHierarchyObjectDiscovery": @0, + @"DBGHierarchyRequestActions": @[ + @{ + @"actionClass": @"DebugHierarchyResetAction" + } + ], + @"DBGHierarchyRequestIdentifier": NSUUID.UUID.UUIDString, + @"DBGHierarchyRequestTransportCompression": @NO + }; + + RETURN_IF_NIL([self _executeRequestPhaseWithRequest:cleanupPhaseDictionary hub:sharedHub outputURL:requestResponses phaseCount:-1 error:error]); + + NSDictionary* metadata = @{ + @"DocumentVersion": @"1", + @"RunnableDisplayName": [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleName"], + @"RunnablePID": @(NSProcessInfo.processInfo.processIdentifier) + }; + + NSData* data = [NSPropertyListSerialization dataWithPropertyList:metadata format:NSPropertyListXMLFormat_v1_0 options:0 error:error]; + RETURN_IF_NIL(data); + RETURN_IF_FALSE([data writeToURL:[outputURL URLByAppendingPathComponent:@"metadata"] options:NSDataWritingAtomic error:error]); + + return YES; +} + +@end + +#endif diff --git a/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+PhaseSupport_macOS.h b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+PhaseSupport_macOS.h new file mode 100644 index 0000000..196f52c --- /dev/null +++ b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+PhaseSupport_macOS.h @@ -0,0 +1,24 @@ +// +// LNViewHierarchyDumper+PhaseSupport_macOS.h +// +// +// Created by Leo Natan on 02/08/2023. +// + +#import "LNViewHierarchyDumper.h" + +#if TARGET_OS_OSX + +NS_ASSUME_NONNULL_BEGIN + +NSArray* LNExtractMacOSPhase1ForPhase2ResponseObjects(NSDictionary* phase1Response); + +@interface LNViewHierarchyDumper (PhaseSupport_macOS) + +- (BOOL)_startPhasesWithHub:(id /*DebugHierarchyTargetHub*/)sharedHub outputURL:(NSURL*)outputURL error:(NSError**)error; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+PhaseSupport_macOS.m b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+PhaseSupport_macOS.m new file mode 100644 index 0000000..7108db9 --- /dev/null +++ b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper+PhaseSupport_macOS.m @@ -0,0 +1,462 @@ +// +// LNViewHierarchyDumper+PhaseSupport_macOS.m +// +// +// Created by Leo Natan on 02/08/2023. +// + +#import "LNViewHierarchyDumper-Private.h" +#import "LNViewHierarchyDumper+PhaseSupport_macOS.h" + +#if TARGET_OS_OSX + +NSArray* LNExtractPhase0ForPhase1ResponseObjects(NSDictionary* phase1Response) +{ + NSArray* topLevelGroupsCALayers = phase1Response[@"topLevelGroups"][@"com.apple.QuartzCore.CALayer"][@"debugHierarchyObjects"]; + + return [topLevelGroupsCALayers valueForKey:@"objectID"]; +} + + +@implementation LNViewHierarchyDumper (PhaseSupport_macOS) + +- (BOOL)_startPhasesWithHub:(id /*DebugHierarchyTargetHub*/)sharedHub outputURL:(NSURL*)outputURL error:(NSError**)error +{ + NSURL* requestResponses = [outputURL URLByAppendingPathComponent:@"RequestResponses" isDirectory:YES]; + RETURN_IF_FALSE([NSFileManager.defaultManager createDirectoryAtURL:requestResponses withIntermediateDirectories:YES attributes:nil error:error]); + + NSDictionary* phase0Dictionary = @{ + @"DBGHierarchyRequestName": @"Initial request", + @"DBGHierarchyRequestInitiatorVersionKey": @4, + @"DBGHierarchyRequestPriority": @0, + @"DBGHierarchyObjectDiscovery": @1, + @"DBGHierarchyRequestActions": @[ + @{ + @"actionClass": @"DebugHierarchyResetAction" + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"dbgFormattedDisplayName" + ], + @"exactTypesAreExclusive": @NO, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @NO, + @"visibility": @15, + @"types": NSNull.null, + @"objectIdentifiersAreExclusive": @NO, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @NO + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"anchorPoint", + @"anchorPointZ", + @"bounds", + @"contentsScale", + @"doubleSided", + @"frame", + @"geometryFlipped", + @"masksToBounds", + @"name", + @"opacity", + @"opaque", + @"position", + @"sublayerTransform", + @"transform", + @"zPosition", + @"dbg_ListID", + @"optimizationOpportunities" + ], + @"exactTypesAreExclusive": @NO, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @NO, + @"visibility": @15, + @"types": @[ + @"CALayer" + ], + @"objectIdentifiersAreExclusive": @NO, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @NO + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"constant", + @"firstAttribute", + @"firstItem", + @"identifier", + @"multiplier", + @"priority", + @"relation", + @"secondAttribute", + @"secondItem" + ], + @"exactTypesAreExclusive": @NO, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @NO, + @"visibility": @15, + @"types": @[ + @"NSLayoutConstraint" + ], + @"objectIdentifiersAreExclusive": @NO, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @NO + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"anchorPoint", + @"frame", + @"hidden", + @"position", + @"untransformedSize", + @"visualRepresentationOffset", + @"xScale", + @"yScale", + @"zPosition", + @"zRotation" + ], + @"exactTypesAreExclusive": @NO, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @NO, + @"visibility": @15, + @"types": @[ + @"SKNode" + ], + @"objectIdentifiersAreExclusive": @NO, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @NO + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"scaleMode", + @"size" + ], + @"exactTypesAreExclusive": @NO, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @NO, + @"visibility": @15, + @"types": @[ + @"SKScene" + ], + @"objectIdentifiersAreExclusive": @NO, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @NO + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @1, + @"propertyNames": NSNull.null, + @"exactTypesAreExclusive": @NO, + @"optionsComparisonStyle": @3, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @NO, + @"visibility": @15, + @"types": @[ + @"SKView" + ], + @"objectIdentifiersAreExclusive": @NO, + @"actionClass": @"DebugHierarchyPropertyActionLegacyV1", + @"propertyNamesAreExclusive": @NO + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @1, + @"propertyNames": NSNull.null, + @"exactTypesAreExclusive": @NO, + @"optionsComparisonStyle": @3, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @NO, + @"visibility": @15, + @"types": @[ + @"SKScene" + ], + @"objectIdentifiersAreExclusive": @NO, + @"actionClass": @"DebugHierarchyPropertyActionLegacyV1", + @"propertyNamesAreExclusive": @NO + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @1, + @"propertyNames": NSNull.null, + @"exactTypesAreExclusive": @NO, + @"optionsComparisonStyle": @3, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @NO, + @"visibility": @15, + @"types": @[ + @"SKNode" + ], + @"objectIdentifiersAreExclusive": @NO, + @"actionClass": @"DebugHierarchyPropertyActionLegacyV1", + @"propertyNamesAreExclusive": @NO + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"keyWindow", + @"mainWindow" + ], + @"exactTypesAreExclusive": @NO, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @NO, + @"visibility": @15, + @"types": @[ + @"NSApplication" + ], + @"objectIdentifiersAreExclusive": @NO, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @NO + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"frame", + @"title", + @"visible", + @"attachedSheet", + @"backingScaleFactor" + ], + @"exactTypesAreExclusive": @NO, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @NO, + @"visibility": @15, + @"types": @[ + @"NSWindow" + ], + @"objectIdentifiersAreExclusive": @NO, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @NO + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"title" + ], + @"exactTypesAreExclusive": @NO, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @NO, + @"visibility": @15, + @"types": @[ + @"NSViewController" + ], + @"objectIdentifiersAreExclusive": @NO, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @NO + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"bounds", + @"firstBaselineOffsetFromTop", + @"flipped", + @"frame", + @"hidden", + @"lastBaselineOffsetFromBottom", + @"visibleRect", + @"wantsDefaultClipping", + @"ambiguityStatusMask", + @"frameAlignmentRect", + @"hasAmbiguousLayout", + @"horizontalAffectingConstraints", + @"verticalAffectingConstraints", + @"dbgSubviewHierarchy" + ], + @"exactTypesAreExclusive": @NO, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @NO, + @"visibility": @15, + @"types": @[ + @"NSView" + ], + @"objectIdentifiersAreExclusive": @NO, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @NO + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"orientation" + ], + @"exactTypesAreExclusive": @NO, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @NO, + @"visibility": @15, + @"types": @[ + @"NSStackView" + ], + @"objectIdentifiersAreExclusive": @NO, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @NO + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"frame", + @"identifier" + ], + @"exactTypesAreExclusive": @NO, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @NO, + @"visibility": @15, + @"types": @[ + @"NSLayoutGuide" + ], + @"objectIdentifiersAreExclusive": @NO, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @NO + } + ], + @"DBGHierarchyRequestIdentifier": NSUUID.UUID.UUIDString, + @"DBGHierarchyRequestTransportCompression": @NO + }; + + NSDictionary* phase0Response = [self _executeRequestPhaseWithRequest:phase0Dictionary hub:sharedHub outputURL:requestResponses phaseCount:0 error:error]; + RETURN_IF_NIL(phase0Response); + + NSArray* objects = LNExtractPhase0ForPhase1ResponseObjects(phase0Response); + RETURN_IF_NIL(objects); + + NSDictionary* phase1Dictionary = @{ + @"DBGHierarchyRequestName": @"Fetch encoded layers", + @"DBGHierarchyRequestInitiatorVersionKey": @4, + @"DBGHierarchyRequestPriority": @0, + @"DBGHierarchyObjectDiscovery": @2, + @"DBGHierarchyRequestActions": @[ + @{ + @"objectIdentifiers": objects, + @"options": @0, + @"propertyNames": @[ + @"encodedPresentationLayer" + ], + @"exactTypesAreExclusive": @NO, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @NO, + @"visibility": @15, + @"types": NSNull.null, + @"objectIdentifiersAreExclusive": @NO, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @NO + } + ], + @"DBGHierarchyRequestIdentifier": NSUUID.UUID.UUIDString, + @"DBGHierarchyRequestTransportCompression": @NO + }; + + RETURN_IF_NIL([self _executeRequestPhaseWithRequest:phase1Dictionary hub:sharedHub outputURL:requestResponses phaseCount:1 error:error]); + + NSDictionary* phase2Dictionary = @{ + @"DBGHierarchyRequestName": @"Fetch remaining lazy properties", + @"DBGHierarchyRequestInitiatorVersionKey": @4, + @"DBGHierarchyRequestPriority": @0, + @"DBGHierarchyObjectDiscovery": @2, + @"DBGHierarchyRequestActions": @[ + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"encodedPresentationLayer", + @"encodedPresentationScene", + @"dbgFormattedDisplayName", + @"snapshotImage", + @"snapshotImageRenderedUsingDrawHierarchyInRect", + @"visualRepresentation", + @"dbgSubviewHierarchy" + ], + @"exactTypesAreExclusive": @NO, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @NO, + @"visibility": @15, + @"types": NSNull.null, + @"objectIdentifiersAreExclusive": @NO, + @"actionClass": @"DebugHierarchyPropertyAction", + @"propertyNamesAreExclusive": @YES + }, + @{ + @"objectIdentifiers": NSNull.null, + @"options": @0, + @"propertyNames": @[ + @"encodedPresentationLayer", + @"encodedPresentationScene", + @"dbgFormattedDisplayName", + @"snapshotImage", + @"snapshotImageRenderedUsingDrawHierarchyInRect", + @"visualRepresentation", + @"dbgSubviewHierarchy" + ], + @"exactTypesAreExclusive": @NO, + @"optionsComparisonStyle": @0, + @"exactTypes": NSNull.null, + @"typesAreExclusive": @NO, + @"visibility": @15, + @"types": NSNull.null, + @"objectIdentifiersAreExclusive": @NO, + @"actionClass": @"DebugHierarchyPropertyActionLegacyV1", + @"propertyNamesAreExclusive": @YES + } + ], + @"DBGHierarchyRequestIdentifier": NSUUID.UUID.UUIDString, + @"DBGHierarchyRequestTransportCompression": @NO + }; + + RETURN_IF_NIL([self _executeRequestPhaseWithRequest:phase1Dictionary hub:sharedHub outputURL:requestResponses phaseCount:2 error:error]); + + NSDictionary* cleanupPhaseDictionary = @{ + @"DBGHierarchyRequestName": @"Cleanup", + @"DBGHierarchyRequestInitiatorVersionKey": @4, + @"DBGHierarchyRequestPriority": @0, + @"DBGHierarchyObjectDiscovery": @0, + @"DBGHierarchyRequestActions": @[ + @{ + @"actionClass": @"DebugHierarchyResetAction" + } + ], + @"DBGHierarchyRequestIdentifier": NSUUID.UUID.UUIDString, + @"DBGHierarchyRequestTransportCompression": @NO + }; + + RETURN_IF_NIL([self _executeRequestPhaseWithRequest:cleanupPhaseDictionary hub:sharedHub outputURL:requestResponses phaseCount:-1 error:error]); + + NSDictionary* metadata = @{ + @"DocumentVersion": @"1", + @"RunnableDisplayName": [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleName"], + @"RunnablePID": @(NSProcessInfo.processInfo.processIdentifier) + }; + + NSData* data = [NSPropertyListSerialization dataWithPropertyList:metadata format:NSPropertyListXMLFormat_v1_0 options:0 error:error]; + RETURN_IF_NIL(data); + RETURN_IF_FALSE([data writeToURL:[outputURL URLByAppendingPathComponent:@"metadata"] options:NSDataWritingAtomic error:error]); + + return YES; +} + +@end + +#endif diff --git a/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper-Private.h b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper-Private.h new file mode 100644 index 0000000..3c6946e --- /dev/null +++ b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper-Private.h @@ -0,0 +1,22 @@ +// +// Header.h +// +// +// Created by Leo Natan on 02/08/2023. +// + +#import + +#define GENERIC_ERROR_IF_NEEDED() if(error && !*error) { *error = [NSError errorWithDomain:@"LNViewHierarchyDumperDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Unknown error encountered; try restarting your simulator, Xcode or Mac (reminder: this framework is as buggy as or as bug-free as Xcode's visual inspector infrastructure.)"}]; } +#define RETURN_IF_FALSE(cmd) if((cmd) == NO) { GENERIC_ERROR_IF_NEEDED(); return NO; } +#define RETURN_IF_NIL(cmd) if((cmd) == nil) { GENERIC_ERROR_IF_NEEDED(); return NO; } + +NS_ASSUME_NONNULL_BEGIN + +@interface LNViewHierarchyDumper () + +- (nullable NSDictionary*)_executeRequestPhaseWithRequest:(NSDictionary*)request hub:(id /*DebugHierarchyTargetHub*/)sharedHub outputURL:(NSURL*)outputURL phaseCount:(NSInteger)count error:(NSError**)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper.m b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper.m index e268e3a..11cabda 100644 --- a/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper.m +++ b/LNViewHierarchyDumper/LNViewHierarchyDumper/LNViewHierarchyDumper.m @@ -5,58 +5,22 @@ // Created by Leo Natan (Wix) on 7/3/20. // -#import -#include +#import "LNViewHierarchyDumper-Private.h" +#import "LNViewHierarchyDumper+LibrarySupport.h" +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST +#import "LNViewHierarchyDumper+PhaseSupport_iOS.h" +#else +#import "LNViewHierarchyDumper+PhaseSupport_macOS.h" +#endif +#import "NSData+GZIP.h" + @import Darwin; +@import MachO.dyld; #if TARGET_OS_MACCATALYST || TARGET_OS_OSX @import AppKit; #endif -#if DEBUG -@import ObjectiveC; -#endif - - -#if TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST - -@interface NSTask : NSObject @end - -@interface NSTask () - -@property (nullable, copy) NSURL* executableURL; -@property (nullable, copy) NSArray* arguments; -@property (nullable, copy) NSDictionary* environment; -@property (nullable, retain) id standardOutput; -@property (nullable, retain) id standardError; - -@property(readonly) int terminationStatus; - -@property(copy, nonnull) void (^terminationHandler)(NSTask* _Nonnull task); - -- (BOOL)launch; - -@end - -#endif - -@interface NSObject () - -//DBGTargetHub -+ (id)sharedHub; -- (NSData*)performRequestWithRequestInBase64:(NSString*)arg1; - -//DebugHierarchyTargetHub -- (id)performRequest:(id /*DBGTargetHub*/)arg1 error:(NSError**)error; - -//DebugHierarchyRequest -+ (id)requestWithBase64Data:(NSString*)arg1 error:(NSError**)arg2; - -//NSTask -- (BOOL)launchAndReturnError:(out NSError **_Nullable)error; - -@end - #if defined(__IPHONE_14_0) || defined(__MAC_10_16) || defined(__MAC_11_0) || defined(__TVOS_14_0) || defined(__WATCHOS_7_0) __attribute__((objc_direct_members)) #endif @@ -94,227 +58,6 @@ - (instancetype)_init return self; } -#if DEBUG -//static void func(const struct mach_header* mh, intptr_t vmaddr_slide) -//{ -// Dl_info DlInfo; -// dladdr(mh, &DlInfo); -// const char* image_name = DlInfo.dli_fname; -// -// NSLog(@"%s", image_name); -//} -// -//__attribute__((constructor)) -//static void zzz(void) -//{ -// _dyld_register_func_for_add_image(func); -// -// //We are looking for: -// // DebugHierarchyFoundation.framework -// // libViewDebuggerSupport.dylib -//} -#endif - -+ (BOOL)_isMacSandboxed -{ - return NSProcessInfo.processInfo.environment[@"APP_SANDBOX_CONTAINER_ID"].length > 0; -} - -+ (NSURL*)_xcodeURLOrError:(out NSError* __strong * _Nullable)outError -{ - static NSURL* rv = nil; - static NSError* error = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - if(self._isMacSandboxed) - { - rv = [NSURL fileURLWithPath:@"/Applications/Xcode.app/Contents"]; - if([rv checkResourceIsReachableAndReturnError:NULL] == NO) - { - rv = [NSURL fileURLWithPath:@"/Applications/Xcode-beta.app/Contents"]; - if([rv checkResourceIsReachableAndReturnError:NULL] == NO) - { - rv = NULL; - error = [NSError errorWithDomain:@"LNViewHierarchyDumperDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: @"App is sandboxed and cannot find Xcode in\n/Applications/Xcode.app\nor\n/Applications/Xcode-beta.app"}]; - return; - } - } - } - else - { - NSTask* whichXcodeTask = [NSTask new]; - [whichXcodeTask setValue:[NSURL fileURLWithPath:@"/usr/bin/xcode-select"] forKey:@"executableURL"]; - whichXcodeTask.arguments = @[@"-p"]; - whichXcodeTask.environment = @{}; - - NSPipe* outPipe = [NSPipe pipe]; - NSMutableData* outData = [NSMutableData new]; - whichXcodeTask.standardOutput = outPipe; - outPipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle * _Nonnull fileHandle) { - [outData appendData:fileHandle.availableData]; - }; - - NSPipe* errPipe = [NSPipe pipe]; - NSMutableData* errData = [NSMutableData new]; - whichXcodeTask.standardError = errPipe; - errPipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle * _Nonnull fileHandle) { - [errData appendData:fileHandle.availableData]; - }; - - dispatch_semaphore_t waitForTermination = dispatch_semaphore_create(0); - - [whichXcodeTask setValue:^(NSTask* _Nonnull task) { - outPipe.fileHandleForReading.readabilityHandler = nil; - errPipe.fileHandleForReading.readabilityHandler = nil; - - dispatch_semaphore_signal(waitForTermination); - } forKey:@"terminationHandler"]; - - NSError* taskError; - [(id)whichXcodeTask launchAndReturnError:&taskError]; - - if(taskError == nil) - { - dispatch_semaphore_wait(waitForTermination, DISPATCH_TIME_FOREVER); - } - else - { - error = taskError; - return; - } - - if(whichXcodeTask.terminationStatus != 0) - { - NSString* errString = [[NSString alloc] initWithData:errData encoding:NSUTF8StringEncoding]; - error = [NSError errorWithDomain:@"LNViewHierarchyDumperDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: errString}]; - return; - } - - NSString* xcodePath = [[[NSString alloc] initWithData:outData encoding:NSUTF8StringEncoding] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; - - if(xcodePath.length == 0) - { - error = [NSError errorWithDomain:@"LNViewHierarchyDumperDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Unable to find the current active developer directory"}]; - - return; - } - - rv = [[[NSURL fileURLWithPath:xcodePath] URLByAppendingPathComponent:@".."] URLByStandardizingPath]; - } - }); - if(outError != NULL) - { - *outError = error; - } - return rv; -} - -+ (NSURL*)_debugHierarchyFoundationFrameworkURL_iOS:(NSURL*)runtimeURL -{ - NSString* rv = nil; - if(@available(iOS 17, *)) - { - rv = @"System/Library/PrivateFrameworks/DebugHierarchyFoundation.framework"; - } - else - { - rv = @"Developer/Library/PrivateFrameworks/DebugHierarchyFoundation.framework"; - } -#if TARGET_OS_SIMULATOR - return [runtimeURL URLByAppendingPathComponent:rv]; -#else - return [NSURL fileURLWithPath:[NSString stringWithFormat:@"/%@", rv]]; -#endif -} - -+ (NSURL*)_debugHierarchyFoundationFrameworkURL_macOS:(NSURL*)runtimeURL isCatalyst:(BOOL)isCatalyst -{ - return [runtimeURL URLByAppendingPathComponent:@"SharedFrameworks/DebugHierarchyFoundation.framework"]; -} - -+ (nullable NSURL*)_debugHierarchyFoundationFrameworkURL:(NSURL*)runtimeURL error:(out NSError** _Nullable)error -{ -#if TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST - if (@available(iOS 14.0, *)) - { - if(NSProcessInfo.processInfo.isiOSAppOnMac) - { - NSError* xcodeError = nil; - NSURL* xcodeURL = [self _xcodeURLOrError:&xcodeError]; - if(xcodeError) - { - if(error) { *error = xcodeError; } - return nil; - } - - return [self _debugHierarchyFoundationFrameworkURL_macOS:xcodeURL isCatalyst:YES]; - } - } - return [self _debugHierarchyFoundationFrameworkURL_iOS:runtimeURL]; -#else -#if TARGET_OS_MACCATALYST - BOOL isCatalyst = YES; -#else - BOOL isCatalyst = NO; -#endif - return [self _debugHierarchyFoundationFrameworkURL_macOS:runtimeURL isCatalyst:isCatalyst]; -#endif -} - -+ (NSURL*)_libViewDebuggerSupportURL_iOS:(NSURL*)runtimeURL -{ - NSString* rv = nil; - if(@available(iOS 17, *)) - { - rv = @"usr/lib/libViewDebuggerSupport.dylib"; - } - else - { - rv = @"Developer/Library/PrivateFrameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib"; - } -#if TARGET_OS_SIMULATOR - return [runtimeURL URLByAppendingPathComponent:rv]; -#else - return [NSURL fileURLWithPath:[NSString stringWithFormat:@"/%@", rv]]; -#endif -} - -+ (NSURL*)_libViewDebuggerSupportURL_macOS:(NSURL*)runtimeURL isCatalyst:(BOOL)isCatalyst -{ - NSString* libName = isCatalyst ? @"libViewDebuggerSupport_macCatalyst.dylib" : @"libViewDebuggerSupport.dylib"; - return [runtimeURL URLByAppendingPathComponent:[NSString stringWithFormat:@"Developer/Platforms/MacOSX.platform/Developer/Library/Debugger/%@", libName]]; -} - -+ (nullable NSURL*)_libViewDebuggerSupportURL:(NSURL*)runtimeURL error:(out NSError** _Nullable)error -{ -#if TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST - if (@available(iOS 14.0, *)) - { - if(NSProcessInfo.processInfo.isiOSAppOnMac) - { - NSError* xcodeError = nil; - NSURL* xcodeURL = [self _xcodeURLOrError:&xcodeError]; - if(xcodeError) - { - if(error) { *error = xcodeError; } - return nil; - } - - return [self _libViewDebuggerSupportURL_macOS:xcodeURL isCatalyst:YES]; - } - } - return [self _libViewDebuggerSupportURL_iOS:runtimeURL]; -#else -#if TARGET_OS_MACCATALYST - BOOL isCatalyst = YES; -#else - BOOL isCatalyst = NO; -#endif - return [self _libViewDebuggerSupportURL_macOS:runtimeURL isCatalyst:isCatalyst]; -#endif -} - - - (void)_loadDebugHierarchyFoundationFramework { static dispatch_once_t onceToken; @@ -355,31 +98,6 @@ - (void)_loadDebugHierarchyFoundationFramework _isFrameworkLoaded = NO; _loadError = [NSError errorWithDomain:@"LNViewHierarchyDumperDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Unable to load %@: %s", libViewDebuggerSupportURL.lastPathComponent, dlerror()]}]; } - -#if DEBUG -// static int cnt = 0; -// if(_isFrameworkLoaded == YES) -// { -// Method m1 = class_getInstanceMethod(NSClassFromString(@"DebugHierarchyTargetHub"), NSSelectorFromString(@"performRequest:error:")); -// NSData* (*orig)(id, SEL, id, NSError**) = (void*)method_getImplementation(m1); -// method_setImplementation(m1, imp_implementationWithBlock(^(id _self, id arg, NSError** error) { -// // NSLog(@"🤦‍♂️ %@", NSThread.callStackSymbols); -// NSLog(@"🤦‍♂️ %@ %@", object_getClass(arg), arg); -// -// NSData* rv = orig(_self, NSSelectorFromString(@"performRequestWithRequestInBase64:"), arg, error); -// if(error && *error) -// { -// NSLog(@"🤡 %@", *error); -// } -// -// NSLog(@"🤦‍♂️ %@ %@", object_getClass(rv), rv); -// -// cnt++; -// -// return rv; -// })); -// } -#endif #else _isFrameworkLoaded = NO; _loadError = [NSError errorWithDomain:@"LNViewHierarchyDumperDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: @"LNViewHierarchyDumper is only supported on simulators, Catalyst and macOS (with Xcode installed)"}]; @@ -387,18 +105,25 @@ - (void)_loadDebugHierarchyFoundationFramework }); } -#define GENERIC_ERROR_IF_NEEDED() if(error && !*error) { *error = [NSError errorWithDomain:@"LNViewHierarchyDumperDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Unknown error encountered; try restarting your simulator (this framework is as buggy as, or as bug-free as, Xcode's visual inspector. 🤷‍♂️)"}]; } -#define RETURN_IF_FALSE(cmd) if(NO == cmd) { GENERIC_ERROR_IF_NEEDED(); return NO; } -#define RETURN_IF_NIL(arg) if(arg == nil) { GENERIC_ERROR_IF_NEEDED(); return NO; } - -#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST || TARGET_OS_OSX -static NSArray* LNExtractPhase1ResponseObjects(NSDictionary* phase1Response) +- (nullable NSDictionary*)_executeRequestPhaseWithRequest:(NSDictionary*)request hub:(id /*DebugHierarchyTargetHub*/)sharedHub outputURL:(NSURL*)outputURL phaseCount:(NSInteger)count error:(NSError**)error { - NSArray* topLevelGroupsCALayers = phase1Response[@"topLevelGroups"][@"com.apple.QuartzCore.CALayer"][@"debugHierarchyObjects"]; + NSData* phaseJsonData = [NSJSONSerialization dataWithJSONObject:request options:0 error:error]; + RETURN_IF_NIL(phaseJsonData); + + id phaseRequest = [NSClassFromString(@"DebugHierarchyRequest") requestWithBase64Data:[phaseJsonData base64EncodedStringWithOptions:0] error:error]; + RETURN_IF_NIL(phaseRequest); + NSData* phaseResponseData = [sharedHub performRequest:phaseRequest error:error]; + RETURN_IF_NIL(phaseResponseData); + if(count >= 0) + { + BOOL didWrite = [phaseResponseData writeToURL:[outputURL URLByAppendingPathComponent:[NSString stringWithFormat:@"Response_%@", @(count)]] options:NSDataWritingAtomic error:error]; + RETURN_IF_FALSE(didWrite); + } - return [topLevelGroupsCALayers valueForKey:@"objectID"]; + phaseResponseData = phaseResponseData.isGzippedData ? phaseResponseData.gunzippedData : phaseResponseData; + + return [NSJSONSerialization JSONObjectWithData:phaseResponseData options:0 error:error]; } -#endif - (BOOL)dumpViewHierarchyToURL:(NSURL*)URL error:(out NSError** _Nullable)error { @@ -414,7 +139,7 @@ - (BOOL)dumpViewHierarchyToURL:(NSURL*)URL error:(out NSError** _Nullable)error if([URL.lastPathComponent hasSuffix:@".viewhierarchy"] == NO) { - URL = [URL URLByAppendingPathComponent:[NSString stringWithFormat:@"%@[%@].viewhierarchy", NSProcessInfo.processInfo.processName, @(NSProcessInfo.processInfo.processIdentifier)]]; + URL = [URL URLByAppendingPathComponent:[NSString stringWithFormat:@"%@[%@].viewhierarchy", NSProcessInfo.processInfo.processName, @(NSProcessInfo.processInfo.processIdentifier)] isDirectory:YES]; } if([NSFileManager.defaultManager fileExistsAtPath:URL.path]) @@ -424,532 +149,10 @@ - (BOOL)dumpViewHierarchyToURL:(NSURL*)URL error:(out NSError** _Nullable)error RETURN_IF_FALSE([NSFileManager.defaultManager createDirectoryAtURL:URL withIntermediateDirectories:YES attributes:nil error:error]); - NSURL* requestResponses = [URL URLByAppendingPathComponent:@"RequestResponses"]; - RETURN_IF_FALSE([NSFileManager.defaultManager createDirectoryAtURL:requestResponses withIntermediateDirectories:YES attributes:nil error:error]); - - NSDictionary* phase1Dictionary = @{ - @"DBGHierarchyRequestName": @"Initial request", - @"DBGHierarchyRequestInitiatorVersionKey": @3, - @"DBGHierarchyRequestPriority": @0, - @"DBGHierarchyObjectDiscovery": @1, - @"DBGHierarchyRequestActions": @[ - @{ - @"actionClass": @"DebugHierarchyResetAction" - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @0, - @"propertyNames": @[ - @"dbgFormattedDisplayName" - ], - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @0, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": NSNull.null, - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyAction", - @"propertyNamesAreExclusive": @0 - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @0, - @"propertyNames": @[ - @"anchorPoint", - @"anchorPointZ", - @"bounds", - @"contentsScale", - @"doubleSided", - @"frame", - @"geometryFlipped", - @"masksToBounds", - @"name", - @"opacity", - @"opaque", - @"position", - @"sublayerTransform", - @"transform", - @"zPosition", - @"dbg_ListID", - @"optimizationOpportunities" - ], - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @0, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": @[ - @"CALayer" - ], - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyAction", - @"propertyNamesAreExclusive": @0 - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @0, - @"propertyNames": @[ - @"constant", - @"firstAttribute", - @"firstItem", - @"identifier", - @"multiplier", - @"priority", - @"relation", - @"secondAttribute", - @"secondItem" - ], - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @0, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": @[ - @"NSLayoutConstraint" - ], - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyAction", - @"propertyNamesAreExclusive": @0 - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @0, - @"propertyNames": @[ - @"anchorPoint", - @"frame", - @"hidden", - @"position", - @"untransformedSize", - @"visualRepresentationOffset", - @"xScale", - @"yScale", - @"zPosition", - @"zRotation" - ], - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @0, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": @[ - @"SKNode" - ], - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyAction", - @"propertyNamesAreExclusive": @0 - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @0, - @"propertyNames": @[ - @"scaleMode", - @"size" - ], - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @0, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": @[ - @"SKScene" - ], - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyAction", - @"propertyNamesAreExclusive": @0 - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @1, - @"propertyNames": NSNull.null, - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @3, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": @[ - @"SKView" - ], - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyActionLegacyV1", - @"propertyNamesAreExclusive": @0 - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @1, - @"propertyNames": NSNull.null, - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @3, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": @[ - @"SKScene" - ], - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyActionLegacyV1", - @"propertyNamesAreExclusive": @0 - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @1, - @"propertyNames": NSNull.null, - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @3, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": @[ - @"SKNode" - ], - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyActionLegacyV1", - @"propertyNamesAreExclusive": @0 - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @0, - @"propertyNames": @[ - @"displayCornerRadius", - @"nativeBounds", - @"nativeDisplayBounds", - @"nativeScale", - @"scale", - @"dbgScreenShape", - @"dbgScreenShapeIsRectangular" - ], - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @0, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": @[ - @"UIScreen" - ], - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyAction", - @"propertyNamesAreExclusive": @0 - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @0, - @"propertyNames": @[ - @"keyWindow", - @"statusBarOrientation" - ], - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @0, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": @[ - @"UIApplication" - ], - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyAction", - @"propertyNamesAreExclusive": @0 - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @0, - @"propertyNames": @[ - @"title", - @"activationState" - ], - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @0, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": @[ - @"UIScene" - ], - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyAction", - @"propertyNamesAreExclusive": @0 - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @0, - @"propertyNames": @[ - @"title", - @"internal", - @"visible" - ], - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @0, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": @[ - @"UIWindow" - ], - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyAction", - @"propertyNamesAreExclusive": @0 - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @0, - @"propertyNames": @[ - @"title" - ], - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @0, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": @[ - @"UIViewController" - ], - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyAction", - @"propertyNamesAreExclusive": @0 - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @0, - @"propertyNames": @[ - @"alpha", - @"ambiguityStatusMask", - @"bounds", - @"dbgViewForFirstBaselineLayout", - @"dbgViewForLastBaselineLayout", - @"firstBaselineOffsetFromTop", - @"frame", - @"hasAmbiguousLayout", - @"hidden", - @"horizontalAffectingConstraints", - @"lastBaselineOffsetFromBottom", - @"layoutMargins", - @"verticalAffectingConstraints", - @"dbgSubviewHierarchy" - ], - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @0, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": @[ - @"UIView" - ], - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyAction", - @"propertyNamesAreExclusive": @0 - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @0, - @"propertyNames": @[ - @"layoutFrame", - @"identifier" - ], - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @0, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": @[ - @"UILayoutGuide" - ], - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyAction", - @"propertyNamesAreExclusive": @0 - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @0, - @"propertyNames": @[ - @"dbg_holdsSymbolImage" - ], - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @0, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": @[ - @"UIImageView" - ], - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyAction", - @"propertyNamesAreExclusive": @0 - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @0, - @"propertyNames": @[ - @"axis" - ], - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @0, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": @[ - @"UIStackView" - ], - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyAction", - @"propertyNamesAreExclusive": @0 - } - ], - @"DBGHierarchyRequestIdentifier": NSUUID.UUID.UUIDString, -// @"DBGHierarchyRequestTransportCompression": @YES - }; - NSData* phase1JsonData = [NSJSONSerialization dataWithJSONObject:phase1Dictionary options:0 error:error]; - RETURN_IF_NIL(phase1JsonData); - id sharedHub = [NSClassFromString(@"DebugHierarchyTargetHub") sharedHub]; - id phase1Request = [NSClassFromString(@"DebugHierarchyRequest") requestWithBase64Data:[phase1JsonData base64EncodedStringWithOptions:0] error:error]; - RETURN_IF_NIL(phase1Request); - NSData* phase1ResponseData = [sharedHub performRequest:phase1Request error:error]; - RETURN_IF_NIL(phase1ResponseData); - RETURN_IF_FALSE([phase1ResponseData writeToURL:[requestResponses URLByAppendingPathComponent:@"Response_0"] options:NSDataWritingAtomic error:error]); - - NSDictionary* phase1Response = [NSJSONSerialization JSONObjectWithData:phase1ResponseData options:0 error:error]; - RETURN_IF_NIL(phase1Response); - - NSArray* objects = LNExtractPhase1ResponseObjects(phase1Response); - RETURN_IF_NIL(objects); - - NSDictionary* phase2Dictionary = @{ - @"DBGHierarchyRequestName": @"Fetch encoded layers", - @"DBGHierarchyRequestInitiatorVersionKey": @3, - @"DBGHierarchyRequestPriority": @0, - @"DBGHierarchyObjectDiscovery": @2, - @"DBGHierarchyRequestActions": @[ - @{ - @"objectIdentifiers": objects, - @"options": @0, - @"propertyNames": @[ - @"encodedPresentationLayer" - ], - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @0, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": NSNull.null, - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyAction", - @"propertyNamesAreExclusive": @0 - } - ], - @"DBGHierarchyRequestIdentifier": NSUUID.UUID.UUIDString, - @"DBGHierarchyRequestTransportCompression": @YES - }; - - NSData* phase2JsonData = [NSJSONSerialization dataWithJSONObject:phase2Dictionary options:0 error:error]; - RETURN_IF_NIL(phase2JsonData); - - id phase2Request = [NSClassFromString(@"DebugHierarchyRequest") requestWithBase64Data:[phase2JsonData base64EncodedStringWithOptions:0] error:error]; - RETURN_IF_NIL(phase2Request); - NSData* phase2ResponseData = [sharedHub performRequest:phase2Request error:error]; - RETURN_IF_NIL(phase2ResponseData); - RETURN_IF_FALSE([phase2ResponseData writeToURL:[requestResponses URLByAppendingPathComponent:@"Response_1"] options:NSDataWritingAtomic error:error]); - -// NSDictionary* phase2Response = [NSJSONSerialization JSONObjectWithData:phase2ResponseData options:0 error:error]; -// RETURN_IF_NIL(phase2Response); - - NSDictionary* phase3Dictionary = @{ - @"DBGHierarchyRequestName": @"Fetch remaining lazy properties", - @"DBGHierarchyRequestInitiatorVersionKey": @3, - @"DBGHierarchyRequestPriority": @0, - @"DBGHierarchyObjectDiscovery": @2, - @"DBGHierarchyRequestActions": @[ - @{ - @"objectIdentifiers": NSNull.null, - @"options": @0, - @"propertyNames": @[ - @"encodedPresentationLayer", - @"encodedPresentationScene", - @"dbgFormattedDisplayName", - @"snapshotImage", - @"snapshotImageRenderedUsingDrawHierarchyInRect", - @"visualRepresentation", - @"dbgSubviewHierarchy" - ], - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @0, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": NSNull.null, - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyAction", - @"propertyNamesAreExclusive": @1 - }, - @{ - @"objectIdentifiers": NSNull.null, - @"options": @0, - @"propertyNames": @[ - @"encodedPresentationLayer", - @"encodedPresentationScene", - @"dbgFormattedDisplayName", - @"snapshotImage", - @"snapshotImageRenderedUsingDrawHierarchyInRect", - @"visualRepresentation", - @"dbgSubviewHierarchy" - ], - @"exactTypesAreExclusive": @0, - @"optionsComparisonStyle": @0, - @"exactTypes": NSNull.null, - @"typesAreExclusive": @0, - @"visibility": @15, - @"types": NSNull.null, - @"objectIdentifiersAreExclusive": @0, - @"actionClass": @"DebugHierarchyPropertyActionLegacyV1", - @"propertyNamesAreExclusive": @1 - } - ], - @"DBGHierarchyRequestIdentifier": NSUUID.UUID.UUIDString, - @"DBGHierarchyRequestTransportCompression": @YES - }; - NSData* phase3JsonData = [NSJSONSerialization dataWithJSONObject:phase3Dictionary options:0 error:error]; - RETURN_IF_NIL(phase3JsonData); - - id phase3Request = [NSClassFromString(@"DebugHierarchyRequest") requestWithBase64Data:[phase3JsonData base64EncodedStringWithOptions:0] error:error]; - RETURN_IF_NIL(phase3Request); - NSData* phase3ResponseData = [sharedHub performRequest:phase3Request error:error]; - RETURN_IF_NIL(phase3ResponseData); - RETURN_IF_FALSE([phase3ResponseData writeToURL:[requestResponses URLByAppendingPathComponent:@"Response_2"] options:NSDataWritingAtomic error:error]); - -// NSDictionary* phase3Response = [NSJSONSerialization JSONObjectWithData:phase3ResponseData options:0 error:error]; -// RETURN_IF_NIL(phase3Response); - - NSDictionary* cleanupPhaseDictionary = @{ - @"DBGHierarchyRequestName": @"Cleanup", - @"DBGHierarchyRequestInitiatorVersionKey": @3, - @"DBGHierarchyRequestPriority": @0, - @"DBGHierarchyObjectDiscovery": @0, - @"DBGHierarchyRequestActions": @[ - @{ - @"actionClass": @"DebugHierarchyResetAction" - } - ], - @"DBGHierarchyRequestIdentifier": NSUUID.UUID.UUIDString, - @"DBGHierarchyRequestTransportCompression": @NO - }; - NSData* cleanupPhaseJsonData = [NSJSONSerialization dataWithJSONObject:cleanupPhaseDictionary options:0 error:error]; - RETURN_IF_NIL(phase3JsonData); - - id cleanupPhaseRequest = [NSClassFromString(@"DebugHierarchyRequest") requestWithBase64Data:[cleanupPhaseJsonData base64EncodedStringWithOptions:0] error:error]; - RETURN_IF_NIL(cleanupPhaseRequest); - [sharedHub performRequest:cleanupPhaseRequest error:error]; - - NSDictionary* metadata = @{ - @"DocumentVersion": @"1", - @"RunnableDisplayName": [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleName"], - @"RunnablePID": @(NSProcessInfo.processInfo.processIdentifier) - }; - - NSData* data = [NSPropertyListSerialization dataWithPropertyList:metadata format:NSPropertyListXMLFormat_v1_0 options:0 error:error]; - if(data == nil) - { - return NO; - } + RETURN_IF_NIL(sharedHub); - RETURN_IF_FALSE([data writeToURL:[URL URLByAppendingPathComponent:@"metadata"] options:NSDataWritingAtomic error:error]); + RETURN_IF_FALSE([self _startPhasesWithHub:sharedHub outputURL:URL error:error]); return YES; #endif diff --git a/LNViewHierarchyDumper/LNViewHierarchyDumper/NSData+GZIP.h b/LNViewHierarchyDumper/LNViewHierarchyDumper/NSData+GZIP.h new file mode 100644 index 0000000..cbbf927 --- /dev/null +++ b/LNViewHierarchyDumper/LNViewHierarchyDumper/NSData+GZIP.h @@ -0,0 +1,19 @@ +// +// NSData+GZIP.h +// +// +// Created by Leo Natan on 02/08/2023. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSData (GZIP) + +@property (nonatomic, readonly, getter=isGzippedData) BOOL gzippedData; +@property (nonatomic, strong, readonly, nullable) NSData* gunzippedData; + +@end + +NS_ASSUME_NONNULL_END diff --git a/LNViewHierarchyDumper/LNViewHierarchyDumper/NSData+GZIP.m b/LNViewHierarchyDumper/LNViewHierarchyDumper/NSData+GZIP.m new file mode 100644 index 0000000..18ca354 --- /dev/null +++ b/LNViewHierarchyDumper/LNViewHierarchyDumper/NSData+GZIP.m @@ -0,0 +1,61 @@ +// +// NSData+GZIP.m +// +// +// Created by Leo Natan on 02/08/2023. +// + +#import "NSData+GZIP.h" +#import + +@implementation NSData (GZIP) + +- (BOOL)isGzippedData +{ + const UInt8 *bytes = (const UInt8 *)self.bytes; + return (self.length >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b); +} + +- (NSData *)gunzippedData +{ + if (self.length == 0 || ![self isGzippedData]) + { + return self; + } + + z_stream stream; + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.avail_in = (uint)self.length; + stream.next_in = (Bytef *)self.bytes; + stream.total_out = 0; + stream.avail_out = 0; + + NSMutableData *output = nil; + if (inflateInit2(&stream, 47) == Z_OK) + { + int status = Z_OK; + output = [NSMutableData dataWithCapacity:self.length * 2]; + while (status == Z_OK) + { + if (stream.total_out >= output.length) + { + output.length += self.length / 2; + } + stream.next_out = (uint8_t *)output.mutableBytes + stream.total_out; + stream.avail_out = (uInt)(output.length - stream.total_out); + status = inflate (&stream, Z_SYNC_FLUSH); + } + if (inflateEnd(&stream) == Z_OK) + { + if (status == Z_STREAM_END) + { + output.length = stream.total_out; + } + } + } + + return output; +} + +@end diff --git a/Package.swift b/Package.swift index bb6bec2..403137b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.5 import PackageDescription @@ -6,6 +6,7 @@ let package = Package( name: "LNViewHierarchyDumper", platforms: [ .iOS(.v13), + .macCatalyst(.v13), .macOS(.v11) ], products: [ diff --git a/README.md b/README.md index a2159a5..a8835ec 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,11 @@ A framework for programmatically dumping the view hierarchy of your app into an

-The framework supports targeting iOS, tvOS and watchOS simulators, hardware devices **with developer image mounted**, and macOS and Catalyst **with Xcode installed**. Under unsupported targets or environments, the frameworks fails silently. +The framework supports dumping the view hierarchy of apps running on iOS, tvOS and watchOS simulators, hardware devices **with developer image mounted**, and macOS and Catalyst **with Xcode installed**. Under unsupported targets or environments, the frameworks fails silently and returns an error. -**This framework uses Xcode's internal DebugHierarchyFoundation framework, and is not AppStore safe**, thus you should use with care, only linking against it in development/testing scenarios/builds. +**This framework uses Xcode's internal DebugHierarchyFoundation framework, and is not AppStore safe**, thus you should use with care, only linking against it in development/testing scenarios/builds. Since the framework requires developer tooling (developer image mounted on iOS hardware; Xcode on macOS), there would be little benefit from having this framework in production anyway. + +Deploying the framework conditionally is a complex topic, beyond the scope of this README. One strategy can be to link the dynamic library with UI testing project, and launch your app with the `DYLD_INSERT_LIBRARIES` environment variable, pointing to the LNViewHierarchyDumper framework. Using the framework is very easy: diff --git a/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/AppDelegate.swift b/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/AppDelegate.swift new file mode 100644 index 0000000..f2ae94b --- /dev/null +++ b/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/AppDelegate.swift @@ -0,0 +1,30 @@ +// +// AppDelegate.swift +// AppKitViewHierarchyDumpTester +// +// Created by Leo Natan on 02/08/2023. +// + +import Cocoa + +@main +class AppDelegate: NSObject, NSApplicationDelegate { + + + + + func applicationDidFinishLaunching(_ aNotification: Notification) { + // Insert code here to initialize your application + } + + func applicationWillTerminate(_ aNotification: Notification) { + // Insert code here to tear down your application + } + + func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } + + +} + diff --git a/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/AppKitViewHierarchyDumpTester.entitlements b/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/AppKitViewHierarchyDumpTester.entitlements new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/AppKitViewHierarchyDumpTester.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/Assets.xcassets/AccentColor.colorset/Contents.json b/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/Assets.xcassets/AppIcon.appiconset/Contents.json b/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..3f00db4 --- /dev/null +++ b/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/Assets.xcassets/Contents.json b/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/Base.lproj/Main.storyboard b/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/Base.lproj/Main.storyboard new file mode 100644 index 0000000..4df8180 --- /dev/null +++ b/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/Base.lproj/Main.storyboard @@ -0,0 +1,861 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Justo nec ultrices dui sapien eget mi proin. Integer malesuada nunc vel risus commodo viverra maecenas. Nulla aliquet enim tortor at. Ac auctor augue mauris augue neque gravida in fermentum. Eget lorem dolor sed viverra. Platea dictumst vestibulum rhoncus est pellentesque elit. Natoque penatibus et magnis dis parturient montes nascetur ridiculus. Pellentesque massa placerat duis ultricies lacus sed turpis. Vehicula ipsum a arcu cursus vitae congue mauris rhoncus. At volutpat diam ut venenatis. Sed elementum tempus egestas sed sed risus pretium quam vulputate. Pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat. A condimentum vitae sapien pellentesque habitant. Risus feugiat in ante metus dictum. Molestie ac feugiat sed lectus vestibulum. Tincidunt nunc pulvinar sapien et ligula ullamcorper. Erat pellentesque adipiscing commodo elit at imperdiet. + +Rhoncus aenean vel elit scelerisque mauris pellentesque pulvinar. Placerat duis ultricies lacus sed turpis tincidunt id. Tempus urna et pharetra pharetra massa massa ultricies mi quis. Libero nunc consequat interdum varius sit amet mattis vulputate enim. Aliquam ut porttitor leo a diam sollicitudin tempor id. Et leo duis ut diam quam nulla porttitor massa id. Fermentum odio eu feugiat pretium nibh ipsum consequat. Est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus. Luctus venenatis lectus magna fringilla. Sollicitudin tempor id eu nisl nunc mi ipsum faucibus vitae. Vulputate eu scelerisque felis imperdiet proin fermentum leo vel. + +Vestibulum rhoncus est pellentesque elit ullamcorper dignissim cras. Non arcu risus quis varius quam quisque id. Quisque egestas diam in arcu cursus euismod quis viverra. Amet dictum sit amet justo. Enim ut tellus elementum sagittis vitae et leo duis. Ac turpis egestas sed tempus urna et pharetra pharetra massa. Blandit massa enim nec dui nunc mattis enim. Ac turpis egestas sed tempus. In iaculis nunc sed augue. Ultricies lacus sed turpis tincidunt. Senectus et netus et malesuada fames. + +Augue lacus viverra vitae congue eu consequat ac felis donec. Sollicitudin tempor id eu nisl. Mus mauris vitae ultricies leo integer malesuada. Tellus orci ac auctor augue mauris augue. Quam adipiscing vitae proin sagittis. Egestas sed sed risus pretium quam vulputate dignissim. Sollicitudin ac orci phasellus egestas. Hac habitasse platea dictumst vestibulum rhoncus. Lacus vel facilisis volutpat est velit egestas. Vulputate enim nulla aliquet porttitor lacus luctus accumsan tortor. Consectetur adipiscing elit pellentesque habitant morbi. Consequat mauris nunc congue nisi vitae. Lacinia at quis risus sed vulputate odio. In ante metus dictum at tempor commodo ullamcorper a. Enim lobortis scelerisque fermentum dui faucibus in. In eu mi bibendum neque egestas. Pretium fusce id velit ut tortor pretium. Sed odio morbi quis commodo odio aenean sed adipiscing diam. Libero volutpat sed cras ornare arcu. + +Aliquam faucibus purus in massa tempor. Enim praesent elementum facilisis leo vel fringilla. Justo laoreet sit amet cursus sit. Enim lobortis scelerisque fermentum dui faucibus in. Malesuada bibendum arcu vitae elementum curabitur vitae nunc sed velit. Mollis aliquam ut porttitor leo a. Aliquam ut porttitor leo a diam sollicitudin tempor. In dictum non consectetur a erat nam at lectus urna. Mollis nunc sed id semper. Fames ac turpis egestas sed tempus urna et. Pellentesque adipiscing commodo elit at imperdiet dui accumsan sit. Pharetra sit amet aliquam id. Urna nec tincidunt praesent semper feugiat. Varius quam quisque id diam vel quam elementum. Gravida rutrum quisque non tellus orci ac auctor augue. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/ViewController.swift b/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/ViewController.swift new file mode 100644 index 0000000..57378c6 --- /dev/null +++ b/ViewHierarchyDumpTester/AppKitViewHierarchyDumpTester/ViewController.swift @@ -0,0 +1,82 @@ +// +// ViewController.swift +// AppKitViewHierarchyDumpTester +// +// Created by Leo Natan on 02/08/2023. +// + +import Cocoa +import WebKit +import LNViewHierarchyDumper + +class ViewController: NSViewController { + @IBOutlet var webView: WKWebView! + + override func viewDidLoad() { + super.viewDidLoad() + + webView.load(URLRequest(url: URL(string:"https://nytimes.com")!)) + } + + @IBAction func dumpHierarchy(_ sender: Any) { + let savePanel = NSSavePanel() + savePanel.nameFieldStringValue = "\(ProcessInfo.processInfo.processName)[\(ProcessInfo.processInfo.processIdentifier)].viewhierarchy" + savePanel.beginSheetModal(for: view.window!) { response in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + guard response == .OK, let url = savePanel.url else { + return + } + + do { + try LNViewHierarchyDumper.shared.dumpViewHierarchy(to: url) + } catch let e { + let alert = NSAlert(error: e) + alert.runModal() + } + } + } + +//#if targetEnvironment(macCatalyst) || !targetEnvironment(simulator) +// pendingTempUrl = URL(fileURLWithPath: "\(NSTemporaryDirectory())/\(ProcessInfo.processInfo.processName)[\(ProcessInfo.processInfo.processIdentifier)].viewhierarchy") +// do { +// try LNViewHierarchyDumper.shared.dumpViewHierarchy(to: pendingTempUrl!) +// let picker = UIDocumentPickerViewController(forExporting: [pendingTempUrl!], asCopy: true) +// picker.view.layoutIfNeeded() +// picker.delegate = self +//#if targetEnvironment(macCatalyst) +// self.present(picker, animated: true, completion: nil) +//#else +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { +// self.present(picker, animated: true, completion: nil) +// } +// RunLoop.current.run(until: .init(timeIntervalSinceNow: 0.3)) +//#endif +// } catch let e { +// let alert = UIAlertController(title: "View Hierarchy Dump Failed", message: e.localizedDescription, preferredStyle: .alert) +// if #available(iOS 16.0, *) { +// alert.severity = .critical +// } +// alert.addAction(.init(title: "OK", style: .default, handler: nil)) +// present(alert, animated: true, completion: nil) +// } +//#else +// let somePath = NSHomeDirectory() +// let userPath = somePath[somePath.startIndex.. + + + + + + + + + + + + + + + + + + + + + + + + + + + +