├── .gitignore ├── .gitmodules ├── PreciseCoverage.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcshareddata │ └── xcschemes │ │ └── PreciseCoverage.xcscheme └── xcuserdata │ └── zats.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── PreciseCoverage ├── Aspects.h ├── Aspects.m ├── Info.plist ├── PreciseCoverage.h └── PreciseCoverage.m ├── README.md └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | xcuserdata -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Vendor/Aspects"] 2 | path = Vendor/Aspects 3 | url = https://github.com/steipete/Aspects.git 4 | -------------------------------------------------------------------------------- /PreciseCoverage.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5C68696E1C0791AE0087A134 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C68696D1C0791AE0087A134 /* AppKit.framework */; }; 11 | 5C6869701C0791AE0087A134 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C68696F1C0791AE0087A134 /* Foundation.framework */; }; 12 | 5C6869741C0791AE0087A134 /* PreciseCoverage.xcscheme in Resources */ = {isa = PBXBuildFile; fileRef = 5C6869731C0791AE0087A134 /* PreciseCoverage.xcscheme */; }; 13 | 5C6869771C0791AE0087A134 /* PreciseCoverage.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6869761C0791AE0087A134 /* PreciseCoverage.m */; }; 14 | 5CA049831C07B3CC006E5A54 /* Aspects.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CA049821C07B3CC006E5A54 /* Aspects.m */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXFileReference section */ 18 | 5C68696A1C0791AE0087A134 /* PreciseCoverage.xcplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PreciseCoverage.xcplugin; sourceTree = BUILT_PRODUCTS_DIR; }; 19 | 5C68696D1C0791AE0087A134 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; 20 | 5C68696F1C0791AE0087A134 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; 21 | 5C6869731C0791AE0087A134 /* PreciseCoverage.xcscheme */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PreciseCoverage.xcscheme; path = PreciseCoverage.xcodeproj/xcshareddata/xcschemes/PreciseCoverage.xcscheme; sourceTree = SOURCE_ROOT; }; 22 | 5C6869751C0791AE0087A134 /* PreciseCoverage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PreciseCoverage.h; sourceTree = ""; }; 23 | 5C6869761C0791AE0087A134 /* PreciseCoverage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PreciseCoverage.m; sourceTree = ""; }; 24 | 5C6869781C0791AE0087A134 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 25 | 5CA049811C07B3CC006E5A54 /* Aspects.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Aspects.h; sourceTree = ""; }; 26 | 5CA049821C07B3CC006E5A54 /* Aspects.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Aspects.m; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | 5C6869681C0791AE0087A134 /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | 5C68696E1C0791AE0087A134 /* AppKit.framework in Frameworks */, 35 | 5C6869701C0791AE0087A134 /* Foundation.framework in Frameworks */, 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 5C6869611C0791AE0087A134 = { 43 | isa = PBXGroup; 44 | children = ( 45 | 5C6869711C0791AE0087A134 /* PreciseCoverage */, 46 | 5C68696C1C0791AE0087A134 /* Frameworks */, 47 | 5C68696B1C0791AE0087A134 /* Products */, 48 | ); 49 | sourceTree = ""; 50 | }; 51 | 5C68696B1C0791AE0087A134 /* Products */ = { 52 | isa = PBXGroup; 53 | children = ( 54 | 5C68696A1C0791AE0087A134 /* PreciseCoverage.xcplugin */, 55 | ); 56 | name = Products; 57 | sourceTree = ""; 58 | }; 59 | 5C68696C1C0791AE0087A134 /* Frameworks */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | 5C68696D1C0791AE0087A134 /* AppKit.framework */, 63 | 5C68696F1C0791AE0087A134 /* Foundation.framework */, 64 | ); 65 | name = Frameworks; 66 | sourceTree = ""; 67 | }; 68 | 5C6869711C0791AE0087A134 /* PreciseCoverage */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | 5CA049801C07B3C3006E5A54 /* Vendor */, 72 | 5C6869751C0791AE0087A134 /* PreciseCoverage.h */, 73 | 5C6869761C0791AE0087A134 /* PreciseCoverage.m */, 74 | 5C6869781C0791AE0087A134 /* Info.plist */, 75 | 5C6869721C0791AE0087A134 /* Supporting Files */, 76 | ); 77 | path = PreciseCoverage; 78 | sourceTree = ""; 79 | }; 80 | 5C6869721C0791AE0087A134 /* Supporting Files */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | 5C6869731C0791AE0087A134 /* PreciseCoverage.xcscheme */, 84 | ); 85 | name = "Supporting Files"; 86 | sourceTree = ""; 87 | }; 88 | 5CA049801C07B3C3006E5A54 /* Vendor */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | 5CA049811C07B3CC006E5A54 /* Aspects.h */, 92 | 5CA049821C07B3CC006E5A54 /* Aspects.m */, 93 | ); 94 | name = Vendor; 95 | sourceTree = ""; 96 | }; 97 | /* End PBXGroup section */ 98 | 99 | /* Begin PBXNativeTarget section */ 100 | 5C6869691C0791AE0087A134 /* PreciseCoverage */ = { 101 | isa = PBXNativeTarget; 102 | buildConfigurationList = 5C68697B1C0791AE0087A134 /* Build configuration list for PBXNativeTarget "PreciseCoverage" */; 103 | buildPhases = ( 104 | 5C6869661C0791AE0087A134 /* Sources */, 105 | 5C6869671C0791AE0087A134 /* Resources */, 106 | 5C6869681C0791AE0087A134 /* Frameworks */, 107 | ); 108 | buildRules = ( 109 | ); 110 | dependencies = ( 111 | ); 112 | name = PreciseCoverage; 113 | productName = PreciseCoverage; 114 | productReference = 5C68696A1C0791AE0087A134 /* PreciseCoverage.xcplugin */; 115 | productType = "com.apple.product-type.bundle"; 116 | }; 117 | /* End PBXNativeTarget section */ 118 | 119 | /* Begin PBXProject section */ 120 | 5C6869621C0791AE0087A134 /* Project object */ = { 121 | isa = PBXProject; 122 | attributes = { 123 | LastUpgradeCheck = 0710; 124 | ORGANIZATIONNAME = "Sash Zats"; 125 | TargetAttributes = { 126 | 5C6869691C0791AE0087A134 = { 127 | CreatedOnToolsVersion = 7.1.1; 128 | }; 129 | }; 130 | }; 131 | buildConfigurationList = 5C6869651C0791AE0087A134 /* Build configuration list for PBXProject "PreciseCoverage" */; 132 | compatibilityVersion = "Xcode 3.2"; 133 | developmentRegion = English; 134 | hasScannedForEncodings = 0; 135 | knownRegions = ( 136 | en, 137 | ); 138 | mainGroup = 5C6869611C0791AE0087A134; 139 | productRefGroup = 5C68696B1C0791AE0087A134 /* Products */; 140 | projectDirPath = ""; 141 | projectRoot = ""; 142 | targets = ( 143 | 5C6869691C0791AE0087A134 /* PreciseCoverage */, 144 | ); 145 | }; 146 | /* End PBXProject section */ 147 | 148 | /* Begin PBXResourcesBuildPhase section */ 149 | 5C6869671C0791AE0087A134 /* Resources */ = { 150 | isa = PBXResourcesBuildPhase; 151 | buildActionMask = 2147483647; 152 | files = ( 153 | 5C6869741C0791AE0087A134 /* PreciseCoverage.xcscheme in Resources */, 154 | ); 155 | runOnlyForDeploymentPostprocessing = 0; 156 | }; 157 | /* End PBXResourcesBuildPhase section */ 158 | 159 | /* Begin PBXSourcesBuildPhase section */ 160 | 5C6869661C0791AE0087A134 /* Sources */ = { 161 | isa = PBXSourcesBuildPhase; 162 | buildActionMask = 2147483647; 163 | files = ( 164 | 5C6869771C0791AE0087A134 /* PreciseCoverage.m in Sources */, 165 | 5CA049831C07B3CC006E5A54 /* Aspects.m in Sources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXSourcesBuildPhase section */ 170 | 171 | /* Begin XCBuildConfiguration section */ 172 | 5C6869791C0791AE0087A134 /* Debug */ = { 173 | isa = XCBuildConfiguration; 174 | buildSettings = { 175 | ALWAYS_SEARCH_USER_PATHS = NO; 176 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 177 | CLANG_CXX_LIBRARY = "libc++"; 178 | CLANG_ENABLE_MODULES = YES; 179 | CLANG_ENABLE_OBJC_ARC = YES; 180 | CLANG_WARN_BOOL_CONVERSION = YES; 181 | CLANG_WARN_CONSTANT_CONVERSION = YES; 182 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 183 | CLANG_WARN_EMPTY_BODY = YES; 184 | CLANG_WARN_ENUM_CONVERSION = YES; 185 | CLANG_WARN_INT_CONVERSION = YES; 186 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 187 | CLANG_WARN_UNREACHABLE_CODE = YES; 188 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 189 | COPY_PHASE_STRIP = NO; 190 | DEBUG_INFORMATION_FORMAT = dwarf; 191 | ENABLE_STRICT_OBJC_MSGSEND = YES; 192 | ENABLE_TESTABILITY = YES; 193 | GCC_C_LANGUAGE_STANDARD = gnu99; 194 | GCC_DYNAMIC_NO_PIC = NO; 195 | GCC_NO_COMMON_BLOCKS = YES; 196 | GCC_OPTIMIZATION_LEVEL = 0; 197 | GCC_PREPROCESSOR_DEFINITIONS = ( 198 | "DEBUG=1", 199 | "$(inherited)", 200 | ); 201 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 202 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 203 | GCC_WARN_UNDECLARED_SELECTOR = YES; 204 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 205 | GCC_WARN_UNUSED_FUNCTION = YES; 206 | GCC_WARN_UNUSED_VARIABLE = YES; 207 | MTL_ENABLE_DEBUG_INFO = YES; 208 | ONLY_ACTIVE_ARCH = YES; 209 | }; 210 | name = Debug; 211 | }; 212 | 5C68697A1C0791AE0087A134 /* Release */ = { 213 | isa = XCBuildConfiguration; 214 | buildSettings = { 215 | ALWAYS_SEARCH_USER_PATHS = NO; 216 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 217 | CLANG_CXX_LIBRARY = "libc++"; 218 | CLANG_ENABLE_MODULES = YES; 219 | CLANG_ENABLE_OBJC_ARC = YES; 220 | CLANG_WARN_BOOL_CONVERSION = YES; 221 | CLANG_WARN_CONSTANT_CONVERSION = YES; 222 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 223 | CLANG_WARN_EMPTY_BODY = YES; 224 | CLANG_WARN_ENUM_CONVERSION = YES; 225 | CLANG_WARN_INT_CONVERSION = YES; 226 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 227 | CLANG_WARN_UNREACHABLE_CODE = YES; 228 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 229 | COPY_PHASE_STRIP = NO; 230 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 231 | ENABLE_NS_ASSERTIONS = NO; 232 | ENABLE_STRICT_OBJC_MSGSEND = YES; 233 | GCC_C_LANGUAGE_STANDARD = gnu99; 234 | GCC_NO_COMMON_BLOCKS = YES; 235 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 236 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 237 | GCC_WARN_UNDECLARED_SELECTOR = YES; 238 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 239 | GCC_WARN_UNUSED_FUNCTION = YES; 240 | GCC_WARN_UNUSED_VARIABLE = YES; 241 | MTL_ENABLE_DEBUG_INFO = NO; 242 | }; 243 | name = Release; 244 | }; 245 | 5C68697C1C0791AE0087A134 /* Debug */ = { 246 | isa = XCBuildConfiguration; 247 | buildSettings = { 248 | COMBINE_HIDPI_IMAGES = YES; 249 | DEPLOYMENT_LOCATION = YES; 250 | DSTROOT = "$(HOME)"; 251 | INFOPLIST_FILE = PreciseCoverage/Info.plist; 252 | INSTALL_PATH = "/Library/Application Support/Developer/Shared/Xcode/Plug-ins"; 253 | MACOSX_DEPLOYMENT_TARGET = 10.11; 254 | PRODUCT_BUNDLE_IDENTIFIER = com.zats.PreciseCoverage; 255 | PRODUCT_NAME = "$(TARGET_NAME)"; 256 | WRAPPER_EXTENSION = xcplugin; 257 | }; 258 | name = Debug; 259 | }; 260 | 5C68697D1C0791AE0087A134 /* Release */ = { 261 | isa = XCBuildConfiguration; 262 | buildSettings = { 263 | COMBINE_HIDPI_IMAGES = YES; 264 | DEPLOYMENT_LOCATION = YES; 265 | DSTROOT = "$(HOME)"; 266 | INFOPLIST_FILE = PreciseCoverage/Info.plist; 267 | INSTALL_PATH = "/Library/Application Support/Developer/Shared/Xcode/Plug-ins"; 268 | MACOSX_DEPLOYMENT_TARGET = 10.11; 269 | PRODUCT_BUNDLE_IDENTIFIER = com.zats.PreciseCoverage; 270 | PRODUCT_NAME = "$(TARGET_NAME)"; 271 | WRAPPER_EXTENSION = xcplugin; 272 | }; 273 | name = Release; 274 | }; 275 | /* End XCBuildConfiguration section */ 276 | 277 | /* Begin XCConfigurationList section */ 278 | 5C6869651C0791AE0087A134 /* Build configuration list for PBXProject "PreciseCoverage" */ = { 279 | isa = XCConfigurationList; 280 | buildConfigurations = ( 281 | 5C6869791C0791AE0087A134 /* Debug */, 282 | 5C68697A1C0791AE0087A134 /* Release */, 283 | ); 284 | defaultConfigurationIsVisible = 0; 285 | defaultConfigurationName = Release; 286 | }; 287 | 5C68697B1C0791AE0087A134 /* Build configuration list for PBXNativeTarget "PreciseCoverage" */ = { 288 | isa = XCConfigurationList; 289 | buildConfigurations = ( 290 | 5C68697C1C0791AE0087A134 /* Debug */, 291 | 5C68697D1C0791AE0087A134 /* Release */, 292 | ); 293 | defaultConfigurationIsVisible = 0; 294 | defaultConfigurationName = Release; 295 | }; 296 | /* End XCConfigurationList section */ 297 | }; 298 | rootObject = 5C6869621C0791AE0087A134 /* Project object */; 299 | } 300 | -------------------------------------------------------------------------------- /PreciseCoverage.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PreciseCoverage.xcodeproj/xcshareddata/xcschemes/PreciseCoverage.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 | -------------------------------------------------------------------------------- /PreciseCoverage.xcodeproj/xcuserdata/zats.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SuppressBuildableAutocreation 6 | 7 | 5C6869691C0791AE0087A134 8 | 9 | primary 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /PreciseCoverage/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 AspectToken 20 | 21 | /// Deregisters an aspect. 22 | /// @return YES if deregistration is successful, otherwise NO. 23 | - (BOOL)remove; 24 | 25 | @end 26 | 27 | /// The AspectInfo protocol is the first parameter of our block syntax. 28 | @protocol 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)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)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 AspectErrorDomain; 84 | -------------------------------------------------------------------------------- /PreciseCoverage/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 _AspectBlock { 23 | __unused Class isa; 24 | AspectBlockFlags flags; 25 | __unused int reserved; 26 | void (__unused *invoke)(struct _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 | } *AspectBlockRef; 39 | 40 | @interface 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 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 AspectsContainer : NSObject 60 | - (void)addAspect:(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 AspectTracker : NSObject 69 | - (id)initWithTrackedClass:(Class)trackedClass; 70 | @property (nonatomic, strong) Class trackedClass; 71 | @property (nonatomic, readonly) NSString *trackedClassName; 72 | @property (nonatomic, strong) NSMutableSet *selectorNames; 73 | @property (nonatomic, strong) NSMutableDictionary *selectorNamesToSubclassTrackers; 74 | - (void)addSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName; 75 | - (void)removeSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName; 76 | - (BOOL)subclassHasHookedSelectorName:(NSString *)selectorName; 77 | - (NSSet *)subclassTrackersHookingSelectorName:(NSString *)selectorName; 78 | @end 79 | 80 | @interface NSInvocation (Aspects) 81 | - (NSArray *)aspects_arguments; 82 | @end 83 | 84 | #define AspectPositionFilter 0x07 85 | 86 | #define AspectError(errorCode, errorDescription) do { \ 87 | AspectLogError(@"Aspects: %@", errorDescription); \ 88 | if (error) { *error = [NSError errorWithDomain:AspectErrorDomain code:errorCode userInfo:@{NSLocalizedDescriptionKey: errorDescription}]; }}while(0) 89 | 90 | NSString *const AspectErrorDomain = @"AspectErrorDomain"; 91 | static NSString *const AspectsSubclassSuffix = @"_Aspects_"; 92 | static NSString *const AspectsMessagePrefix = @"aspects_"; 93 | 94 | @implementation NSObject (Aspects) 95 | 96 | /////////////////////////////////////////////////////////////////////////////////////////// 97 | #pragma mark - Public Aspects API 98 | 99 | + (id)aspect_hookSelector:(SEL)selector 100 | withOptions:(AspectOptions)options 101 | usingBlock:(id)block 102 | error:(NSError **)error { 103 | return aspect_add((id)self, selector, options, block, error); 104 | } 105 | 106 | /// @return A token which allows to later deregister the aspect. 107 | - (id)aspect_hookSelector:(SEL)selector 108 | withOptions:(AspectOptions)options 109 | usingBlock:(id)block 110 | error:(NSError **)error { 111 | return aspect_add(self, selector, options, block, error); 112 | } 113 | 114 | /////////////////////////////////////////////////////////////////////////////////////////// 115 | #pragma mark - Private Helper 116 | 117 | static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) { 118 | NSCParameterAssert(self); 119 | NSCParameterAssert(selector); 120 | NSCParameterAssert(block); 121 | 122 | __block AspectIdentifier *identifier = nil; 123 | aspect_performLocked(^{ 124 | if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) { 125 | AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector); 126 | identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error]; 127 | if (identifier) { 128 | [aspectContainer addAspect:identifier withOptions:options]; 129 | 130 | // Modify the class to allow message interception. 131 | aspect_prepareClassAndHookSelector(self, selector, error); 132 | } 133 | } 134 | }); 135 | return identifier; 136 | } 137 | 138 | static BOOL aspect_remove(AspectIdentifier *aspect, NSError **error) { 139 | NSCAssert([aspect isKindOfClass:AspectIdentifier.class], @"Must have correct type."); 140 | 141 | __block BOOL success = NO; 142 | aspect_performLocked(^{ 143 | id self = aspect.object; // strongify 144 | if (self) { 145 | AspectsContainer *aspectContainer = aspect_getContainerForObject(self, aspect.selector); 146 | success = [aspectContainer removeAspect:aspect]; 147 | 148 | aspect_cleanupHookedClassAndSelector(self, aspect.selector); 149 | // destroy token 150 | aspect.object = nil; 151 | aspect.block = nil; 152 | aspect.selector = NULL; 153 | }else { 154 | NSString *errrorDesc = [NSString stringWithFormat:@"Unable to deregister hook. Object already deallocated: %@", aspect]; 155 | AspectError(AspectErrorRemoveObjectAlreadyDeallocated, errrorDesc); 156 | } 157 | }); 158 | return success; 159 | } 160 | 161 | static void aspect_performLocked(dispatch_block_t block) { 162 | static OSSpinLock aspect_lock = OS_SPINLOCK_INIT; 163 | OSSpinLockLock(&aspect_lock); 164 | block(); 165 | OSSpinLockUnlock(&aspect_lock); 166 | } 167 | 168 | static SEL aspect_aliasForSelector(SEL selector) { 169 | NSCParameterAssert(selector); 170 | return NSSelectorFromString([AspectsMessagePrefix stringByAppendingFormat:@"_%@", NSStringFromSelector(selector)]); 171 | } 172 | 173 | static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) { 174 | AspectBlockRef layout = (__bridge void *)block; 175 | if (!(layout->flags & AspectBlockFlagsHasSignature)) { 176 | NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block]; 177 | AspectError(AspectErrorMissingBlockSignature, description); 178 | return nil; 179 | } 180 | void *desc = layout->descriptor; 181 | desc += 2 * sizeof(unsigned long int); 182 | if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) { 183 | desc += 2 * sizeof(void *); 184 | } 185 | if (!desc) { 186 | NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block]; 187 | AspectError(AspectErrorMissingBlockSignature, description); 188 | return nil; 189 | } 190 | const char *signature = (*(const char **)desc); 191 | return [NSMethodSignature signatureWithObjCTypes:signature]; 192 | } 193 | 194 | static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id object, SEL selector, NSError **error) { 195 | NSCParameterAssert(blockSignature); 196 | NSCParameterAssert(object); 197 | NSCParameterAssert(selector); 198 | 199 | BOOL signaturesMatch = YES; 200 | NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector]; 201 | if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) { 202 | signaturesMatch = NO; 203 | }else { 204 | if (blockSignature.numberOfArguments > 1) { 205 | const char *blockType = [blockSignature getArgumentTypeAtIndex:1]; 206 | if (blockType[0] != '@') { 207 | signaturesMatch = NO; 208 | } 209 | } 210 | // Argument 0 is self/block, argument 1 is SEL or id. We start comparing at argument 2. 211 | // The block can have less arguments than the method, that's ok. 212 | if (signaturesMatch) { 213 | for (NSUInteger idx = 2; idx < blockSignature.numberOfArguments; idx++) { 214 | const char *methodType = [methodSignature getArgumentTypeAtIndex:idx]; 215 | const char *blockType = [blockSignature getArgumentTypeAtIndex:idx]; 216 | // Only compare parameter, not the optional type data. 217 | if (!methodType || !blockType || methodType[0] != blockType[0]) { 218 | signaturesMatch = NO; break; 219 | } 220 | } 221 | } 222 | } 223 | 224 | if (!signaturesMatch) { 225 | NSString *description = [NSString stringWithFormat:@"Block signature %@ doesn't match %@.", blockSignature, methodSignature]; 226 | AspectError(AspectErrorIncompatibleBlockSignature, description); 227 | return NO; 228 | } 229 | return YES; 230 | } 231 | 232 | /////////////////////////////////////////////////////////////////////////////////////////// 233 | #pragma mark - Class + Selector Preparation 234 | 235 | static BOOL aspect_isMsgForwardIMP(IMP impl) { 236 | return impl == _objc_msgForward 237 | #if !defined(__arm64__) 238 | || impl == (IMP)_objc_msgForward_stret 239 | #endif 240 | ; 241 | } 242 | 243 | static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) { 244 | IMP msgForwardIMP = _objc_msgForward; 245 | #if !defined(__arm64__) 246 | // 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. 247 | // https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/LowLevelABI/000-Introduction/introduction.html 248 | // https://github.com/ReactiveCocoa/ReactiveCocoa/issues/783 249 | // http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042e/IHI0042E_aapcs.pdf (Section 5.4) 250 | Method method = class_getInstanceMethod(self.class, selector); 251 | const char *encoding = method_getTypeEncoding(method); 252 | BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B; 253 | if (methodReturnsStructValue) { 254 | @try { 255 | NSUInteger valueSize = 0; 256 | NSGetSizeAndAlignment(encoding, &valueSize, NULL); 257 | 258 | if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) { 259 | methodReturnsStructValue = NO; 260 | } 261 | } @catch (__unused NSException *e) {} 262 | } 263 | if (methodReturnsStructValue) { 264 | msgForwardIMP = (IMP)_objc_msgForward_stret; 265 | } 266 | #endif 267 | return msgForwardIMP; 268 | } 269 | 270 | static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) { 271 | NSCParameterAssert(selector); 272 | Class klass = aspect_hookClass(self, error); 273 | Method targetMethod = class_getInstanceMethod(klass, selector); 274 | IMP targetMethodIMP = method_getImplementation(targetMethod); 275 | if (!aspect_isMsgForwardIMP(targetMethodIMP)) { 276 | // Make a method alias for the existing method implementation, it not already copied. 277 | const char *typeEncoding = method_getTypeEncoding(targetMethod); 278 | SEL aliasSelector = aspect_aliasForSelector(selector); 279 | if (![klass instancesRespondToSelector:aliasSelector]) { 280 | __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding); 281 | NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass); 282 | } 283 | 284 | // We use forwardInvocation to hook in. 285 | class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding); 286 | AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector)); 287 | } 288 | } 289 | 290 | // Will undo the runtime changes made. 291 | static void aspect_cleanupHookedClassAndSelector(NSObject *self, SEL selector) { 292 | NSCParameterAssert(self); 293 | NSCParameterAssert(selector); 294 | 295 | Class klass = object_getClass(self); 296 | BOOL isMetaClass = class_isMetaClass(klass); 297 | if (isMetaClass) { 298 | klass = (Class)self; 299 | } 300 | 301 | // Check if the method is marked as forwarded and undo that. 302 | Method targetMethod = class_getInstanceMethod(klass, selector); 303 | IMP targetMethodIMP = method_getImplementation(targetMethod); 304 | if (aspect_isMsgForwardIMP(targetMethodIMP)) { 305 | // Restore the original method implementation. 306 | const char *typeEncoding = method_getTypeEncoding(targetMethod); 307 | SEL aliasSelector = aspect_aliasForSelector(selector); 308 | Method originalMethod = class_getInstanceMethod(klass, aliasSelector); 309 | IMP originalIMP = method_getImplementation(originalMethod); 310 | NSCAssert(originalMethod, @"Original implementation for %@ not found %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass); 311 | 312 | class_replaceMethod(klass, selector, originalIMP, typeEncoding); 313 | AspectLog(@"Aspects: Removed hook for -[%@ %@].", klass, NSStringFromSelector(selector)); 314 | } 315 | 316 | // Deregister global tracked selector 317 | aspect_deregisterTrackedSelector(self, selector); 318 | 319 | // Get the aspect container and check if there are any hooks remaining. Clean up if there are not. 320 | AspectsContainer *container = aspect_getContainerForObject(self, selector); 321 | if (!container.hasAspects) { 322 | // Destroy the container 323 | aspect_destroyContainerForObject(self, selector); 324 | 325 | // Figure out how the class was modified to undo the changes. 326 | NSString *className = NSStringFromClass(klass); 327 | if ([className hasSuffix:AspectsSubclassSuffix]) { 328 | Class originalClass = NSClassFromString([className stringByReplacingOccurrencesOfString:AspectsSubclassSuffix withString:@""]); 329 | NSCAssert(originalClass != nil, @"Original class must exist"); 330 | object_setClass(self, originalClass); 331 | AspectLog(@"Aspects: %@ has been restored.", NSStringFromClass(originalClass)); 332 | 333 | // We can only dispose the class pair if we can ensure that no instances exist using our subclass. 334 | // Since we don't globally track this, we can't ensure this - but there's also not much overhead in keeping it around. 335 | //objc_disposeClassPair(object.class); 336 | }else { 337 | // Class is most likely swizzled in place. Undo that. 338 | if (isMetaClass) { 339 | aspect_undoSwizzleClassInPlace((Class)self); 340 | }else if (self.class != klass) { 341 | aspect_undoSwizzleClassInPlace(klass); 342 | } 343 | } 344 | } 345 | } 346 | 347 | /////////////////////////////////////////////////////////////////////////////////////////// 348 | #pragma mark - Hook Class 349 | 350 | static Class aspect_hookClass(NSObject *self, NSError **error) { 351 | NSCParameterAssert(self); 352 | Class statedClass = self.class; 353 | Class baseClass = object_getClass(self); 354 | NSString *className = NSStringFromClass(baseClass); 355 | 356 | // Already subclassed 357 | if ([className hasSuffix:AspectsSubclassSuffix]) { 358 | return baseClass; 359 | 360 | // We swizzle a class object, not a single object. 361 | }else if (class_isMetaClass(baseClass)) { 362 | return aspect_swizzleClassInPlace((Class)self); 363 | // Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place. 364 | }else if (statedClass != baseClass) { 365 | return aspect_swizzleClassInPlace(baseClass); 366 | } 367 | 368 | // Default case. Create dynamic subclass. 369 | const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String; 370 | Class subclass = objc_getClass(subclassName); 371 | 372 | if (subclass == nil) { 373 | subclass = objc_allocateClassPair(baseClass, subclassName, 0); 374 | if (subclass == nil) { 375 | NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName]; 376 | AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc); 377 | return nil; 378 | } 379 | 380 | aspect_swizzleForwardInvocation(subclass); 381 | aspect_hookedGetClass(subclass, statedClass); 382 | aspect_hookedGetClass(object_getClass(subclass), statedClass); 383 | objc_registerClassPair(subclass); 384 | } 385 | 386 | object_setClass(self, subclass); 387 | return subclass; 388 | } 389 | 390 | static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:"; 391 | static void aspect_swizzleForwardInvocation(Class klass) { 392 | NSCParameterAssert(klass); 393 | // If there is no method, replace will act like class_addMethod. 394 | IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@"); 395 | if (originalImplementation) { 396 | class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@"); 397 | } 398 | AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass)); 399 | } 400 | 401 | static void aspect_undoSwizzleForwardInvocation(Class klass) { 402 | NSCParameterAssert(klass); 403 | Method originalMethod = class_getInstanceMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName)); 404 | Method objectMethod = class_getInstanceMethod(NSObject.class, @selector(forwardInvocation:)); 405 | // There is no class_removeMethod, so the best we can do is to retore the original implementation, or use a dummy. 406 | IMP originalImplementation = method_getImplementation(originalMethod ?: objectMethod); 407 | class_replaceMethod(klass, @selector(forwardInvocation:), originalImplementation, "v@:@"); 408 | 409 | AspectLog(@"Aspects: %@ has been restored.", NSStringFromClass(klass)); 410 | } 411 | 412 | static void aspect_hookedGetClass(Class class, Class statedClass) { 413 | NSCParameterAssert(class); 414 | NSCParameterAssert(statedClass); 415 | Method method = class_getInstanceMethod(class, @selector(class)); 416 | IMP newIMP = imp_implementationWithBlock(^(id self) { 417 | return statedClass; 418 | }); 419 | class_replaceMethod(class, @selector(class), newIMP, method_getTypeEncoding(method)); 420 | } 421 | 422 | /////////////////////////////////////////////////////////////////////////////////////////// 423 | #pragma mark - Swizzle Class In Place 424 | 425 | static void _aspect_modifySwizzledClasses(void (^block)(NSMutableSet *swizzledClasses)) { 426 | static NSMutableSet *swizzledClasses; 427 | static dispatch_once_t pred; 428 | dispatch_once(&pred, ^{ 429 | swizzledClasses = [NSMutableSet new]; 430 | }); 431 | @synchronized(swizzledClasses) { 432 | block(swizzledClasses); 433 | } 434 | } 435 | 436 | static Class aspect_swizzleClassInPlace(Class klass) { 437 | NSCParameterAssert(klass); 438 | NSString *className = NSStringFromClass(klass); 439 | 440 | _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) { 441 | if (![swizzledClasses containsObject:className]) { 442 | aspect_swizzleForwardInvocation(klass); 443 | [swizzledClasses addObject:className]; 444 | } 445 | }); 446 | return klass; 447 | } 448 | 449 | static void aspect_undoSwizzleClassInPlace(Class klass) { 450 | NSCParameterAssert(klass); 451 | NSString *className = NSStringFromClass(klass); 452 | 453 | _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) { 454 | if ([swizzledClasses containsObject:className]) { 455 | aspect_undoSwizzleForwardInvocation(klass); 456 | [swizzledClasses removeObject:className]; 457 | } 458 | }); 459 | } 460 | 461 | /////////////////////////////////////////////////////////////////////////////////////////// 462 | #pragma mark - Aspect Invoke Point 463 | 464 | // This is a macro so we get a cleaner stack trace. 465 | #define aspect_invoke(aspects, info) \ 466 | for (AspectIdentifier *aspect in aspects) {\ 467 | [aspect invokeWithInfo:info];\ 468 | if (aspect.options & AspectOptionAutomaticRemoval) { \ 469 | aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect]; \ 470 | } \ 471 | } 472 | 473 | // This is the swizzled forwardInvocation: method. 474 | static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) { 475 | NSCParameterAssert(self); 476 | NSCParameterAssert(invocation); 477 | SEL originalSelector = invocation.selector; 478 | SEL aliasSelector = aspect_aliasForSelector(invocation.selector); 479 | invocation.selector = aliasSelector; 480 | AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector); 481 | AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector); 482 | AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation]; 483 | NSArray *aspectsToRemove = nil; 484 | 485 | // Before hooks. 486 | aspect_invoke(classContainer.beforeAspects, info); 487 | aspect_invoke(objectContainer.beforeAspects, info); 488 | 489 | // Instead hooks. 490 | BOOL respondsToAlias = YES; 491 | if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) { 492 | aspect_invoke(classContainer.insteadAspects, info); 493 | aspect_invoke(objectContainer.insteadAspects, info); 494 | }else { 495 | Class klass = object_getClass(invocation.target); 496 | do { 497 | if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) { 498 | [invocation invoke]; 499 | break; 500 | } 501 | }while (!respondsToAlias && (klass = class_getSuperclass(klass))); 502 | } 503 | 504 | // After hooks. 505 | aspect_invoke(classContainer.afterAspects, info); 506 | aspect_invoke(objectContainer.afterAspects, info); 507 | 508 | // If no hooks are installed, call original implementation (usually to throw an exception) 509 | if (!respondsToAlias) { 510 | invocation.selector = originalSelector; 511 | SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName); 512 | if ([self respondsToSelector:originalForwardInvocationSEL]) { 513 | ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation); 514 | }else { 515 | [self doesNotRecognizeSelector:invocation.selector]; 516 | } 517 | } 518 | 519 | // Remove any hooks that are queued for deregistration. 520 | [aspectsToRemove makeObjectsPerformSelector:@selector(remove)]; 521 | } 522 | #undef aspect_invoke 523 | 524 | /////////////////////////////////////////////////////////////////////////////////////////// 525 | #pragma mark - Aspect Container Management 526 | 527 | // Loads or creates the aspect container. 528 | static AspectsContainer *aspect_getContainerForObject(NSObject *self, SEL selector) { 529 | NSCParameterAssert(self); 530 | SEL aliasSelector = aspect_aliasForSelector(selector); 531 | AspectsContainer *aspectContainer = objc_getAssociatedObject(self, aliasSelector); 532 | if (!aspectContainer) { 533 | aspectContainer = [AspectsContainer new]; 534 | objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN); 535 | } 536 | return aspectContainer; 537 | } 538 | 539 | static AspectsContainer *aspect_getContainerForClass(Class klass, SEL selector) { 540 | NSCParameterAssert(klass); 541 | AspectsContainer *classContainer = nil; 542 | do { 543 | classContainer = objc_getAssociatedObject(klass, selector); 544 | if (classContainer.hasAspects) break; 545 | }while ((klass = class_getSuperclass(klass))); 546 | 547 | return classContainer; 548 | } 549 | 550 | static void aspect_destroyContainerForObject(id self, SEL selector) { 551 | NSCParameterAssert(self); 552 | SEL aliasSelector = aspect_aliasForSelector(selector); 553 | objc_setAssociatedObject(self, aliasSelector, nil, OBJC_ASSOCIATION_RETAIN); 554 | } 555 | 556 | /////////////////////////////////////////////////////////////////////////////////////////// 557 | #pragma mark - Selector Blacklist Checking 558 | 559 | static NSMutableDictionary *aspect_getSwizzledClassesDict() { 560 | static NSMutableDictionary *swizzledClassesDict; 561 | static dispatch_once_t pred; 562 | dispatch_once(&pred, ^{ 563 | swizzledClassesDict = [NSMutableDictionary new]; 564 | }); 565 | return swizzledClassesDict; 566 | } 567 | 568 | static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) { 569 | static NSSet *disallowedSelectorList; 570 | static dispatch_once_t pred; 571 | dispatch_once(&pred, ^{ 572 | disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil]; 573 | }); 574 | 575 | // Check against the blacklist. 576 | NSString *selectorName = NSStringFromSelector(selector); 577 | if ([disallowedSelectorList containsObject:selectorName]) { 578 | NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName]; 579 | AspectError(AspectErrorSelectorBlacklisted, errorDescription); 580 | return NO; 581 | } 582 | 583 | // Additional checks. 584 | AspectOptions position = options&AspectPositionFilter; 585 | if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) { 586 | NSString *errorDesc = @"AspectPositionBefore is the only valid position when hooking dealloc."; 587 | AspectError(AspectErrorSelectorDeallocPosition, errorDesc); 588 | return NO; 589 | } 590 | 591 | if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) { 592 | NSString *errorDesc = [NSString stringWithFormat:@"Unable to find selector -[%@ %@].", NSStringFromClass(self.class), selectorName]; 593 | AspectError(AspectErrorDoesNotRespondToSelector, errorDesc); 594 | return NO; 595 | } 596 | 597 | // Search for the current class and the class hierarchy IF we are modifying a class object 598 | if (class_isMetaClass(object_getClass(self))) { 599 | Class klass = [self class]; 600 | NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict(); 601 | Class currentClass = [self class]; 602 | 603 | AspectTracker *tracker = swizzledClassesDict[currentClass]; 604 | if ([tracker subclassHasHookedSelectorName:selectorName]) { 605 | NSSet *subclassTracker = [tracker subclassTrackersHookingSelectorName:selectorName]; 606 | NSSet *subclassNames = [subclassTracker valueForKey:@"trackedClassName"]; 607 | NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked subclasses: %@. A method can only be hooked once per class hierarchy.", selectorName, subclassNames]; 608 | AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription); 609 | return NO; 610 | } 611 | 612 | do { 613 | tracker = swizzledClassesDict[currentClass]; 614 | if ([tracker.selectorNames containsObject:selectorName]) { 615 | if (klass == currentClass) { 616 | // Already modified and topmost! 617 | return YES; 618 | } 619 | NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(currentClass)]; 620 | AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription); 621 | return NO; 622 | } 623 | } while ((currentClass = class_getSuperclass(currentClass))); 624 | 625 | // Add the selector as being modified. 626 | currentClass = klass; 627 | AspectTracker *subclassTracker = nil; 628 | do { 629 | tracker = swizzledClassesDict[currentClass]; 630 | if (!tracker) { 631 | tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass]; 632 | swizzledClassesDict[(id)currentClass] = tracker; 633 | } 634 | if (subclassTracker) { 635 | [tracker addSubclassTracker:subclassTracker hookingSelectorName:selectorName]; 636 | } else { 637 | [tracker.selectorNames addObject:selectorName]; 638 | } 639 | 640 | // All superclasses get marked as having a subclass that is modified. 641 | subclassTracker = tracker; 642 | }while ((currentClass = class_getSuperclass(currentClass))); 643 | } else { 644 | return YES; 645 | } 646 | 647 | return YES; 648 | } 649 | 650 | static void aspect_deregisterTrackedSelector(id self, SEL selector) { 651 | if (!class_isMetaClass(object_getClass(self))) return; 652 | 653 | NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict(); 654 | NSString *selectorName = NSStringFromSelector(selector); 655 | Class currentClass = [self class]; 656 | AspectTracker *subclassTracker = nil; 657 | do { 658 | AspectTracker *tracker = swizzledClassesDict[currentClass]; 659 | if (subclassTracker) { 660 | [tracker removeSubclassTracker:subclassTracker hookingSelectorName:selectorName]; 661 | } else { 662 | [tracker.selectorNames removeObject:selectorName]; 663 | } 664 | if (tracker.selectorNames.count == 0 && tracker.selectorNamesToSubclassTrackers) { 665 | [swizzledClassesDict removeObjectForKey:currentClass]; 666 | } 667 | subclassTracker = tracker; 668 | }while ((currentClass = class_getSuperclass(currentClass))); 669 | } 670 | 671 | @end 672 | 673 | @implementation AspectTracker 674 | 675 | - (id)initWithTrackedClass:(Class)trackedClass { 676 | if (self = [super init]) { 677 | _trackedClass = trackedClass; 678 | _selectorNames = [NSMutableSet new]; 679 | _selectorNamesToSubclassTrackers = [NSMutableDictionary new]; 680 | } 681 | return self; 682 | } 683 | 684 | - (BOOL)subclassHasHookedSelectorName:(NSString *)selectorName { 685 | return self.selectorNamesToSubclassTrackers[selectorName] != nil; 686 | } 687 | 688 | - (void)addSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName { 689 | NSMutableSet *trackerSet = self.selectorNamesToSubclassTrackers[selectorName]; 690 | if (!trackerSet) { 691 | trackerSet = [NSMutableSet new]; 692 | self.selectorNamesToSubclassTrackers[selectorName] = trackerSet; 693 | } 694 | [trackerSet addObject:subclassTracker]; 695 | } 696 | - (void)removeSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName { 697 | NSMutableSet *trackerSet = self.selectorNamesToSubclassTrackers[selectorName]; 698 | [trackerSet removeObject:subclassTracker]; 699 | if (trackerSet.count == 0) { 700 | [self.selectorNamesToSubclassTrackers removeObjectForKey:selectorName]; 701 | } 702 | } 703 | - (NSSet *)subclassTrackersHookingSelectorName:(NSString *)selectorName { 704 | NSMutableSet *hookingSubclassTrackers = [NSMutableSet new]; 705 | for (AspectTracker *tracker in self.selectorNamesToSubclassTrackers[selectorName]) { 706 | if ([tracker.selectorNames containsObject:selectorName]) { 707 | [hookingSubclassTrackers addObject:tracker]; 708 | } 709 | [hookingSubclassTrackers unionSet:[tracker subclassTrackersHookingSelectorName:selectorName]]; 710 | } 711 | return hookingSubclassTrackers; 712 | } 713 | - (NSString *)trackedClassName { 714 | return NSStringFromClass(self.trackedClass); 715 | } 716 | 717 | - (NSString *)description { 718 | return [NSString stringWithFormat:@"<%@: %@, trackedClass: %@, selectorNames:%@, subclass selector names: %@>", self.class, self, NSStringFromClass(self.trackedClass), self.selectorNames, self.selectorNamesToSubclassTrackers.allKeys]; 719 | } 720 | 721 | @end 722 | 723 | /////////////////////////////////////////////////////////////////////////////////////////// 724 | #pragma mark - NSInvocation (Aspects) 725 | 726 | @implementation NSInvocation (Aspects) 727 | 728 | // Thanks to the ReactiveCocoa team for providing a generic solution for this. 729 | - (id)aspect_argumentAtIndex:(NSUInteger)index { 730 | const char *argType = [self.methodSignature getArgumentTypeAtIndex:index]; 731 | // Skip const type qualifier. 732 | if (argType[0] == _C_CONST) argType++; 733 | 734 | #define WRAP_AND_RETURN(type) do { type val = 0; [self getArgument:&val atIndex:(NSInteger)index]; return @(val); } while (0) 735 | if (strcmp(argType, @encode(id)) == 0 || strcmp(argType, @encode(Class)) == 0) { 736 | __autoreleasing id returnObj; 737 | [self getArgument:&returnObj atIndex:(NSInteger)index]; 738 | return returnObj; 739 | } else if (strcmp(argType, @encode(SEL)) == 0) { 740 | SEL selector = 0; 741 | [self getArgument:&selector atIndex:(NSInteger)index]; 742 | return NSStringFromSelector(selector); 743 | } else if (strcmp(argType, @encode(Class)) == 0) { 744 | __autoreleasing Class theClass = Nil; 745 | [self getArgument:&theClass atIndex:(NSInteger)index]; 746 | return theClass; 747 | // Using this list will box the number with the appropriate constructor, instead of the generic NSValue. 748 | } else if (strcmp(argType, @encode(char)) == 0) { 749 | WRAP_AND_RETURN(char); 750 | } else if (strcmp(argType, @encode(int)) == 0) { 751 | WRAP_AND_RETURN(int); 752 | } else if (strcmp(argType, @encode(short)) == 0) { 753 | WRAP_AND_RETURN(short); 754 | } else if (strcmp(argType, @encode(long)) == 0) { 755 | WRAP_AND_RETURN(long); 756 | } else if (strcmp(argType, @encode(long long)) == 0) { 757 | WRAP_AND_RETURN(long long); 758 | } else if (strcmp(argType, @encode(unsigned char)) == 0) { 759 | WRAP_AND_RETURN(unsigned char); 760 | } else if (strcmp(argType, @encode(unsigned int)) == 0) { 761 | WRAP_AND_RETURN(unsigned int); 762 | } else if (strcmp(argType, @encode(unsigned short)) == 0) { 763 | WRAP_AND_RETURN(unsigned short); 764 | } else if (strcmp(argType, @encode(unsigned long)) == 0) { 765 | WRAP_AND_RETURN(unsigned long); 766 | } else if (strcmp(argType, @encode(unsigned long long)) == 0) { 767 | WRAP_AND_RETURN(unsigned long long); 768 | } else if (strcmp(argType, @encode(float)) == 0) { 769 | WRAP_AND_RETURN(float); 770 | } else if (strcmp(argType, @encode(double)) == 0) { 771 | WRAP_AND_RETURN(double); 772 | } else if (strcmp(argType, @encode(BOOL)) == 0) { 773 | WRAP_AND_RETURN(BOOL); 774 | } else if (strcmp(argType, @encode(bool)) == 0) { 775 | WRAP_AND_RETURN(BOOL); 776 | } else if (strcmp(argType, @encode(char *)) == 0) { 777 | WRAP_AND_RETURN(const char *); 778 | } else if (strcmp(argType, @encode(void (^)(void))) == 0) { 779 | __unsafe_unretained id block = nil; 780 | [self getArgument:&block atIndex:(NSInteger)index]; 781 | return [block copy]; 782 | } else { 783 | NSUInteger valueSize = 0; 784 | NSGetSizeAndAlignment(argType, &valueSize, NULL); 785 | 786 | unsigned char valueBytes[valueSize]; 787 | [self getArgument:valueBytes atIndex:(NSInteger)index]; 788 | 789 | return [NSValue valueWithBytes:valueBytes objCType:argType]; 790 | } 791 | return nil; 792 | #undef WRAP_AND_RETURN 793 | } 794 | 795 | - (NSArray *)aspects_arguments { 796 | NSMutableArray *argumentsArray = [NSMutableArray array]; 797 | for (NSUInteger idx = 2; idx < self.methodSignature.numberOfArguments; idx++) { 798 | [argumentsArray addObject:[self aspect_argumentAtIndex:idx] ?: NSNull.null]; 799 | } 800 | return [argumentsArray copy]; 801 | } 802 | 803 | @end 804 | 805 | /////////////////////////////////////////////////////////////////////////////////////////// 806 | #pragma mark - AspectIdentifier 807 | 808 | @implementation AspectIdentifier 809 | 810 | + (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error { 811 | NSCParameterAssert(block); 812 | NSCParameterAssert(selector); 813 | NSMethodSignature *blockSignature = aspect_blockMethodSignature(block, error); // TODO: check signature compatibility, etc. 814 | if (!aspect_isCompatibleBlockSignature(blockSignature, object, selector, error)) { 815 | return nil; 816 | } 817 | 818 | AspectIdentifier *identifier = nil; 819 | if (blockSignature) { 820 | identifier = [AspectIdentifier new]; 821 | identifier.selector = selector; 822 | identifier.block = block; 823 | identifier.blockSignature = blockSignature; 824 | identifier.options = options; 825 | identifier.object = object; // weak 826 | } 827 | return identifier; 828 | } 829 | 830 | - (BOOL)invokeWithInfo:(id)info { 831 | NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature]; 832 | NSInvocation *originalInvocation = info.originalInvocation; 833 | NSUInteger numberOfArguments = self.blockSignature.numberOfArguments; 834 | 835 | // Be extra paranoid. We already check that on hook registration. 836 | if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) { 837 | AspectLogError(@"Block has too many arguments. Not calling %@", info); 838 | return NO; 839 | } 840 | 841 | // The `self` of the block will be the AspectInfo. Optional. 842 | if (numberOfArguments > 1) { 843 | [blockInvocation setArgument:&info atIndex:1]; 844 | } 845 | 846 | void *argBuf = NULL; 847 | for (NSUInteger idx = 2; idx < numberOfArguments; idx++) { 848 | const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx]; 849 | NSUInteger argSize; 850 | NSGetSizeAndAlignment(type, &argSize, NULL); 851 | 852 | if (!(argBuf = reallocf(argBuf, argSize))) { 853 | AspectLogError(@"Failed to allocate memory for block invocation."); 854 | return NO; 855 | } 856 | 857 | [originalInvocation getArgument:argBuf atIndex:idx]; 858 | [blockInvocation setArgument:argBuf atIndex:idx]; 859 | } 860 | 861 | [blockInvocation invokeWithTarget:self.block]; 862 | 863 | if (argBuf != NULL) { 864 | free(argBuf); 865 | } 866 | return YES; 867 | } 868 | 869 | - (NSString *)description { 870 | 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]; 871 | } 872 | 873 | - (BOOL)remove { 874 | return aspect_remove(self, NULL); 875 | } 876 | 877 | @end 878 | 879 | /////////////////////////////////////////////////////////////////////////////////////////// 880 | #pragma mark - AspectsContainer 881 | 882 | @implementation AspectsContainer 883 | 884 | - (BOOL)hasAspects { 885 | return self.beforeAspects.count > 0 || self.insteadAspects.count > 0 || self.afterAspects.count > 0; 886 | } 887 | 888 | - (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)options { 889 | NSParameterAssert(aspect); 890 | NSUInteger position = options&AspectPositionFilter; 891 | switch (position) { 892 | case AspectPositionBefore: self.beforeAspects = [(self.beforeAspects ?:@[]) arrayByAddingObject:aspect]; break; 893 | case AspectPositionInstead: self.insteadAspects = [(self.insteadAspects?:@[]) arrayByAddingObject:aspect]; break; 894 | case AspectPositionAfter: self.afterAspects = [(self.afterAspects ?:@[]) arrayByAddingObject:aspect]; break; 895 | } 896 | } 897 | 898 | - (BOOL)removeAspect:(id)aspect { 899 | for (NSString *aspectArrayName in @[NSStringFromSelector(@selector(beforeAspects)), 900 | NSStringFromSelector(@selector(insteadAspects)), 901 | NSStringFromSelector(@selector(afterAspects))]) { 902 | NSArray *array = [self valueForKey:aspectArrayName]; 903 | NSUInteger index = [array indexOfObjectIdenticalTo:aspect]; 904 | if (array && index != NSNotFound) { 905 | NSMutableArray *newArray = [NSMutableArray arrayWithArray:array]; 906 | [newArray removeObjectAtIndex:index]; 907 | [self setValue:newArray forKey:aspectArrayName]; 908 | return YES; 909 | } 910 | } 911 | return NO; 912 | } 913 | 914 | - (NSString *)description { 915 | return [NSString stringWithFormat:@"<%@: %p, before:%@, instead:%@, after:%@>", self.class, self, self.beforeAspects, self.insteadAspects, self.afterAspects]; 916 | } 917 | 918 | @end 919 | 920 | /////////////////////////////////////////////////////////////////////////////////////////// 921 | #pragma mark - AspectInfo 922 | 923 | @implementation AspectInfo 924 | 925 | @synthesize arguments = _arguments; 926 | 927 | - (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation { 928 | NSCParameterAssert(instance); 929 | NSCParameterAssert(invocation); 930 | if (self = [super init]) { 931 | _instance = instance; 932 | _originalInvocation = invocation; 933 | } 934 | return self; 935 | } 936 | 937 | - (NSArray *)arguments { 938 | // Lazily evaluate arguments, boxing is expensive. 939 | if (!_arguments) { 940 | _arguments = self.originalInvocation.aspects_arguments; 941 | } 942 | return _arguments; 943 | } 944 | 945 | @end 946 | -------------------------------------------------------------------------------- /PreciseCoverage/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 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 | 9AFF134A-08DC-4096-8CEE-62A4BB123046 28 | 7265231C-39B4-402C-89E1-16167C4CC990 29 | 9AFF134A-08DC-4096-8CEE-62A4BB123046 30 | F41BD31E-2683-44B8-AE7F-5F09E919790E 31 | ACA8656B-FEA8-4B6D-8E4A-93F4C95C362C 32 | 33 | LSMinimumSystemVersion 34 | $(MACOSX_DEPLOYMENT_TARGET) 35 | NSPrincipalClass 36 | PreciseCoverage 37 | XC4Compatible 38 | 39 | XCPluginHasUI 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /PreciseCoverage/PreciseCoverage.h: -------------------------------------------------------------------------------- 1 | // 2 | // PreciseCoverage.h 3 | // PreciseCoverage 4 | // 5 | // Created by Sash Zats on 11/26/15. 6 | // Copyright © 2015 Sash Zats. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface PreciseCoverage : NSObject 12 | 13 | + (instancetype)sharedPlugin; 14 | 15 | @property (nonatomic, strong, readonly) NSBundle* bundle; 16 | @end -------------------------------------------------------------------------------- /PreciseCoverage/PreciseCoverage.m: -------------------------------------------------------------------------------- 1 | // 2 | // PreciseCoverage.m 3 | // PreciseCoverage 4 | // 5 | // Created by Sash Zats on 11/26/15. 6 | // Copyright © 2015 Sash Zats. All rights reserved. 7 | // 8 | 9 | #import "PreciseCoverage.h" 10 | #import 11 | #import "Aspects.h" 12 | 13 | 14 | static PreciseCoverage *sharedPlugin; 15 | 16 | 17 | @interface PreciseCoverage() 18 | @property (nonatomic, strong, readwrite) NSBundle *bundle; 19 | @end 20 | 21 | 22 | @implementation PreciseCoverage 23 | 24 | + (void)pluginDidLoad:(NSBundle *)plugin { 25 | static dispatch_once_t onceToken; 26 | NSString *currentApplicationName = [[NSBundle mainBundle] infoDictionary][@"CFBundleName"]; 27 | if ([currentApplicationName isEqual:@"Xcode"]) { 28 | dispatch_once(&onceToken, ^{ 29 | sharedPlugin = [[self alloc] initWithBundle:plugin]; 30 | }); 31 | } 32 | } 33 | 34 | + (instancetype)sharedPlugin { 35 | return sharedPlugin; 36 | } 37 | 38 | - (id)initWithBundle:(NSBundle *)plugin { 39 | self = [super init]; 40 | if (!self) { 41 | return nil; 42 | } 43 | self.bundle = plugin; 44 | [self _init]; 45 | return self; 46 | } 47 | 48 | - (void)_init { 49 | Class IDECoverageReportMeterBar = NSClassFromString(@"IDECoverageReportMeterBar"); 50 | [IDECoverageReportMeterBar aspect_hookSelector:@selector(setDoubleValue:) withOptions:AspectPositionAfter usingBlock:^(id info, double newValue){ 51 | NSView *instance = [info instance]; 52 | static const NSInteger labelTag = 124; 53 | NSTextField *label = [instance viewWithTag:labelTag]; 54 | if (!label) { 55 | label = [[NSTextField alloc] init]; 56 | label.editable = NO; 57 | label.bezeled = NO; 58 | label.drawsBackground = NO; 59 | label.tag = labelTag; 60 | label.selectable = NO; 61 | label.font = [NSFont systemFontOfSize:10]; 62 | label.alignment = NSTextAlignmentCenter; 63 | [instance addSubview:label]; 64 | } 65 | label.frame = ({ 66 | CGRect frame = instance.bounds; 67 | frame.size.width -= 4; 68 | frame.size.height -= 4; 69 | frame.origin.y -= 2; 70 | frame; 71 | }); 72 | NSString *string = [NSString stringWithFormat:@"%.0f%%", newValue]; 73 | label.alignment = NSTextAlignmentRight; 74 | label.stringValue = string; 75 | } error:nil]; 76 | [IDECoverageReportMeterBar aspect_hookSelector:@selector(drawRect:) withOptions:AspectPositionInstead usingBlock:^(id info, CGRect bounds){ 77 | double progress = [[info instance] doubleValue]; 78 | 79 | NSRect frame = [[info instance] bounds]; 80 | frame = CGRectInset(frame, 4, 6); 81 | frame.origin.y -= 2; 82 | frame.size.width -= 35; 83 | 84 | const CGFloat radius = 4; 85 | 86 | NSBezierPath *background = [NSBezierPath bezierPathWithRoundedRect:frame xRadius:radius yRadius:radius]; 87 | [[NSColor colorWithDeviceWhite:0.851 alpha:1.000] setFill]; 88 | [background fill]; 89 | 90 | frame.size.width *= progress / 100; 91 | NSBezierPath *track = [NSBezierPath bezierPathWithRoundedRect:frame xRadius:radius yRadius:radius]; 92 | if (progress == 100) { 93 | [[NSColor colorWithDeviceRed:0.191 green:0.638 blue:0.261 alpha:1.000] setFill]; 94 | } else if (progress >= 70) { 95 | [[NSColor colorWithDeviceRed:0.986 green:0.780 blue:0.000 alpha:1.000] setFill]; 96 | } else { 97 | [[NSColor colorWithDeviceRed:0.675 green:0.157 blue:0.109 alpha:1.000] setFill]; 98 | } 99 | [track fill]; 100 | NSLog(@"\n\n\nprogress %f\n\n\n", progress); 101 | } error:nil]; 102 | } 103 | 104 | - (void)dealloc { 105 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 106 | } 107 | 108 | @end 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/zats/PreciseCoverage/raw/master/screenshot.png) 2 | 3 | Available through alcatraz, too 4 | 5 | # Features 6 | 7 | * Show precise percentage 8 | * Color coding: 9 | * 100% - green; 10 | * ≥70% - yellow; 11 | * <70% - red; 12 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zats/PreciseCoverage/809976e29563aad371e2346464a50711fdcf2ee7/screenshot.png --------------------------------------------------------------------------------