├── plugin.gif ├── Screenshot.png ├── TOCAssetCatalogBackground.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcshareddata │ └── xcschemes │ │ └── TOCAssetCatalogBackground.xcscheme └── project.pbxproj ├── TOCAssetCatalogBackground ├── TOCAssetCatalogBackground.h ├── TOCAssetCatalogBackgroundButtonTarget.h ├── Info.plist ├── TOCAssetCatalogBackgroundButtonTarget.m └── TOCAssetCatalogBackground.m ├── Readme.md ├── LICENSE.txt └── contrib ├── LICENSE_Aspects.txt ├── Aspects.h └── Aspects.m /plugin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toco/TOCAssetCatalogBackground/HEAD/plugin.gif -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toco/TOCAssetCatalogBackground/HEAD/Screenshot.png -------------------------------------------------------------------------------- /TOCAssetCatalogBackground.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TOCAssetCatalogBackground/TOCAssetCatalogBackground.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOCAssetCatalogBackground.h 3 | // TOCAssetCatalogBackground 4 | // 5 | // Created by Tobias Conradi on 17.05.15. 6 | // Copyright (c) 2015 Tobias Conradi. Licensed under the MIT license. 7 | // 8 | 9 | #import 10 | 11 | @interface TOCAssetCatalogBackground : NSObject 12 | 13 | + (instancetype)sharedPlugin; 14 | 15 | @property (nonatomic, strong, readonly) NSBundle* bundle; 16 | @end -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # TOCAssetCatalogBackground 2 | Xcode plugin to switch between white and dark background color in the asset catalog viewer. 3 | 4 | ![](plugin.gif) 5 | 6 | ## Installation 7 | 8 | Either 9 | 10 | - Clone and build the plugin yourself, it will be installed to the right location automatically by building it. 11 | 12 | or 13 | 14 | - Install it via [Alcatraz](http://alcatraz.io/) 15 | 16 | In any case, relaunch Xcode to load it. 17 | 18 | ## License 19 | MIT License, see LICENSE.txt. -------------------------------------------------------------------------------- /TOCAssetCatalogBackground/TOCAssetCatalogBackgroundButtonTarget.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOCAssetCatalogBackgroundButtonTarget.h 3 | // TOCAssetCatalogBackground 4 | // 5 | // Created by Tobias Conradi on 17.05.15. 6 | // Copyright (c) 2015 Tobias Conradi. Licensed under the MIT license. 7 | // 8 | 9 | #import 10 | typedef NS_ENUM(NSUInteger, TOCAssetCatalogBackgroundType) { 11 | TOCAssetCatalogBackgroundTypeLightBackground, 12 | TOCAssetCatalogBackgroundTypeDarkBackground 13 | }; 14 | extern NSString *const TOCAssetCatalogBackgroundColorChangedNotification; 15 | extern TOCAssetCatalogBackgroundType TOCAssetCatalogBackgroundCurrentBackgroundType; 16 | 17 | @interface TOCAssetCatalogBackgroundButtonTarget : NSObject 18 | @property (nonatomic, weak) NSScrollView *scrollView; 19 | - (void)updateBackgroundColor; 20 | - (void)segmentedControlChanged:(id)sender; 21 | @end -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tobias Conradi, github@tobias-conradi.de 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /contrib/LICENSE_Aspects.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Peter Steinberger, steipete@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /TOCAssetCatalogBackground/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | de.tobias-conradi.$(PRODUCT_NAME:rfc1034identifier) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | BNDL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | DVTPlugInCompatibilityUUIDs 26 | 27 | 7FDF5C7A-131F-4ABB-9EDC-8C5F8F0B8A90 28 | C4A681B0-4A26-480E-93EC-1218098B9AA0 29 | AABB7188-E14E-4433-AD3B-5CD791EAD9A3 30 | AD68E85B-441B-4301-B564-A45E4919A6AD 31 | A16FF353-8441-459E-A50C-B071F53F51B7 32 | 9F75337B-21B4-4ADC-B558-F9CADF7073A7 33 | E969541F-E6F9-4D25-8158-72DC3545A6C6 34 | 35 | LSMinimumSystemVersion 36 | $(MACOSX_DEPLOYMENT_TARGET) 37 | NSPrincipalClass 38 | TOCAssetCatalogBackground 39 | XC4Compatible 40 | 41 | XCPluginHasUI 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /TOCAssetCatalogBackground/TOCAssetCatalogBackgroundButtonTarget.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOCAssetCatalogBackgroundButtonTarget.m 3 | // TOCAssetCatalogBackground 4 | // 5 | // Created by Tobias Conradi on 17.05.15. 6 | // Copyright (c) 2015 Tobias Conradi. Licensed under the MIT license. 7 | // 8 | 9 | #import "TOCAssetCatalogBackgroundButtonTarget.h" 10 | 11 | NSString *const TOCAssetCatalogBackgroundColorChangedNotification = @"TOCAssetCatalogBackgroundColorChanged"; 12 | TOCAssetCatalogBackgroundType TOCAssetCatalogBackgroundCurrentBackgroundType = TOCAssetCatalogBackgroundTypeLightBackground; 13 | 14 | @implementation TOCAssetCatalogBackgroundButtonTarget 15 | 16 | - (instancetype)init 17 | { 18 | self = [super init]; 19 | if (self) { 20 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateBackgroundColor) name:TOCAssetCatalogBackgroundColorChangedNotification object:nil]; 21 | } 22 | return self; 23 | } 24 | 25 | - (void)dealloc 26 | { 27 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 28 | } 29 | 30 | - (void)updateBackgroundColor 31 | { 32 | NSColor *nextColor = nil; 33 | switch (TOCAssetCatalogBackgroundCurrentBackgroundType) { 34 | case TOCAssetCatalogBackgroundTypeDarkBackground: 35 | nextColor = [NSColor colorWithWhite:0.1 alpha:1.0]; 36 | break; 37 | case TOCAssetCatalogBackgroundTypeLightBackground: 38 | default: 39 | nextColor = [NSColor whiteColor]; 40 | } 41 | self.scrollView.backgroundColor = nextColor; 42 | } 43 | 44 | - (void)segmentedControlChanged:(id)sender 45 | { 46 | switch (TOCAssetCatalogBackgroundCurrentBackgroundType) { 47 | case TOCAssetCatalogBackgroundTypeDarkBackground: 48 | TOCAssetCatalogBackgroundCurrentBackgroundType = TOCAssetCatalogBackgroundTypeLightBackground; 49 | break; 50 | case TOCAssetCatalogBackgroundTypeLightBackground: 51 | default: 52 | TOCAssetCatalogBackgroundCurrentBackgroundType = TOCAssetCatalogBackgroundTypeDarkBackground; 53 | } 54 | [[NSNotificationCenter defaultCenter] postNotificationName:TOCAssetCatalogBackgroundColorChangedNotification object:self]; 55 | } 56 | 57 | @end 58 | -------------------------------------------------------------------------------- /TOCAssetCatalogBackground.xcodeproj/xcshareddata/xcschemes/TOCAssetCatalogBackground.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 58 | 60 | 61 | 62 | 68 | 69 | 70 | 71 | 72 | 73 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /contrib/Aspects.h: -------------------------------------------------------------------------------- 1 | // 2 | // Aspects.h 3 | // Aspects - A delightful, simple library for aspect oriented programming. 4 | // 5 | // Copyright (c) 2014 Peter Steinberger. Licensed under the MIT license. 6 | // 7 | 8 | #import 9 | 10 | typedef NS_OPTIONS(NSUInteger, AspectOptions) { 11 | AspectPositionAfter = 0, /// Called after the original implementation (default) 12 | AspectPositionInstead = 1, /// Will replace the original implementation. 13 | AspectPositionBefore = 2, /// Called before the original implementation. 14 | 15 | AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution. 16 | }; 17 | 18 | /// Opaque Aspect Token that allows to deregister the hook. 19 | @protocol TOCAssetCatalogBackground_AspectToken 20 | 21 | /// Deregisters an aspect. 22 | /// @return YES if deregistration is successful, otherwise NO. 23 | - (BOOL)remove; 24 | 25 | @end 26 | 27 | /// The TOCAssetCatalogBackground_AspectInfo protocol is the first parameter of our block syntax. 28 | @protocol TOCAssetCatalogBackground_AspectInfo 29 | 30 | /// The instance that is currently hooked. 31 | - (id)instance; 32 | 33 | /// The original invocation of the hooked method. 34 | - (NSInvocation *)originalInvocation; 35 | 36 | /// All method arguments, boxed. This is lazily evaluated. 37 | - (NSArray *)arguments; 38 | 39 | @end 40 | 41 | /** 42 | Aspects uses Objective-C message forwarding to hook into messages. This will create some overhead. Don't add aspects to methods that are called a lot. Aspects is meant for view/controller code that is not called a 1000 times per second. 43 | 44 | Adding aspects returns an opaque token which can be used to deregister again. All calls are thread safe. 45 | */ 46 | @interface NSObject (Aspects) 47 | 48 | /// Adds a block of code before/instead/after the current `selector` for a specific class. 49 | /// 50 | /// @param block Aspects replicates the type signature of the method being hooked. 51 | /// The first parameter will be `id`, followed by all parameters of the method. 52 | /// These parameters are optional and will be filled to match the block signature. 53 | /// You can even use an empty block, or one that simple gets `id`. 54 | /// 55 | /// @note Hooking static methods is not supported. 56 | /// @return A token which allows to later deregister the aspect. 57 | + (id)tocassetcatalogbackground_aspect_hookSelector:(SEL)selector 58 | withOptions:(AspectOptions)options 59 | usingBlock:(id)block 60 | error:(NSError **)error; 61 | 62 | /// Adds a block of code before/instead/after the current `selector` for a specific instance. 63 | - (id)tocassetcatalogbackground_aspect_hookSelector:(SEL)selector 64 | withOptions:(AspectOptions)options 65 | usingBlock:(id)block 66 | error:(NSError **)error; 67 | 68 | @end 69 | 70 | 71 | typedef NS_ENUM(NSUInteger, AspectErrorCode) { 72 | AspectErrorSelectorBlacklisted, /// Selectors like release, retain, autorelease are blacklisted. 73 | AspectErrorDoesNotRespondToSelector, /// Selector could not be found. 74 | AspectErrorSelectorDeallocPosition, /// When hooking dealloc, only AspectPositionBefore is allowed. 75 | AspectErrorSelectorAlreadyHookedInClassHierarchy, /// Statically hooking the same method in subclasses is not allowed. 76 | AspectErrorFailedToAllocateClassPair, /// The runtime failed creating a class pair. 77 | AspectErrorMissingBlockSignature, /// The block misses compile time signature info and can't be called. 78 | AspectErrorIncompatibleBlockSignature, /// The block signature does not match the method or is too large. 79 | 80 | AspectErrorRemoveObjectAlreadyDeallocated = 100 /// (for removing) The object hooked is already deallocated. 81 | }; 82 | 83 | extern NSString *const TOCAssetCatalogBackground_AspectErrorDomain; 84 | -------------------------------------------------------------------------------- /TOCAssetCatalogBackground/TOCAssetCatalogBackground.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOCAssetCatalogBackground.m 3 | // TOCAssetCatalogBackground 4 | // 5 | // Created by Tobias Conradi on 17.05.15. 6 | // Copyright (c) 2015 Tobias Conradi. Licensed under the MIT license. 7 | // 8 | 9 | #import 10 | #import "TOCAssetCatalogBackground.h" 11 | #import "TOCAssetCatalogBackgroundButtonTarget.h" 12 | #import "Aspects.h" 13 | 14 | static TOCAssetCatalogBackground *sharedPlugin; 15 | 16 | @interface NSObject (ShutUpWarnings) 17 | + (id)barButtonWithTitle:(id)arg1; 18 | - (id)effectiveTitleColor; 19 | - (void)refreshHighlightState; 20 | @end 21 | 22 | @interface TOCAssetCatalogBackground() 23 | 24 | @property (nonatomic, strong, readwrite) NSBundle *bundle; 25 | @property (nonatomic) BOOL hookedIBICAbstractCatalogDetailController; 26 | @property (nonatomic) BOOL hookIBICMultipartImageView; 27 | 28 | @end 29 | 30 | @implementation TOCAssetCatalogBackground 31 | 32 | + (void)pluginDidLoad:(NSBundle *)plugin 33 | { 34 | static dispatch_once_t onceToken; 35 | NSString *currentApplicationName = [[NSBundle mainBundle] infoDictionary][@"CFBundleName"]; 36 | if ([currentApplicationName isEqual:@"Xcode"]) { 37 | dispatch_once(&onceToken, ^{ 38 | sharedPlugin = [[self alloc] initWithBundle:plugin]; 39 | }); 40 | } 41 | } 42 | 43 | + (instancetype)sharedPlugin 44 | { 45 | return sharedPlugin; 46 | } 47 | 48 | - (id)initWithBundle:(NSBundle *)plugin 49 | { 50 | if (self = [super init]) { 51 | // reference to plugin's bundle, for resource access 52 | self.bundle = plugin; 53 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(hookClasses) name:NSBundleDidLoadNotification object:nil]; 54 | [self hookClasses]; 55 | 56 | } 57 | return self; 58 | } 59 | 60 | // since official plugins might not be loaded when we try to hook the classes, 61 | // we might need to wait for the bundles to load. 62 | // therefore hookClasses gets called every time a bundle is loaded, so we keep state 63 | // which class was hooked successfully. 64 | - (void)hookClasses { 65 | if (!self.hookedIBICAbstractCatalogDetailController) { 66 | self.hookedIBICAbstractCatalogDetailController = [self hookIBICAbstractCatalogDetailController]; 67 | } 68 | if (!self.hookIBICMultipartImageView) { 69 | self.hookIBICMultipartImageView = [self hookIBICMultipartImageView]; 70 | } 71 | 72 | BOOL allHooked = self.hookedIBICAbstractCatalogDetailController && self.hookIBICMultipartImageView; 73 | if (allHooked) { 74 | [[NSNotificationCenter defaultCenter] removeObserver:self name:NSBundleDidLoadNotification object:nil]; 75 | } 76 | } 77 | 78 | - (BOOL)hookIBICAbstractCatalogDetailController 79 | { 80 | 81 | id catalogControllerClass = NSClassFromString(@"IBICAbstractCatalogDetailController"); 82 | if (!catalogControllerClass) { 83 | return NO; 84 | } 85 | 86 | NSError *error; 87 | [catalogControllerClass tocassetcatalogbackground_aspect_hookSelector:@selector(viewDidLoad) 88 | withOptions:AspectPositionAfter 89 | usingBlock:^(id info) { 90 | [self abstractCatalogDetailControllerDidLoad:(NSViewController *)info.instance]; 91 | } 92 | error:&error]; 93 | if (error != nil) { 94 | NSLog(@"Failed to hook -[IBICAbstractCatalogDetailController viewDidLoad] with error: %@",error); 95 | } 96 | 97 | return YES; 98 | } 99 | 100 | - (void)abstractCatalogDetailControllerDidLoad:(NSViewController *)controller 101 | { 102 | NSSegmentedControl *barButton = [NSClassFromString(@"IBAccessorizedScrollViewButtonBar") barButtonWithTitle:@"Change Background Color"]; 103 | 104 | NSScrollView *scrollView = [controller valueForKey:@"scrollView"]; 105 | id buttonBar = [scrollView valueForKey:@"buttonBar"]; 106 | 107 | NSMutableArray *buttonsArray = [[buttonBar valueForKey:@"rightViews"] mutableCopy]; 108 | [buttonsArray insertObject:barButton atIndex:0]; 109 | [buttonBar setValue:[buttonsArray copy] forKey:@"rightViews"]; 110 | 111 | TOCAssetCatalogBackgroundButtonTarget *target = [TOCAssetCatalogBackgroundButtonTarget new]; 112 | target.scrollView = scrollView; 113 | barButton.target = target; 114 | barButton.action = @selector(segmentedControlChanged:); 115 | [target updateBackgroundColor]; 116 | 117 | const void *key = @selector(segmentedControlChanged:); // just need a key 118 | objc_setAssociatedObject(barButton,key, target, OBJC_ASSOCIATION_RETAIN); 119 | } 120 | 121 | - (BOOL)hookIBICMultipartImageView 122 | { 123 | id multiPartImageViewClass = NSClassFromString(@"IBICMultipartImageView"); 124 | if (!multiPartImageViewClass) { 125 | return NO; 126 | } 127 | NSError *error; 128 | 129 | void (^effectiveTitleColorBlock)(id) = 130 | ^(id info){ 131 | BOOL replaceColor = YES; 132 | id instance = [info instance]; 133 | NSInvocation *invocation = [info originalInvocation]; 134 | 135 | if (TOCAssetCatalogBackgroundCurrentBackgroundType != TOCAssetCatalogBackgroundTypeDarkBackground) { 136 | replaceColor = NO; 137 | } 138 | if ([[instance valueForKey:@"wholeSetShowsSelection"] boolValue]) { 139 | replaceColor = NO; 140 | } 141 | if (replaceColor) { 142 | __unsafe_unretained NSColor *color = [NSColor whiteColor]; 143 | [invocation setReturnValue:(void *)&color]; 144 | } else { 145 | [invocation invoke]; 146 | } 147 | }; 148 | 149 | [multiPartImageViewClass tocassetcatalogbackground_aspect_hookSelector:@selector(effectiveTitleColor) 150 | withOptions:AspectPositionInstead 151 | usingBlock:effectiveTitleColorBlock 152 | error:&error]; 153 | if (error != nil) { 154 | NSLog(@"Failed to hook -[IBICMultipartImageView effectiveTitleColor] with error: %@",error); 155 | error = nil; 156 | } 157 | 158 | 159 | void (^viewDidMoveToWindowBlock)(id) = 160 | ^(id info){ 161 | NSView *instance = [info instance]; 162 | if (instance.window) { 163 | [[NSNotificationCenter defaultCenter] addObserver:instance selector:@selector(refreshHighlightState) name:TOCAssetCatalogBackgroundColorChangedNotification object:nil]; 164 | } else { 165 | [[NSNotificationCenter defaultCenter] removeObserver:instance name:TOCAssetCatalogBackgroundColorChangedNotification object:nil]; 166 | } 167 | }; 168 | 169 | [multiPartImageViewClass tocassetcatalogbackground_aspect_hookSelector:@selector(viewDidMoveToWindow) 170 | withOptions:AspectPositionAfter 171 | usingBlock:viewDidMoveToWindowBlock 172 | error:&error]; 173 | if (error != nil) { 174 | NSLog(@"Failed to hook -[IBICMultipartImageView viewDidMoveToWindow] with error: %@",error); 175 | } 176 | return YES; 177 | } 178 | 179 | - (void)dealloc 180 | { 181 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 182 | } 183 | 184 | @end 185 | -------------------------------------------------------------------------------- /TOCAssetCatalogBackground.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | DD0386A01B08C3BD004925EA /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD03869F1B08C3BD004925EA /* AppKit.framework */; }; 11 | DD0386A21B08C3BD004925EA /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD0386A11B08C3BD004925EA /* Foundation.framework */; }; 12 | DD0386A71B08C3BD004925EA /* TOCAssetCatalogBackground.xcscheme in Resources */ = {isa = PBXBuildFile; fileRef = DD0386A61B08C3BD004925EA /* TOCAssetCatalogBackground.xcscheme */; }; 13 | DD0386AA1B08C3BD004925EA /* TOCAssetCatalogBackground.m in Sources */ = {isa = PBXBuildFile; fileRef = DD0386A91B08C3BD004925EA /* TOCAssetCatalogBackground.m */; }; 14 | DD27848E1B092FBB00FF617B /* TOCAssetCatalogBackgroundButtonTarget.m in Sources */ = {isa = PBXBuildFile; fileRef = DD27848D1B092FBB00FF617B /* TOCAssetCatalogBackgroundButtonTarget.m */; }; 15 | DD2784901B0930CC00FF617B /* LICENSE.txt in Resources */ = {isa = PBXBuildFile; fileRef = DD27848F1B0930CC00FF617B /* LICENSE.txt */; }; 16 | DD4C2D951B09233700170340 /* Aspects.m in Sources */ = {isa = PBXBuildFile; fileRef = DD4C2D931B09233700170340 /* Aspects.m */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | DD03869C1B08C3BD004925EA /* TOCAssetCatalogBackground.xcplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TOCAssetCatalogBackground.xcplugin; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | DD03869F1B08C3BD004925EA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; 22 | DD0386A11B08C3BD004925EA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; 23 | DD0386A51B08C3BD004925EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 24 | DD0386A61B08C3BD004925EA /* TOCAssetCatalogBackground.xcscheme */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = TOCAssetCatalogBackground.xcscheme; path = TOCAssetCatalogBackground.xcodeproj/xcshareddata/xcschemes/TOCAssetCatalogBackground.xcscheme; sourceTree = SOURCE_ROOT; }; 25 | DD0386A81B08C3BD004925EA /* TOCAssetCatalogBackground.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOCAssetCatalogBackground.h; sourceTree = ""; }; 26 | DD0386A91B08C3BD004925EA /* TOCAssetCatalogBackground.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOCAssetCatalogBackground.m; sourceTree = ""; }; 27 | DD27848C1B092FBB00FF617B /* TOCAssetCatalogBackgroundButtonTarget.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TOCAssetCatalogBackgroundButtonTarget.h; sourceTree = ""; }; 28 | DD27848D1B092FBB00FF617B /* TOCAssetCatalogBackgroundButtonTarget.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TOCAssetCatalogBackgroundButtonTarget.m; sourceTree = ""; }; 29 | DD27848F1B0930CC00FF617B /* LICENSE.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = LICENSE.txt; path = TOCAssetCatalogBackground.xcodeproj/../LICENSE.txt; sourceTree = SOURCE_ROOT; }; 30 | DD4C2D921B09233700170340 /* Aspects.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Aspects.h; sourceTree = ""; }; 31 | DD4C2D931B09233700170340 /* Aspects.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Aspects.m; sourceTree = ""; }; 32 | DD4C2D941B09233700170340 /* LICENSE_Aspects.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE_Aspects.txt; sourceTree = ""; }; 33 | /* End PBXFileReference section */ 34 | 35 | /* Begin PBXFrameworksBuildPhase section */ 36 | DD03869A1B08C3BD004925EA /* Frameworks */ = { 37 | isa = PBXFrameworksBuildPhase; 38 | buildActionMask = 2147483647; 39 | files = ( 40 | DD0386A01B08C3BD004925EA /* AppKit.framework in Frameworks */, 41 | DD0386A21B08C3BD004925EA /* Foundation.framework in Frameworks */, 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | DD0386931B08C3BD004925EA = { 49 | isa = PBXGroup; 50 | children = ( 51 | DD4C2D911B09233700170340 /* contrib */, 52 | DD0386A31B08C3BD004925EA /* TOCAssetCatalogBackground */, 53 | DD03869E1B08C3BD004925EA /* Frameworks */, 54 | DD03869D1B08C3BD004925EA /* Products */, 55 | ); 56 | sourceTree = ""; 57 | }; 58 | DD03869D1B08C3BD004925EA /* Products */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | DD03869C1B08C3BD004925EA /* TOCAssetCatalogBackground.xcplugin */, 62 | ); 63 | name = Products; 64 | sourceTree = ""; 65 | }; 66 | DD03869E1B08C3BD004925EA /* Frameworks */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | DD03869F1B08C3BD004925EA /* AppKit.framework */, 70 | DD0386A11B08C3BD004925EA /* Foundation.framework */, 71 | ); 72 | name = Frameworks; 73 | sourceTree = ""; 74 | }; 75 | DD0386A31B08C3BD004925EA /* TOCAssetCatalogBackground */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | DD0386A81B08C3BD004925EA /* TOCAssetCatalogBackground.h */, 79 | DD0386A91B08C3BD004925EA /* TOCAssetCatalogBackground.m */, 80 | DD27848C1B092FBB00FF617B /* TOCAssetCatalogBackgroundButtonTarget.h */, 81 | DD27848D1B092FBB00FF617B /* TOCAssetCatalogBackgroundButtonTarget.m */, 82 | DD0386A41B08C3BD004925EA /* Supporting Files */, 83 | ); 84 | path = TOCAssetCatalogBackground; 85 | sourceTree = ""; 86 | }; 87 | DD0386A41B08C3BD004925EA /* Supporting Files */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | DD27848F1B0930CC00FF617B /* LICENSE.txt */, 91 | DD0386A51B08C3BD004925EA /* Info.plist */, 92 | DD0386A61B08C3BD004925EA /* TOCAssetCatalogBackground.xcscheme */, 93 | ); 94 | name = "Supporting Files"; 95 | sourceTree = ""; 96 | }; 97 | DD4C2D911B09233700170340 /* contrib */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | DD4C2D921B09233700170340 /* Aspects.h */, 101 | DD4C2D931B09233700170340 /* Aspects.m */, 102 | DD4C2D941B09233700170340 /* LICENSE_Aspects.txt */, 103 | ); 104 | path = contrib; 105 | sourceTree = ""; 106 | }; 107 | /* End PBXGroup section */ 108 | 109 | /* Begin PBXNativeTarget section */ 110 | DD03869B1B08C3BD004925EA /* TOCAssetCatalogBackground */ = { 111 | isa = PBXNativeTarget; 112 | buildConfigurationList = DD0386AD1B08C3BD004925EA /* Build configuration list for PBXNativeTarget "TOCAssetCatalogBackground" */; 113 | buildPhases = ( 114 | DD0386981B08C3BD004925EA /* Sources */, 115 | DD0386991B08C3BD004925EA /* Resources */, 116 | DD03869A1B08C3BD004925EA /* Frameworks */, 117 | ); 118 | buildRules = ( 119 | ); 120 | dependencies = ( 121 | ); 122 | name = TOCAssetCatalogBackground; 123 | productName = TOCAssetCatalogBackground; 124 | productReference = DD03869C1B08C3BD004925EA /* TOCAssetCatalogBackground.xcplugin */; 125 | productType = "com.apple.product-type.bundle"; 126 | }; 127 | /* End PBXNativeTarget section */ 128 | 129 | /* Begin PBXProject section */ 130 | DD0386941B08C3BD004925EA /* Project object */ = { 131 | isa = PBXProject; 132 | attributes = { 133 | LastUpgradeCheck = 0630; 134 | ORGANIZATIONNAME = "Tobias Conradi"; 135 | TargetAttributes = { 136 | DD03869B1B08C3BD004925EA = { 137 | CreatedOnToolsVersion = 6.3.1; 138 | }; 139 | }; 140 | }; 141 | buildConfigurationList = DD0386971B08C3BD004925EA /* Build configuration list for PBXProject "TOCAssetCatalogBackground" */; 142 | compatibilityVersion = "Xcode 3.2"; 143 | developmentRegion = English; 144 | hasScannedForEncodings = 0; 145 | knownRegions = ( 146 | en, 147 | ); 148 | mainGroup = DD0386931B08C3BD004925EA; 149 | productRefGroup = DD03869D1B08C3BD004925EA /* Products */; 150 | projectDirPath = ""; 151 | projectRoot = ""; 152 | targets = ( 153 | DD03869B1B08C3BD004925EA /* TOCAssetCatalogBackground */, 154 | ); 155 | }; 156 | /* End PBXProject section */ 157 | 158 | /* Begin PBXResourcesBuildPhase section */ 159 | DD0386991B08C3BD004925EA /* Resources */ = { 160 | isa = PBXResourcesBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | DD2784901B0930CC00FF617B /* LICENSE.txt in Resources */, 164 | DD0386A71B08C3BD004925EA /* TOCAssetCatalogBackground.xcscheme in Resources */, 165 | ); 166 | runOnlyForDeploymentPostprocessing = 0; 167 | }; 168 | /* End PBXResourcesBuildPhase section */ 169 | 170 | /* Begin PBXSourcesBuildPhase section */ 171 | DD0386981B08C3BD004925EA /* Sources */ = { 172 | isa = PBXSourcesBuildPhase; 173 | buildActionMask = 2147483647; 174 | files = ( 175 | DD0386AA1B08C3BD004925EA /* TOCAssetCatalogBackground.m in Sources */, 176 | DD4C2D951B09233700170340 /* Aspects.m in Sources */, 177 | DD27848E1B092FBB00FF617B /* TOCAssetCatalogBackgroundButtonTarget.m in Sources */, 178 | ); 179 | runOnlyForDeploymentPostprocessing = 0; 180 | }; 181 | /* End PBXSourcesBuildPhase section */ 182 | 183 | /* Begin XCBuildConfiguration section */ 184 | DD0386AB1B08C3BD004925EA /* Debug */ = { 185 | isa = XCBuildConfiguration; 186 | buildSettings = { 187 | ALWAYS_SEARCH_USER_PATHS = NO; 188 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 189 | CLANG_CXX_LIBRARY = "libc++"; 190 | CLANG_ENABLE_MODULES = YES; 191 | CLANG_ENABLE_OBJC_ARC = YES; 192 | CLANG_WARN_BOOL_CONVERSION = YES; 193 | CLANG_WARN_CONSTANT_CONVERSION = YES; 194 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 195 | CLANG_WARN_EMPTY_BODY = YES; 196 | CLANG_WARN_ENUM_CONVERSION = YES; 197 | CLANG_WARN_INT_CONVERSION = YES; 198 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 199 | CLANG_WARN_UNREACHABLE_CODE = YES; 200 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 201 | COPY_PHASE_STRIP = NO; 202 | ENABLE_STRICT_OBJC_MSGSEND = YES; 203 | GCC_C_LANGUAGE_STANDARD = gnu99; 204 | GCC_DYNAMIC_NO_PIC = NO; 205 | GCC_NO_COMMON_BLOCKS = YES; 206 | GCC_OPTIMIZATION_LEVEL = 0; 207 | GCC_PREPROCESSOR_DEFINITIONS = ( 208 | "DEBUG=1", 209 | "$(inherited)", 210 | ); 211 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 212 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 213 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 214 | GCC_WARN_UNDECLARED_SELECTOR = YES; 215 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 216 | GCC_WARN_UNUSED_FUNCTION = YES; 217 | GCC_WARN_UNUSED_VARIABLE = YES; 218 | MTL_ENABLE_DEBUG_INFO = YES; 219 | ONLY_ACTIVE_ARCH = YES; 220 | }; 221 | name = Debug; 222 | }; 223 | DD0386AC1B08C3BD004925EA /* Release */ = { 224 | isa = XCBuildConfiguration; 225 | buildSettings = { 226 | ALWAYS_SEARCH_USER_PATHS = NO; 227 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 228 | CLANG_CXX_LIBRARY = "libc++"; 229 | CLANG_ENABLE_MODULES = YES; 230 | CLANG_ENABLE_OBJC_ARC = YES; 231 | CLANG_WARN_BOOL_CONVERSION = YES; 232 | CLANG_WARN_CONSTANT_CONVERSION = YES; 233 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 234 | CLANG_WARN_EMPTY_BODY = YES; 235 | CLANG_WARN_ENUM_CONVERSION = YES; 236 | CLANG_WARN_INT_CONVERSION = YES; 237 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 238 | CLANG_WARN_UNREACHABLE_CODE = YES; 239 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 240 | COPY_PHASE_STRIP = NO; 241 | ENABLE_NS_ASSERTIONS = NO; 242 | ENABLE_STRICT_OBJC_MSGSEND = YES; 243 | GCC_C_LANGUAGE_STANDARD = gnu99; 244 | GCC_NO_COMMON_BLOCKS = YES; 245 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 246 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 247 | GCC_WARN_UNDECLARED_SELECTOR = YES; 248 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 249 | GCC_WARN_UNUSED_FUNCTION = YES; 250 | GCC_WARN_UNUSED_VARIABLE = YES; 251 | MTL_ENABLE_DEBUG_INFO = NO; 252 | }; 253 | name = Release; 254 | }; 255 | DD0386AE1B08C3BD004925EA /* Debug */ = { 256 | isa = XCBuildConfiguration; 257 | buildSettings = { 258 | COMBINE_HIDPI_IMAGES = YES; 259 | DEPLOYMENT_LOCATION = YES; 260 | DSTROOT = "$(HOME)"; 261 | INFOPLIST_FILE = TOCAssetCatalogBackground/Info.plist; 262 | INSTALL_PATH = "/Library/Application Support/Developer/Shared/Xcode/Plug-ins"; 263 | MACOSX_DEPLOYMENT_TARGET = 10.10; 264 | PRODUCT_NAME = "$(TARGET_NAME)"; 265 | SDKROOT = macosx; 266 | WRAPPER_EXTENSION = xcplugin; 267 | }; 268 | name = Debug; 269 | }; 270 | DD0386AF1B08C3BD004925EA /* Release */ = { 271 | isa = XCBuildConfiguration; 272 | buildSettings = { 273 | COMBINE_HIDPI_IMAGES = YES; 274 | DEPLOYMENT_LOCATION = YES; 275 | DSTROOT = "$(HOME)"; 276 | INFOPLIST_FILE = TOCAssetCatalogBackground/Info.plist; 277 | INSTALL_PATH = "/Library/Application Support/Developer/Shared/Xcode/Plug-ins"; 278 | MACOSX_DEPLOYMENT_TARGET = 10.10; 279 | PRODUCT_NAME = "$(TARGET_NAME)"; 280 | SDKROOT = macosx; 281 | WRAPPER_EXTENSION = xcplugin; 282 | }; 283 | name = Release; 284 | }; 285 | /* End XCBuildConfiguration section */ 286 | 287 | /* Begin XCConfigurationList section */ 288 | DD0386971B08C3BD004925EA /* Build configuration list for PBXProject "TOCAssetCatalogBackground" */ = { 289 | isa = XCConfigurationList; 290 | buildConfigurations = ( 291 | DD0386AB1B08C3BD004925EA /* Debug */, 292 | DD0386AC1B08C3BD004925EA /* Release */, 293 | ); 294 | defaultConfigurationIsVisible = 0; 295 | defaultConfigurationName = Release; 296 | }; 297 | DD0386AD1B08C3BD004925EA /* Build configuration list for PBXNativeTarget "TOCAssetCatalogBackground" */ = { 298 | isa = XCConfigurationList; 299 | buildConfigurations = ( 300 | DD0386AE1B08C3BD004925EA /* Debug */, 301 | DD0386AF1B08C3BD004925EA /* Release */, 302 | ); 303 | defaultConfigurationIsVisible = 0; 304 | defaultConfigurationName = Release; 305 | }; 306 | /* End XCConfigurationList section */ 307 | }; 308 | rootObject = DD0386941B08C3BD004925EA /* Project object */; 309 | } 310 | -------------------------------------------------------------------------------- /contrib/Aspects.m: -------------------------------------------------------------------------------- 1 | // 2 | // Aspects.m 3 | // Aspects - A delightful, simple library for aspect oriented programming. 4 | // 5 | // Copyright (c) 2014 Peter Steinberger. Licensed under the MIT license. 6 | // 7 | 8 | #import "Aspects.h" 9 | #import 10 | #import 11 | #import 12 | 13 | #define AspectLog(...) 14 | //#define AspectLog(...) do { NSLog(__VA_ARGS__); }while(0) 15 | #define AspectLogError(...) do { NSLog(__VA_ARGS__); }while(0) 16 | 17 | // Block internals. 18 | typedef NS_OPTIONS(int, AspectBlockFlags) { 19 | AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25), 20 | AspectBlockFlagsHasSignature = (1 << 30) 21 | }; 22 | typedef struct _TOCAssetCatalogBackground_AspectBlock { 23 | __unused Class isa; 24 | AspectBlockFlags flags; 25 | __unused int reserved; 26 | void (__unused *invoke)(struct _TOCAssetCatalogBackground_AspectBlock *block, ...); 27 | struct { 28 | unsigned long int reserved; 29 | unsigned long int size; 30 | // requires AspectBlockFlagsHasCopyDisposeHelpers 31 | void (*copy)(void *dst, const void *src); 32 | void (*dispose)(const void *); 33 | // requires AspectBlockFlagsHasSignature 34 | const char *signature; 35 | const char *layout; 36 | } *descriptor; 37 | // imported variables 38 | } *TOCAssetCatalogBackground_AspectBlockRef; 39 | 40 | @interface TOCAssetCatalogBackground_AspectInfo : NSObject 41 | - (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation; 42 | @property (nonatomic, unsafe_unretained, readonly) id instance; 43 | @property (nonatomic, strong, readonly) NSArray *arguments; 44 | @property (nonatomic, strong, readonly) NSInvocation *originalInvocation; 45 | @end 46 | 47 | // Tracks a single aspect. 48 | @interface TOCAssetCatalogBackground_AspectIdentifier : NSObject 49 | + (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error; 50 | - (BOOL)invokeWithInfo:(id)info; 51 | @property (nonatomic, assign) SEL selector; 52 | @property (nonatomic, strong) id block; 53 | @property (nonatomic, strong) NSMethodSignature *blockSignature; 54 | @property (nonatomic, weak) id object; 55 | @property (nonatomic, assign) AspectOptions options; 56 | @end 57 | 58 | // Tracks all aspects for an object/class. 59 | @interface TOCAssetCatalogBackground_AspectsContainer : NSObject 60 | - (void)addAspect:(TOCAssetCatalogBackground_AspectIdentifier *)aspect withOptions:(AspectOptions)injectPosition; 61 | - (BOOL)removeAspect:(id)aspect; 62 | - (BOOL)hasAspects; 63 | @property (atomic, copy) NSArray *beforeAspects; 64 | @property (atomic, copy) NSArray *insteadAspects; 65 | @property (atomic, copy) NSArray *afterAspects; 66 | @end 67 | 68 | @interface TOCAssetCatalogBackground_AspectTracker : NSObject 69 | - (id)initWithTrackedClass:(Class)trackedClass parent:(TOCAssetCatalogBackground_AspectTracker *)parent; 70 | @property (nonatomic, strong) Class trackedClass; 71 | @property (nonatomic, strong) NSMutableSet *selectorNames; 72 | @property (nonatomic, weak) TOCAssetCatalogBackground_AspectTracker *parentEntry; 73 | @end 74 | 75 | @interface NSInvocation (Aspects) 76 | - (NSArray *)tocassetcatalogbackground_aspects_arguments; 77 | @end 78 | 79 | #define AspectPositionFilter 0x07 80 | 81 | #define AspectError(errorCode, errorDescription) do { \ 82 | AspectLogError(@"Aspects: %@", errorDescription); \ 83 | if (error) { *error = [NSError errorWithDomain:TOCAssetCatalogBackground_AspectErrorDomain code:errorCode userInfo:@{NSLocalizedDescriptionKey: errorDescription}]; }}while(0) 84 | 85 | NSString *const TOCAssetCatalogBackground_AspectErrorDomain = @"TOCAssetCatalogBackground_AspectErrorDomain"; 86 | static NSString *const AspectsSubclassSuffix = @"_TOCAssetCatalogBackground_Aspects_"; 87 | static NSString *const AspectsMessagePrefix = @"aspects_"; 88 | 89 | @implementation NSObject (Aspects) 90 | 91 | /////////////////////////////////////////////////////////////////////////////////////////// 92 | #pragma mark - Public Aspects API 93 | 94 | + (id)tocassetcatalogbackground_aspect_hookSelector:(SEL)selector 95 | withOptions:(AspectOptions)options 96 | usingBlock:(id)block 97 | error:(NSError **)error { 98 | return aspect_add((id)self, selector, options, block, error); 99 | } 100 | 101 | /// @return A token which allows to later deregister the aspect. 102 | - (id)tocassetcatalogbackground_aspect_hookSelector:(SEL)selector 103 | withOptions:(AspectOptions)options 104 | usingBlock:(id)block 105 | error:(NSError **)error { 106 | return aspect_add(self, selector, options, block, error); 107 | } 108 | 109 | /////////////////////////////////////////////////////////////////////////////////////////// 110 | #pragma mark - Private Helper 111 | 112 | static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) { 113 | NSCParameterAssert(self); 114 | NSCParameterAssert(selector); 115 | NSCParameterAssert(block); 116 | 117 | __block TOCAssetCatalogBackground_AspectIdentifier *identifier = nil; 118 | aspect_performLocked(^{ 119 | if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) { 120 | TOCAssetCatalogBackground_AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector); 121 | identifier = [TOCAssetCatalogBackground_AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error]; 122 | if (identifier) { 123 | [aspectContainer addAspect:identifier withOptions:options]; 124 | 125 | // Modify the class to allow message interception. 126 | aspect_prepareClassAndHookSelector(self, selector, error); 127 | } 128 | } 129 | }); 130 | return identifier; 131 | } 132 | 133 | static BOOL aspect_remove(TOCAssetCatalogBackground_AspectIdentifier *aspect, NSError **error) { 134 | NSCAssert([aspect isKindOfClass:TOCAssetCatalogBackground_AspectIdentifier.class], @"Must have correct type."); 135 | 136 | __block BOOL success = NO; 137 | aspect_performLocked(^{ 138 | id self = aspect.object; // strongify 139 | if (self) { 140 | TOCAssetCatalogBackground_AspectsContainer *aspectContainer = aspect_getContainerForObject(self, aspect.selector); 141 | success = [aspectContainer removeAspect:aspect]; 142 | 143 | aspect_cleanupHookedClassAndSelector(self, aspect.selector); 144 | // destroy token 145 | aspect.object = nil; 146 | aspect.block = nil; 147 | aspect.selector = NULL; 148 | }else { 149 | NSString *errrorDesc = [NSString stringWithFormat:@"Unable to deregister hook. Object already deallocated: %@", aspect]; 150 | AspectError(AspectErrorRemoveObjectAlreadyDeallocated, errrorDesc); 151 | } 152 | }); 153 | return success; 154 | } 155 | 156 | static void aspect_performLocked(dispatch_block_t block) { 157 | static OSSpinLock aspect_lock = OS_SPINLOCK_INIT; 158 | OSSpinLockLock(&aspect_lock); 159 | block(); 160 | OSSpinLockUnlock(&aspect_lock); 161 | } 162 | 163 | static SEL aspect_aliasForSelector(SEL selector) { 164 | NSCParameterAssert(selector); 165 | return NSSelectorFromString([AspectsMessagePrefix stringByAppendingFormat:@"_%@", NSStringFromSelector(selector)]); 166 | } 167 | 168 | static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) { 169 | TOCAssetCatalogBackground_AspectBlockRef layout = (__bridge void *)block; 170 | if (!(layout->flags & AspectBlockFlagsHasSignature)) { 171 | NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block]; 172 | AspectError(AspectErrorMissingBlockSignature, description); 173 | return nil; 174 | } 175 | void *desc = layout->descriptor; 176 | desc += 2 * sizeof(unsigned long int); 177 | if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) { 178 | desc += 2 * sizeof(void *); 179 | } 180 | if (!desc) { 181 | NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block]; 182 | AspectError(AspectErrorMissingBlockSignature, description); 183 | return nil; 184 | } 185 | const char *signature = (*(const char **)desc); 186 | return [NSMethodSignature signatureWithObjCTypes:signature]; 187 | } 188 | 189 | static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id object, SEL selector, NSError **error) { 190 | NSCParameterAssert(blockSignature); 191 | NSCParameterAssert(object); 192 | NSCParameterAssert(selector); 193 | 194 | BOOL signaturesMatch = YES; 195 | NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector]; 196 | if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) { 197 | signaturesMatch = NO; 198 | }else { 199 | if (blockSignature.numberOfArguments > 1) { 200 | const char *blockType = [blockSignature getArgumentTypeAtIndex:1]; 201 | if (blockType[0] != '@') { 202 | signaturesMatch = NO; 203 | } 204 | } 205 | // Argument 0 is self/block, argument 1 is SEL or id. We start comparing at argument 2. 206 | // The block can have less arguments than the method, that's ok. 207 | if (signaturesMatch) { 208 | for (NSUInteger idx = 2; idx < blockSignature.numberOfArguments; idx++) { 209 | const char *methodType = [methodSignature getArgumentTypeAtIndex:idx]; 210 | const char *blockType = [blockSignature getArgumentTypeAtIndex:idx]; 211 | // Only compare parameter, not the optional type data. 212 | if (!methodType || !blockType || methodType[0] != blockType[0]) { 213 | signaturesMatch = NO; break; 214 | } 215 | } 216 | } 217 | } 218 | 219 | if (!signaturesMatch) { 220 | NSString *description = [NSString stringWithFormat:@"Block signature %@ doesn't match %@.", blockSignature, methodSignature]; 221 | AspectError(AspectErrorIncompatibleBlockSignature, description); 222 | return NO; 223 | } 224 | return YES; 225 | } 226 | 227 | /////////////////////////////////////////////////////////////////////////////////////////// 228 | #pragma mark - Class + Selector Preparation 229 | 230 | static BOOL aspect_isMsgForwardIMP(IMP impl) { 231 | return impl == _objc_msgForward 232 | #if !defined(__arm64__) 233 | || impl == (IMP)_objc_msgForward_stret 234 | #endif 235 | ; 236 | } 237 | 238 | static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) { 239 | IMP msgForwardIMP = _objc_msgForward; 240 | #if !defined(__arm64__) 241 | // As an ugly internal runtime implementation detail in the 32bit runtime, we need to determine of the method we hook returns a struct or anything larger than id. 242 | // https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/LowLevelABI/000-Introduction/introduction.html 243 | // https://github.com/ReactiveCocoa/ReactiveCocoa/issues/783 244 | // http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042e/IHI0042E_aapcs.pdf (Section 5.4) 245 | Method method = class_getInstanceMethod(self.class, selector); 246 | const char *encoding = method_getTypeEncoding(method); 247 | BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B; 248 | if (methodReturnsStructValue) { 249 | @try { 250 | NSUInteger valueSize = 0; 251 | NSGetSizeAndAlignment(encoding, &valueSize, NULL); 252 | 253 | if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) { 254 | methodReturnsStructValue = NO; 255 | } 256 | } @catch (__unused NSException *e) {} 257 | } 258 | if (methodReturnsStructValue) { 259 | msgForwardIMP = (IMP)_objc_msgForward_stret; 260 | } 261 | #endif 262 | return msgForwardIMP; 263 | } 264 | 265 | static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) { 266 | NSCParameterAssert(selector); 267 | Class klass = aspect_hookClass(self, error); 268 | Method targetMethod = class_getInstanceMethod(klass, selector); 269 | IMP targetMethodIMP = method_getImplementation(targetMethod); 270 | if (!aspect_isMsgForwardIMP(targetMethodIMP)) { 271 | // Make a method alias for the existing method implementation, it not already copied. 272 | const char *typeEncoding = method_getTypeEncoding(targetMethod); 273 | SEL aliasSelector = aspect_aliasForSelector(selector); 274 | if (![klass instancesRespondToSelector:aliasSelector]) { 275 | __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding); 276 | NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass); 277 | } 278 | 279 | // We use forwardInvocation to hook in. 280 | class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding); 281 | AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector)); 282 | } 283 | } 284 | 285 | // Will undo the runtime changes made. 286 | static void aspect_cleanupHookedClassAndSelector(NSObject *self, SEL selector) { 287 | NSCParameterAssert(self); 288 | NSCParameterAssert(selector); 289 | 290 | Class klass = object_getClass(self); 291 | BOOL isMetaClass = class_isMetaClass(klass); 292 | if (isMetaClass) { 293 | klass = (Class)self; 294 | } 295 | 296 | // Check if the method is marked as forwarded and undo that. 297 | Method targetMethod = class_getInstanceMethod(klass, selector); 298 | IMP targetMethodIMP = method_getImplementation(targetMethod); 299 | if (aspect_isMsgForwardIMP(targetMethodIMP)) { 300 | // Restore the original method implementation. 301 | const char *typeEncoding = method_getTypeEncoding(targetMethod); 302 | SEL aliasSelector = aspect_aliasForSelector(selector); 303 | Method originalMethod = class_getInstanceMethod(klass, aliasSelector); 304 | IMP originalIMP = method_getImplementation(originalMethod); 305 | NSCAssert(originalMethod, @"Original implementation for %@ not found %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass); 306 | 307 | class_replaceMethod(klass, selector, originalIMP, typeEncoding); 308 | AspectLog(@"Aspects: Removed hook for -[%@ %@].", klass, NSStringFromSelector(selector)); 309 | } 310 | 311 | // Deregister global tracked selector 312 | aspect_deregisterTrackedSelector(self, selector); 313 | 314 | // Get the aspect container and check if there are any hooks remaining. Clean up if there are not. 315 | TOCAssetCatalogBackground_AspectsContainer *container = aspect_getContainerForObject(self, selector); 316 | if (!container.hasAspects) { 317 | // Destroy the container 318 | aspect_destroyContainerForObject(self, selector); 319 | 320 | // Figure out how the class was modified to undo the changes. 321 | NSString *className = NSStringFromClass(klass); 322 | if ([className hasSuffix:AspectsSubclassSuffix]) { 323 | Class originalClass = NSClassFromString([className stringByReplacingOccurrencesOfString:AspectsSubclassSuffix withString:@""]); 324 | NSCAssert(originalClass != nil, @"Original class must exist"); 325 | object_setClass(self, originalClass); 326 | AspectLog(@"Aspects: %@ has been restored.", NSStringFromClass(originalClass)); 327 | 328 | // We can only dispose the class pair if we can ensure that no instances exist using our subclass. 329 | // Since we don't globally track this, we can't ensure this - but there's also not much overhead in keeping it around. 330 | //objc_disposeClassPair(object.class); 331 | }else { 332 | // Class is most likely swizzled in place. Undo that. 333 | if (isMetaClass) { 334 | aspect_undoSwizzleClassInPlace((Class)self); 335 | }else if (self.class != klass) { 336 | aspect_undoSwizzleClassInPlace(klass); 337 | } 338 | } 339 | } 340 | } 341 | 342 | /////////////////////////////////////////////////////////////////////////////////////////// 343 | #pragma mark - Hook Class 344 | 345 | static Class aspect_hookClass(NSObject *self, NSError **error) { 346 | NSCParameterAssert(self); 347 | Class statedClass = self.class; 348 | Class baseClass = object_getClass(self); 349 | NSString *className = NSStringFromClass(baseClass); 350 | 351 | // Already subclassed 352 | if ([className hasSuffix:AspectsSubclassSuffix]) { 353 | return baseClass; 354 | 355 | // We swizzle a class object, not a single object. 356 | }else if (class_isMetaClass(baseClass)) { 357 | return aspect_swizzleClassInPlace((Class)self); 358 | // Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place. 359 | }else if (statedClass != baseClass) { 360 | return aspect_swizzleClassInPlace(baseClass); 361 | } 362 | 363 | // Default case. Create dynamic subclass. 364 | const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String; 365 | Class subclass = objc_getClass(subclassName); 366 | 367 | if (subclass == nil) { 368 | subclass = objc_allocateClassPair(baseClass, subclassName, 0); 369 | if (subclass == nil) { 370 | NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName]; 371 | AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc); 372 | return nil; 373 | } 374 | 375 | aspect_swizzleForwardInvocation(subclass); 376 | aspect_hookedGetClass(subclass, statedClass); 377 | aspect_hookedGetClass(object_getClass(subclass), statedClass); 378 | objc_registerClassPair(subclass); 379 | } 380 | 381 | object_setClass(self, subclass); 382 | return subclass; 383 | } 384 | 385 | static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:"; 386 | static void aspect_swizzleForwardInvocation(Class klass) { 387 | NSCParameterAssert(klass); 388 | // If there is no method, replace will act like class_addMethod. 389 | IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@"); 390 | if (originalImplementation) { 391 | class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@"); 392 | } 393 | AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass)); 394 | } 395 | 396 | static void aspect_undoSwizzleForwardInvocation(Class klass) { 397 | NSCParameterAssert(klass); 398 | Method originalMethod = class_getInstanceMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName)); 399 | Method objectMethod = class_getInstanceMethod(NSObject.class, @selector(forwardInvocation:)); 400 | // There is no class_removeMethod, so the best we can do is to retore the original implementation, or use a dummy. 401 | IMP originalImplementation = method_getImplementation(originalMethod ?: objectMethod); 402 | class_replaceMethod(klass, @selector(forwardInvocation:), originalImplementation, "v@:@"); 403 | 404 | AspectLog(@"Aspects: %@ has been restored.", NSStringFromClass(klass)); 405 | } 406 | 407 | static void aspect_hookedGetClass(Class class, Class statedClass) { 408 | NSCParameterAssert(class); 409 | NSCParameterAssert(statedClass); 410 | Method method = class_getInstanceMethod(class, @selector(class)); 411 | IMP newIMP = imp_implementationWithBlock(^(id self) { 412 | return statedClass; 413 | }); 414 | class_replaceMethod(class, @selector(class), newIMP, method_getTypeEncoding(method)); 415 | } 416 | 417 | /////////////////////////////////////////////////////////////////////////////////////////// 418 | #pragma mark - Swizzle Class In Place 419 | 420 | static void _aspect_modifySwizzledClasses(void (^block)(NSMutableSet *swizzledClasses)) { 421 | static NSMutableSet *swizzledClasses; 422 | static dispatch_once_t pred; 423 | dispatch_once(&pred, ^{ 424 | swizzledClasses = [NSMutableSet new]; 425 | }); 426 | @synchronized(swizzledClasses) { 427 | block(swizzledClasses); 428 | } 429 | } 430 | 431 | static Class aspect_swizzleClassInPlace(Class klass) { 432 | NSCParameterAssert(klass); 433 | NSString *className = NSStringFromClass(klass); 434 | 435 | _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) { 436 | if (![swizzledClasses containsObject:className]) { 437 | aspect_swizzleForwardInvocation(klass); 438 | [swizzledClasses addObject:className]; 439 | } 440 | }); 441 | return klass; 442 | } 443 | 444 | static void aspect_undoSwizzleClassInPlace(Class klass) { 445 | NSCParameterAssert(klass); 446 | NSString *className = NSStringFromClass(klass); 447 | 448 | _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) { 449 | if ([swizzledClasses containsObject:className]) { 450 | aspect_undoSwizzleForwardInvocation(klass); 451 | [swizzledClasses removeObject:className]; 452 | } 453 | }); 454 | } 455 | 456 | /////////////////////////////////////////////////////////////////////////////////////////// 457 | #pragma mark - Aspect Invoke Point 458 | 459 | // This is a macro so we get a cleaner stack trace. 460 | #define aspect_invoke(aspects, info) \ 461 | for (TOCAssetCatalogBackground_AspectIdentifier *aspect in aspects) {\ 462 | [aspect invokeWithInfo:info];\ 463 | if (aspect.options & AspectOptionAutomaticRemoval) { \ 464 | aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect]; \ 465 | } \ 466 | } 467 | 468 | // This is the swizzled forwardInvocation: method. 469 | static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) { 470 | NSCParameterAssert(self); 471 | NSCParameterAssert(invocation); 472 | SEL originalSelector = invocation.selector; 473 | SEL aliasSelector = aspect_aliasForSelector(invocation.selector); 474 | invocation.selector = aliasSelector; 475 | TOCAssetCatalogBackground_AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector); 476 | TOCAssetCatalogBackground_AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector); 477 | TOCAssetCatalogBackground_AspectInfo *info = [[TOCAssetCatalogBackground_AspectInfo alloc] initWithInstance:self invocation:invocation]; 478 | NSArray *aspectsToRemove = nil; 479 | 480 | // Before hooks. 481 | aspect_invoke(classContainer.beforeAspects, info); 482 | aspect_invoke(objectContainer.beforeAspects, info); 483 | 484 | // Instead hooks. 485 | BOOL respondsToAlias = YES; 486 | if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) { 487 | aspect_invoke(classContainer.insteadAspects, info); 488 | aspect_invoke(objectContainer.insteadAspects, info); 489 | }else { 490 | Class klass = object_getClass(invocation.target); 491 | do { 492 | if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) { 493 | [invocation invoke]; 494 | break; 495 | } 496 | }while (!respondsToAlias && (klass = class_getSuperclass(klass))); 497 | } 498 | 499 | // After hooks. 500 | aspect_invoke(classContainer.afterAspects, info); 501 | aspect_invoke(objectContainer.afterAspects, info); 502 | 503 | // If no hooks are installed, call original implementation (usually to throw an exception) 504 | if (!respondsToAlias) { 505 | invocation.selector = originalSelector; 506 | SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName); 507 | if ([self respondsToSelector:originalForwardInvocationSEL]) { 508 | ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation); 509 | }else { 510 | [self doesNotRecognizeSelector:invocation.selector]; 511 | } 512 | } 513 | 514 | // Remove any hooks that are queued for deregistration. 515 | [aspectsToRemove makeObjectsPerformSelector:@selector(remove)]; 516 | } 517 | #undef aspect_invoke 518 | 519 | /////////////////////////////////////////////////////////////////////////////////////////// 520 | #pragma mark - Aspect Container Management 521 | 522 | // Loads or creates the aspect container. 523 | static TOCAssetCatalogBackground_AspectsContainer *aspect_getContainerForObject(NSObject *self, SEL selector) { 524 | NSCParameterAssert(self); 525 | SEL aliasSelector = aspect_aliasForSelector(selector); 526 | TOCAssetCatalogBackground_AspectsContainer *aspectContainer = objc_getAssociatedObject(self, aliasSelector); 527 | if (!aspectContainer) { 528 | aspectContainer = [TOCAssetCatalogBackground_AspectsContainer new]; 529 | objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN); 530 | } 531 | return aspectContainer; 532 | } 533 | 534 | static TOCAssetCatalogBackground_AspectsContainer *aspect_getContainerForClass(Class klass, SEL selector) { 535 | NSCParameterAssert(klass); 536 | TOCAssetCatalogBackground_AspectsContainer *classContainer = nil; 537 | do { 538 | classContainer = objc_getAssociatedObject(klass, selector); 539 | if (classContainer.hasAspects) break; 540 | }while ((klass = class_getSuperclass(klass))); 541 | 542 | return classContainer; 543 | } 544 | 545 | static void aspect_destroyContainerForObject(id self, SEL selector) { 546 | NSCParameterAssert(self); 547 | SEL aliasSelector = aspect_aliasForSelector(selector); 548 | objc_setAssociatedObject(self, aliasSelector, nil, OBJC_ASSOCIATION_RETAIN); 549 | } 550 | 551 | /////////////////////////////////////////////////////////////////////////////////////////// 552 | #pragma mark - Selector Blacklist Checking 553 | 554 | static NSMutableDictionary *aspect_getSwizzledClassesDict() { 555 | static NSMutableDictionary *swizzledClassesDict; 556 | static dispatch_once_t pred; 557 | dispatch_once(&pred, ^{ 558 | swizzledClassesDict = [NSMutableDictionary new]; 559 | }); 560 | return swizzledClassesDict; 561 | } 562 | 563 | static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) { 564 | static NSSet *disallowedSelectorList; 565 | static dispatch_once_t pred; 566 | dispatch_once(&pred, ^{ 567 | disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil]; 568 | }); 569 | 570 | // Check against the blacklist. 571 | NSString *selectorName = NSStringFromSelector(selector); 572 | if ([disallowedSelectorList containsObject:selectorName]) { 573 | NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName]; 574 | AspectError(AspectErrorSelectorBlacklisted, errorDescription); 575 | return NO; 576 | } 577 | 578 | // Additional checks. 579 | AspectOptions position = options&AspectPositionFilter; 580 | if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) { 581 | NSString *errorDesc = @"AspectPositionBefore is the only valid position when hooking dealloc."; 582 | AspectError(AspectErrorSelectorDeallocPosition, errorDesc); 583 | return NO; 584 | } 585 | 586 | if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) { 587 | NSString *errorDesc = [NSString stringWithFormat:@"Unable to find selector -[%@ %@].", NSStringFromClass(self.class), selectorName]; 588 | AspectError(AspectErrorDoesNotRespondToSelector, errorDesc); 589 | return NO; 590 | } 591 | 592 | // Search for the current class and the class hierarchy IF we are modifying a class object 593 | if (class_isMetaClass(object_getClass(self))) { 594 | Class klass = [self class]; 595 | NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict(); 596 | Class currentClass = [self class]; 597 | do { 598 | TOCAssetCatalogBackground_AspectTracker *tracker = swizzledClassesDict[currentClass]; 599 | if ([tracker.selectorNames containsObject:selectorName]) { 600 | 601 | // Find the topmost class for the log. 602 | if (tracker.parentEntry) { 603 | TOCAssetCatalogBackground_AspectTracker *topmostEntry = tracker.parentEntry; 604 | while (topmostEntry.parentEntry) { 605 | topmostEntry = topmostEntry.parentEntry; 606 | } 607 | NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(topmostEntry.trackedClass)]; 608 | AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription); 609 | return NO; 610 | }else if (klass == currentClass) { 611 | // Already modified and topmost! 612 | return YES; 613 | } 614 | } 615 | }while ((currentClass = class_getSuperclass(currentClass))); 616 | 617 | // Add the selector as being modified. 618 | currentClass = klass; 619 | TOCAssetCatalogBackground_AspectTracker *parentTracker = nil; 620 | do { 621 | TOCAssetCatalogBackground_AspectTracker *tracker = swizzledClassesDict[currentClass]; 622 | if (!tracker) { 623 | tracker = [[TOCAssetCatalogBackground_AspectTracker alloc] initWithTrackedClass:currentClass parent:parentTracker]; 624 | swizzledClassesDict[(id)currentClass] = tracker; 625 | } 626 | [tracker.selectorNames addObject:selectorName]; 627 | // All superclasses get marked as having a subclass that is modified. 628 | parentTracker = tracker; 629 | }while ((currentClass = class_getSuperclass(currentClass))); 630 | } 631 | 632 | return YES; 633 | } 634 | 635 | static void aspect_deregisterTrackedSelector(id self, SEL selector) { 636 | if (!class_isMetaClass(object_getClass(self))) return; 637 | 638 | NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict(); 639 | NSString *selectorName = NSStringFromSelector(selector); 640 | Class currentClass = [self class]; 641 | do { 642 | TOCAssetCatalogBackground_AspectTracker *tracker = swizzledClassesDict[currentClass]; 643 | if (tracker) { 644 | [tracker.selectorNames removeObject:selectorName]; 645 | if (tracker.selectorNames.count == 0) { 646 | [swizzledClassesDict removeObjectForKey:tracker]; 647 | } 648 | } 649 | }while ((currentClass = class_getSuperclass(currentClass))); 650 | } 651 | 652 | @end 653 | 654 | @implementation TOCAssetCatalogBackground_AspectTracker 655 | 656 | - (id)initWithTrackedClass:(Class)trackedClass parent:(TOCAssetCatalogBackground_AspectTracker *)parent { 657 | if (self = [super init]) { 658 | _trackedClass = trackedClass; 659 | _parentEntry = parent; 660 | _selectorNames = [NSMutableSet new]; 661 | } 662 | return self; 663 | } 664 | - (NSString *)description { 665 | return [NSString stringWithFormat:@"<%@: %@, trackedClass: %@, selectorNames:%@, parent:%p>", self.class, self, NSStringFromClass(self.trackedClass), self.selectorNames, self.parentEntry]; 666 | } 667 | 668 | @end 669 | 670 | /////////////////////////////////////////////////////////////////////////////////////////// 671 | #pragma mark - NSInvocation (Aspects) 672 | 673 | @implementation NSInvocation (Aspects) 674 | 675 | // Thanks to the ReactiveCocoa team for providing a generic solution for this. 676 | - (id)tocassetcatalogbackground_aspect_argumentAtIndex:(NSUInteger)index { 677 | const char *argType = [self.methodSignature getArgumentTypeAtIndex:index]; 678 | // Skip const type qualifier. 679 | if (argType[0] == _C_CONST) argType++; 680 | 681 | #define WRAP_AND_RETURN(type) do { type val = 0; [self getArgument:&val atIndex:(NSInteger)index]; return @(val); } while (0) 682 | if (strcmp(argType, @encode(id)) == 0 || strcmp(argType, @encode(Class)) == 0) { 683 | __autoreleasing id returnObj; 684 | [self getArgument:&returnObj atIndex:(NSInteger)index]; 685 | return returnObj; 686 | } else if (strcmp(argType, @encode(SEL)) == 0) { 687 | SEL selector = 0; 688 | [self getArgument:&selector atIndex:(NSInteger)index]; 689 | return NSStringFromSelector(selector); 690 | } else if (strcmp(argType, @encode(Class)) == 0) { 691 | __autoreleasing Class theClass = Nil; 692 | [self getArgument:&theClass atIndex:(NSInteger)index]; 693 | return theClass; 694 | // Using this list will box the number with the appropriate constructor, instead of the generic NSValue. 695 | } else if (strcmp(argType, @encode(char)) == 0) { 696 | WRAP_AND_RETURN(char); 697 | } else if (strcmp(argType, @encode(int)) == 0) { 698 | WRAP_AND_RETURN(int); 699 | } else if (strcmp(argType, @encode(short)) == 0) { 700 | WRAP_AND_RETURN(short); 701 | } else if (strcmp(argType, @encode(long)) == 0) { 702 | WRAP_AND_RETURN(long); 703 | } else if (strcmp(argType, @encode(long long)) == 0) { 704 | WRAP_AND_RETURN(long long); 705 | } else if (strcmp(argType, @encode(unsigned char)) == 0) { 706 | WRAP_AND_RETURN(unsigned char); 707 | } else if (strcmp(argType, @encode(unsigned int)) == 0) { 708 | WRAP_AND_RETURN(unsigned int); 709 | } else if (strcmp(argType, @encode(unsigned short)) == 0) { 710 | WRAP_AND_RETURN(unsigned short); 711 | } else if (strcmp(argType, @encode(unsigned long)) == 0) { 712 | WRAP_AND_RETURN(unsigned long); 713 | } else if (strcmp(argType, @encode(unsigned long long)) == 0) { 714 | WRAP_AND_RETURN(unsigned long long); 715 | } else if (strcmp(argType, @encode(float)) == 0) { 716 | WRAP_AND_RETURN(float); 717 | } else if (strcmp(argType, @encode(double)) == 0) { 718 | WRAP_AND_RETURN(double); 719 | } else if (strcmp(argType, @encode(BOOL)) == 0) { 720 | WRAP_AND_RETURN(BOOL); 721 | } else if (strcmp(argType, @encode(bool)) == 0) { 722 | WRAP_AND_RETURN(BOOL); 723 | } else if (strcmp(argType, @encode(char *)) == 0) { 724 | WRAP_AND_RETURN(const char *); 725 | } else if (strcmp(argType, @encode(void (^)(void))) == 0) { 726 | __unsafe_unretained id block = nil; 727 | [self getArgument:&block atIndex:(NSInteger)index]; 728 | return [block copy]; 729 | } else { 730 | NSUInteger valueSize = 0; 731 | NSGetSizeAndAlignment(argType, &valueSize, NULL); 732 | 733 | unsigned char valueBytes[valueSize]; 734 | [self getArgument:valueBytes atIndex:(NSInteger)index]; 735 | 736 | return [NSValue valueWithBytes:valueBytes objCType:argType]; 737 | } 738 | return nil; 739 | #undef WRAP_AND_RETURN 740 | } 741 | 742 | - (NSArray *)tocassetcatalogbackground_aspects_arguments { 743 | NSMutableArray *argumentsArray = [NSMutableArray array]; 744 | for (NSUInteger idx = 2; idx < self.methodSignature.numberOfArguments; idx++) { 745 | [argumentsArray addObject:[self tocassetcatalogbackground_aspect_argumentAtIndex:idx] ?: NSNull.null]; 746 | } 747 | return [argumentsArray copy]; 748 | } 749 | 750 | @end 751 | 752 | /////////////////////////////////////////////////////////////////////////////////////////// 753 | #pragma mark - TOCAssetCatalogBackground_AspectIdentifier 754 | 755 | @implementation TOCAssetCatalogBackground_AspectIdentifier 756 | 757 | + (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error { 758 | NSCParameterAssert(block); 759 | NSCParameterAssert(selector); 760 | NSMethodSignature *blockSignature = aspect_blockMethodSignature(block, error); // TODO: check signature compatibility, etc. 761 | if (!aspect_isCompatibleBlockSignature(blockSignature, object, selector, error)) { 762 | return nil; 763 | } 764 | 765 | TOCAssetCatalogBackground_AspectIdentifier *identifier = nil; 766 | if (blockSignature) { 767 | identifier = [TOCAssetCatalogBackground_AspectIdentifier new]; 768 | identifier.selector = selector; 769 | identifier.block = block; 770 | identifier.blockSignature = blockSignature; 771 | identifier.options = options; 772 | identifier.object = object; // weak 773 | } 774 | return identifier; 775 | } 776 | 777 | - (BOOL)invokeWithInfo:(id)info { 778 | NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature]; 779 | NSInvocation *originalInvocation = info.originalInvocation; 780 | NSUInteger numberOfArguments = self.blockSignature.numberOfArguments; 781 | 782 | // Be extra paranoid. We already check that on hook registration. 783 | if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) { 784 | AspectLogError(@"Block has too many arguments. Not calling %@", info); 785 | return NO; 786 | } 787 | 788 | // The `self` of the block will be the TOCAssetCatalogBackground_AspectInfo. Optional. 789 | if (numberOfArguments > 1) { 790 | [blockInvocation setArgument:&info atIndex:1]; 791 | } 792 | 793 | void *argBuf = NULL; 794 | for (NSUInteger idx = 2; idx < numberOfArguments; idx++) { 795 | const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx]; 796 | NSUInteger argSize; 797 | NSGetSizeAndAlignment(type, &argSize, NULL); 798 | 799 | if (!(argBuf = reallocf(argBuf, argSize))) { 800 | AspectLogError(@"Failed to allocate memory for block invocation."); 801 | return NO; 802 | } 803 | 804 | [originalInvocation getArgument:argBuf atIndex:idx]; 805 | [blockInvocation setArgument:argBuf atIndex:idx]; 806 | } 807 | 808 | [blockInvocation invokeWithTarget:self.block]; 809 | 810 | if (argBuf != NULL) { 811 | free(argBuf); 812 | } 813 | return YES; 814 | } 815 | 816 | - (NSString *)description { 817 | return [NSString stringWithFormat:@"<%@: %p, SEL:%@ object:%@ options:%tu block:%@ (#%tu args)>", self.class, self, NSStringFromSelector(self.selector), self.object, self.options, self.block, self.blockSignature.numberOfArguments]; 818 | } 819 | 820 | - (BOOL)remove { 821 | return aspect_remove(self, NULL); 822 | } 823 | 824 | @end 825 | 826 | /////////////////////////////////////////////////////////////////////////////////////////// 827 | #pragma mark - TOCAssetCatalogBackground_AspectsContainer 828 | 829 | @implementation TOCAssetCatalogBackground_AspectsContainer 830 | 831 | - (BOOL)hasAspects { 832 | return self.beforeAspects.count > 0 || self.insteadAspects.count > 0 || self.afterAspects.count > 0; 833 | } 834 | 835 | - (void)addAspect:(TOCAssetCatalogBackground_AspectIdentifier *)aspect withOptions:(AspectOptions)options { 836 | NSParameterAssert(aspect); 837 | NSUInteger position = options&AspectPositionFilter; 838 | switch (position) { 839 | case AspectPositionBefore: self.beforeAspects = [(self.beforeAspects ?:@[]) arrayByAddingObject:aspect]; break; 840 | case AspectPositionInstead: self.insteadAspects = [(self.insteadAspects?:@[]) arrayByAddingObject:aspect]; break; 841 | case AspectPositionAfter: self.afterAspects = [(self.afterAspects ?:@[]) arrayByAddingObject:aspect]; break; 842 | } 843 | } 844 | 845 | - (BOOL)removeAspect:(id)aspect { 846 | for (NSString *aspectArrayName in @[NSStringFromSelector(@selector(beforeAspects)), 847 | NSStringFromSelector(@selector(insteadAspects)), 848 | NSStringFromSelector(@selector(afterAspects))]) { 849 | NSArray *array = [self valueForKey:aspectArrayName]; 850 | NSUInteger index = [array indexOfObjectIdenticalTo:aspect]; 851 | if (array && index != NSNotFound) { 852 | NSMutableArray *newArray = [NSMutableArray arrayWithArray:array]; 853 | [newArray removeObjectAtIndex:index]; 854 | [self setValue:newArray forKey:aspectArrayName]; 855 | return YES; 856 | } 857 | } 858 | return NO; 859 | } 860 | 861 | - (NSString *)description { 862 | return [NSString stringWithFormat:@"<%@: %p, before:%@, instead:%@, after:%@>", self.class, self, self.beforeAspects, self.insteadAspects, self.afterAspects]; 863 | } 864 | 865 | @end 866 | 867 | /////////////////////////////////////////////////////////////////////////////////////////// 868 | #pragma mark - TOCAssetCatalogBackground_AspectInfo 869 | 870 | @implementation TOCAssetCatalogBackground_AspectInfo 871 | 872 | @synthesize arguments = _arguments; 873 | 874 | - (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation { 875 | NSCParameterAssert(instance); 876 | NSCParameterAssert(invocation); 877 | if (self = [super init]) { 878 | _instance = instance; 879 | _originalInvocation = invocation; 880 | } 881 | return self; 882 | } 883 | 884 | - (NSArray *)arguments { 885 | // Lazily evaluate arguments, boxing is expensive. 886 | if (!_arguments) { 887 | _arguments = self.originalInvocation.tocassetcatalogbackground_aspects_arguments; 888 | } 889 | return _arguments; 890 | } 891 | 892 | @end 893 | --------------------------------------------------------------------------------