├── .gitignore ├── NTKCustomFace.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ ├── NTKCustomFace.xcscheme │ └── NTKCustomFaceLauncher.xcscheme ├── NTKCustomFace ├── Hooking.h ├── Loader.m ├── NTKCustomFace.h ├── NTKCustomFace.m ├── NTKCustomFaceView.h ├── NTKCustomFaceView.m └── NanoTimeKit.h ├── Private Frameworks └── PUT_PRIVATE_FRAMEWORKS_HERE ├── README.md ├── launcher.sh └── lldb.py /.gitignore: -------------------------------------------------------------------------------- 1 | Private Frameworks/*.framework 2 | 3 | build/ 4 | tmp/ 5 | DerivedData/ 6 | 7 | ## Various settings 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata/ 17 | 18 | ## Other 19 | *.moved-aside 20 | *.xccheckout 21 | *.xcscmblueprint 22 | 23 | -------------------------------------------------------------------------------- /NTKCustomFace.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2E39357D217361B2002AE7B1 /* RelevanceEngine.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2E39357B217361B2002AE7B1 /* RelevanceEngine.framework */; }; 11 | 2E39357E217361B2002AE7B1 /* NanoTimeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2E39357C217361B2002AE7B1 /* NanoTimeKit.framework */; }; 12 | 2E6812012172086600B528F1 /* NTKCustomFace.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E6812002172086600B528F1 /* NTKCustomFace.m */; }; 13 | 2E68120621720B0B00B528F1 /* NTKCustomFaceView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E68120521720B0B00B528F1 /* NTKCustomFaceView.m */; }; 14 | 6FDF5E261B7E4A4B002ABBD8 /* Loader.m in Sources */ = {isa = PBXBuildFile; fileRef = 6FDF5E251B7E4A4B002ABBD8 /* Loader.m */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXCopyFilesBuildPhase section */ 18 | 6FC5B99E1B7E46F100E87B8D /* CopyFiles */ = { 19 | isa = PBXCopyFilesBuildPhase; 20 | buildActionMask = 2147483647; 21 | dstPath = "include/$(PRODUCT_NAME)"; 22 | dstSubfolderSpec = 16; 23 | files = ( 24 | ); 25 | runOnlyForDeploymentPostprocessing = 0; 26 | }; 27 | /* End PBXCopyFilesBuildPhase section */ 28 | 29 | /* Begin PBXFileReference section */ 30 | 2E3935752172209F002AE7B1 /* NanoTimeKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NanoTimeKit.h; sourceTree = ""; }; 31 | 2E39357B217361B2002AE7B1 /* RelevanceEngine.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = RelevanceEngine.framework; sourceTree = ""; }; 32 | 2E39357C217361B2002AE7B1 /* NanoTimeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = NanoTimeKit.framework; sourceTree = ""; }; 33 | 2E393580217361DB002AE7B1 /* launcher.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = launcher.sh; sourceTree = ""; }; 34 | 2E393581217361DB002AE7B1 /* lldb.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = lldb.py; sourceTree = ""; }; 35 | 2E6811FF2172086600B528F1 /* NTKCustomFace.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NTKCustomFace.h; sourceTree = ""; }; 36 | 2E6812002172086600B528F1 /* NTKCustomFace.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NTKCustomFace.m; sourceTree = ""; }; 37 | 2E68120421720B0B00B528F1 /* NTKCustomFaceView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NTKCustomFaceView.h; sourceTree = ""; }; 38 | 2E68120521720B0B00B528F1 /* NTKCustomFaceView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NTKCustomFaceView.m; sourceTree = ""; }; 39 | 6F7B7D011B81111C00AB713F /* Hooking.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Hooking.h; sourceTree = ""; }; 40 | 6FC5B9A01B7E46F100E87B8D /* NTKCustomFace.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = NTKCustomFace.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 6FDF5E251B7E4A4B002ABBD8 /* Loader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Loader.m; sourceTree = ""; }; 42 | /* End PBXFileReference section */ 43 | 44 | /* Begin PBXFrameworksBuildPhase section */ 45 | 6FC5B99D1B7E46F100E87B8D /* Frameworks */ = { 46 | isa = PBXFrameworksBuildPhase; 47 | buildActionMask = 2147483647; 48 | files = ( 49 | 2E39357E217361B2002AE7B1 /* NanoTimeKit.framework in Frameworks */, 50 | 2E39357D217361B2002AE7B1 /* RelevanceEngine.framework in Frameworks */, 51 | ); 52 | runOnlyForDeploymentPostprocessing = 0; 53 | }; 54 | /* End PBXFrameworksBuildPhase section */ 55 | 56 | /* Begin PBXGroup section */ 57 | 2E39357721723B5B002AE7B1 /* Private Frameworks */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 2E39357C217361B2002AE7B1 /* NanoTimeKit.framework */, 61 | 2E39357B217361B2002AE7B1 /* RelevanceEngine.framework */, 62 | ); 63 | path = "Private Frameworks"; 64 | sourceTree = ""; 65 | }; 66 | 2E39357F217361C8002AE7B1 /* Scripts */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | 2E393580217361DB002AE7B1 /* launcher.sh */, 70 | 2E393581217361DB002AE7B1 /* lldb.py */, 71 | ); 72 | name = Scripts; 73 | sourceTree = ""; 74 | }; 75 | 2E680E832172025E00B528F1 /* Frameworks */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | ); 79 | name = Frameworks; 80 | sourceTree = ""; 81 | }; 82 | 6FC5B9971B7E46F100E87B8D = { 83 | isa = PBXGroup; 84 | children = ( 85 | 6FC5B9A21B7E46F100E87B8D /* NTKCustomFace */, 86 | 6FC5B9A11B7E46F100E87B8D /* Products */, 87 | 2E39357F217361C8002AE7B1 /* Scripts */, 88 | 2E39357721723B5B002AE7B1 /* Private Frameworks */, 89 | 2E680E832172025E00B528F1 /* Frameworks */, 90 | ); 91 | sourceTree = ""; 92 | }; 93 | 6FC5B9A11B7E46F100E87B8D /* Products */ = { 94 | isa = PBXGroup; 95 | children = ( 96 | 6FC5B9A01B7E46F100E87B8D /* NTKCustomFace.dylib */, 97 | ); 98 | name = Products; 99 | sourceTree = ""; 100 | }; 101 | 6FC5B9A21B7E46F100E87B8D /* NTKCustomFace */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 6F7B7D011B81111C00AB713F /* Hooking.h */, 105 | 6FDF5E251B7E4A4B002ABBD8 /* Loader.m */, 106 | 2E6811FF2172086600B528F1 /* NTKCustomFace.h */, 107 | 2E6812002172086600B528F1 /* NTKCustomFace.m */, 108 | 2E68120421720B0B00B528F1 /* NTKCustomFaceView.h */, 109 | 2E68120521720B0B00B528F1 /* NTKCustomFaceView.m */, 110 | 2E3935752172209F002AE7B1 /* NanoTimeKit.h */, 111 | ); 112 | path = NTKCustomFace; 113 | sourceTree = ""; 114 | }; 115 | /* End PBXGroup section */ 116 | 117 | /* Begin PBXLegacyTarget section */ 118 | 2EEA2D1D21763EF9001D1C41 /* NTKCustomFaceLauncher */ = { 119 | isa = PBXLegacyTarget; 120 | buildArgumentsString = "$(ACTION)"; 121 | buildConfigurationList = 2EEA2D2021763EF9001D1C41 /* Build configuration list for PBXLegacyTarget "NTKCustomFaceLauncher" */; 122 | buildPhases = ( 123 | ); 124 | buildToolPath = "$(SRCROOT)/launcher.sh"; 125 | buildWorkingDirectory = "/Users/maku/Documents/Projects/Open Source/NTKCustomFace"; 126 | dependencies = ( 127 | ); 128 | name = NTKCustomFaceLauncher; 129 | passBuildSettingsInEnvironment = 1; 130 | productName = NTKCustomFaceLauncher; 131 | }; 132 | /* End PBXLegacyTarget section */ 133 | 134 | /* Begin PBXNativeTarget section */ 135 | 6FC5B99F1B7E46F100E87B8D /* NTKCustomFace */ = { 136 | isa = PBXNativeTarget; 137 | buildConfigurationList = 6FC5B9A91B7E46F100E87B8D /* Build configuration list for PBXNativeTarget "NTKCustomFace" */; 138 | buildPhases = ( 139 | 6FC5B99C1B7E46F100E87B8D /* Sources */, 140 | 6FC5B99D1B7E46F100E87B8D /* Frameworks */, 141 | 6FC5B99E1B7E46F100E87B8D /* CopyFiles */, 142 | ); 143 | buildRules = ( 144 | ); 145 | dependencies = ( 146 | ); 147 | name = NTKCustomFace; 148 | productName = CustomWatchFaceTest; 149 | productReference = 6FC5B9A01B7E46F100E87B8D /* NTKCustomFace.dylib */; 150 | productType = "com.apple.product-type.library.dynamic"; 151 | }; 152 | /* End PBXNativeTarget section */ 153 | 154 | /* Begin PBXProject section */ 155 | 6FC5B9981B7E46F100E87B8D /* Project object */ = { 156 | isa = PBXProject; 157 | attributes = { 158 | LastUpgradeCheck = 0700; 159 | TargetAttributes = { 160 | 2EEA2D1D21763EF9001D1C41 = { 161 | DevelopmentTeam = Q7LB9QSYQ9; 162 | ProvisioningStyle = Automatic; 163 | }; 164 | }; 165 | }; 166 | buildConfigurationList = 6FC5B99B1B7E46F100E87B8D /* Build configuration list for PBXProject "NTKCustomFace" */; 167 | compatibilityVersion = "Xcode 3.2"; 168 | developmentRegion = English; 169 | hasScannedForEncodings = 0; 170 | knownRegions = ( 171 | en, 172 | ); 173 | mainGroup = 6FC5B9971B7E46F100E87B8D; 174 | productRefGroup = 6FC5B9A11B7E46F100E87B8D /* Products */; 175 | projectDirPath = ""; 176 | projectRoot = ""; 177 | targets = ( 178 | 6FC5B99F1B7E46F100E87B8D /* NTKCustomFace */, 179 | 2EEA2D1D21763EF9001D1C41 /* NTKCustomFaceLauncher */, 180 | ); 181 | }; 182 | /* End PBXProject section */ 183 | 184 | /* Begin PBXSourcesBuildPhase section */ 185 | 6FC5B99C1B7E46F100E87B8D /* Sources */ = { 186 | isa = PBXSourcesBuildPhase; 187 | buildActionMask = 2147483647; 188 | files = ( 189 | 6FDF5E261B7E4A4B002ABBD8 /* Loader.m in Sources */, 190 | 2E6812012172086600B528F1 /* NTKCustomFace.m in Sources */, 191 | 2E68120621720B0B00B528F1 /* NTKCustomFaceView.m in Sources */, 192 | ); 193 | runOnlyForDeploymentPostprocessing = 0; 194 | }; 195 | /* End PBXSourcesBuildPhase section */ 196 | 197 | /* Begin XCBuildConfiguration section */ 198 | 2EEA2D1E21763EF9001D1C41 /* Debug */ = { 199 | isa = XCBuildConfiguration; 200 | buildSettings = { 201 | CLANG_ANALYZER_NONNULL = YES; 202 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 203 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 204 | CLANG_ENABLE_OBJC_WEAK = YES; 205 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 206 | CLANG_WARN_COMMA = YES; 207 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 208 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 209 | CLANG_WARN_INFINITE_RECURSION = YES; 210 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 211 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 212 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 213 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 214 | CLANG_WARN_STRICT_PROTOTYPES = YES; 215 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 216 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 217 | CODE_SIGN_STYLE = Automatic; 218 | DEBUGGING_SYMBOLS = YES; 219 | DEBUG_INFORMATION_FORMAT = dwarf; 220 | DEVELOPMENT_TEAM = Q7LB9QSYQ9; 221 | GCC_C_LANGUAGE_STANDARD = gnu11; 222 | GCC_GENERATE_DEBUGGING_SYMBOLS = YES; 223 | GCC_OPTIMIZATION_LEVEL = 0; 224 | GCC_WARN_UNDECLARED_SELECTOR = YES; 225 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 226 | MTL_FAST_MATH = YES; 227 | ONLY_ACTIVE_ARCH = YES; 228 | OTHER_CFLAGS = ""; 229 | OTHER_LDFLAGS = ""; 230 | PRODUCT_NAME = "$(TARGET_NAME)"; 231 | }; 232 | name = Debug; 233 | }; 234 | 2EEA2D1F21763EF9001D1C41 /* Release */ = { 235 | isa = XCBuildConfiguration; 236 | buildSettings = { 237 | CLANG_ANALYZER_NONNULL = YES; 238 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 239 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 240 | CLANG_ENABLE_OBJC_WEAK = YES; 241 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 242 | CLANG_WARN_COMMA = YES; 243 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 244 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 245 | CLANG_WARN_INFINITE_RECURSION = YES; 246 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 247 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 248 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 249 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 250 | CLANG_WARN_STRICT_PROTOTYPES = YES; 251 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 252 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 253 | CODE_SIGN_STYLE = Automatic; 254 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 255 | DEVELOPMENT_TEAM = Q7LB9QSYQ9; 256 | GCC_C_LANGUAGE_STANDARD = gnu11; 257 | GCC_WARN_UNDECLARED_SELECTOR = YES; 258 | MTL_FAST_MATH = YES; 259 | OTHER_CFLAGS = ""; 260 | OTHER_LDFLAGS = ""; 261 | PRODUCT_NAME = "$(TARGET_NAME)"; 262 | }; 263 | name = Release; 264 | }; 265 | 6FC5B9A71B7E46F100E87B8D /* Debug */ = { 266 | isa = XCBuildConfiguration; 267 | buildSettings = { 268 | ALWAYS_SEARCH_USER_PATHS = NO; 269 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 270 | CLANG_CXX_LIBRARY = "libc++"; 271 | CLANG_ENABLE_MODULES = YES; 272 | CLANG_ENABLE_OBJC_ARC = YES; 273 | CLANG_WARN_BOOL_CONVERSION = YES; 274 | CLANG_WARN_CONSTANT_CONVERSION = YES; 275 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 276 | CLANG_WARN_EMPTY_BODY = YES; 277 | CLANG_WARN_ENUM_CONVERSION = YES; 278 | CLANG_WARN_INT_CONVERSION = YES; 279 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 280 | CLANG_WARN_UNREACHABLE_CODE = YES; 281 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 282 | COPY_PHASE_STRIP = NO; 283 | DEBUG_INFORMATION_FORMAT = dwarf; 284 | ENABLE_STRICT_OBJC_MSGSEND = YES; 285 | ENABLE_TESTABILITY = YES; 286 | GCC_C_LANGUAGE_STANDARD = gnu99; 287 | GCC_DYNAMIC_NO_PIC = NO; 288 | GCC_NO_COMMON_BLOCKS = YES; 289 | GCC_OPTIMIZATION_LEVEL = 0; 290 | GCC_PREPROCESSOR_DEFINITIONS = ( 291 | "DEBUG=1", 292 | "$(inherited)", 293 | ); 294 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 295 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 296 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 297 | GCC_WARN_UNUSED_FUNCTION = YES; 298 | GCC_WARN_UNUSED_VARIABLE = YES; 299 | MTL_ENABLE_DEBUG_INFO = YES; 300 | SDKROOT = watchos; 301 | }; 302 | name = Debug; 303 | }; 304 | 6FC5B9A81B7E46F100E87B8D /* Release */ = { 305 | isa = XCBuildConfiguration; 306 | buildSettings = { 307 | ALWAYS_SEARCH_USER_PATHS = NO; 308 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 309 | CLANG_CXX_LIBRARY = "libc++"; 310 | CLANG_ENABLE_MODULES = YES; 311 | CLANG_ENABLE_OBJC_ARC = YES; 312 | CLANG_WARN_BOOL_CONVERSION = YES; 313 | CLANG_WARN_CONSTANT_CONVERSION = YES; 314 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 315 | CLANG_WARN_EMPTY_BODY = YES; 316 | CLANG_WARN_ENUM_CONVERSION = YES; 317 | CLANG_WARN_INT_CONVERSION = YES; 318 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 319 | CLANG_WARN_UNREACHABLE_CODE = YES; 320 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 321 | COPY_PHASE_STRIP = NO; 322 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 323 | ENABLE_NS_ASSERTIONS = NO; 324 | ENABLE_STRICT_OBJC_MSGSEND = YES; 325 | GCC_C_LANGUAGE_STANDARD = gnu99; 326 | GCC_NO_COMMON_BLOCKS = YES; 327 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 328 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 329 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 330 | GCC_WARN_UNUSED_FUNCTION = YES; 331 | GCC_WARN_UNUSED_VARIABLE = YES; 332 | MTL_ENABLE_DEBUG_INFO = NO; 333 | SDKROOT = watchos; 334 | VALIDATE_PRODUCT = YES; 335 | }; 336 | name = Release; 337 | }; 338 | 6FC5B9AA1B7E46F100E87B8D /* Debug */ = { 339 | isa = XCBuildConfiguration; 340 | buildSettings = { 341 | CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/"; 342 | FRAMEWORK_SEARCH_PATHS = ( 343 | "$(inherited)", 344 | "\"$(PROJECT_DIR)/Private Frameworks\"", 345 | "$(PROJECT_DIR)", 346 | "$(PROJECT_DIR)/Private\\ Frameworks", 347 | "$(PROJECT_DIR)/Private\\ Frameworks", 348 | ); 349 | OTHER_LDFLAGS = ( 350 | "-framework", 351 | NanoTimeKit, 352 | "-framework", 353 | RelevanceEngine, 354 | ); 355 | PRODUCT_NAME = "$(TARGET_NAME)"; 356 | SKIP_INSTALL = YES; 357 | }; 358 | name = Debug; 359 | }; 360 | 6FC5B9AB1B7E46F100E87B8D /* Release */ = { 361 | isa = XCBuildConfiguration; 362 | buildSettings = { 363 | CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/"; 364 | FRAMEWORK_SEARCH_PATHS = ( 365 | "$(inherited)", 366 | "\"$(PROJECT_DIR)/Private Frameworks\"", 367 | "$(PROJECT_DIR)", 368 | "$(PROJECT_DIR)/Private\\ Frameworks", 369 | "$(PROJECT_DIR)/Private\\ Frameworks", 370 | ); 371 | OTHER_LDFLAGS = ( 372 | "-framework", 373 | NanoTimeKit, 374 | "-framework", 375 | RelevanceEngine, 376 | ); 377 | PRODUCT_NAME = "$(TARGET_NAME)"; 378 | SKIP_INSTALL = YES; 379 | }; 380 | name = Release; 381 | }; 382 | /* End XCBuildConfiguration section */ 383 | 384 | /* Begin XCConfigurationList section */ 385 | 2EEA2D2021763EF9001D1C41 /* Build configuration list for PBXLegacyTarget "NTKCustomFaceLauncher" */ = { 386 | isa = XCConfigurationList; 387 | buildConfigurations = ( 388 | 2EEA2D1E21763EF9001D1C41 /* Debug */, 389 | 2EEA2D1F21763EF9001D1C41 /* Release */, 390 | ); 391 | defaultConfigurationIsVisible = 0; 392 | defaultConfigurationName = Release; 393 | }; 394 | 6FC5B99B1B7E46F100E87B8D /* Build configuration list for PBXProject "NTKCustomFace" */ = { 395 | isa = XCConfigurationList; 396 | buildConfigurations = ( 397 | 6FC5B9A71B7E46F100E87B8D /* Debug */, 398 | 6FC5B9A81B7E46F100E87B8D /* Release */, 399 | ); 400 | defaultConfigurationIsVisible = 0; 401 | defaultConfigurationName = Release; 402 | }; 403 | 6FC5B9A91B7E46F100E87B8D /* Build configuration list for PBXNativeTarget "NTKCustomFace" */ = { 404 | isa = XCConfigurationList; 405 | buildConfigurations = ( 406 | 6FC5B9AA1B7E46F100E87B8D /* Debug */, 407 | 6FC5B9AB1B7E46F100E87B8D /* Release */, 408 | ); 409 | defaultConfigurationIsVisible = 0; 410 | defaultConfigurationName = Release; 411 | }; 412 | /* End XCConfigurationList section */ 413 | }; 414 | rootObject = 6FC5B9981B7E46F100E87B8D /* Project object */; 415 | } 416 | -------------------------------------------------------------------------------- /NTKCustomFace.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NTKCustomFace.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NTKCustomFace.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /NTKCustomFace.xcodeproj/xcshareddata/xcschemes/NTKCustomFace.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 48 | 49 | 50 | 56 | 57 | 58 | 59 | 60 | 61 | 67 | 68 | 74 | 75 | 76 | 77 | 79 | 80 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /NTKCustomFace.xcodeproj/xcshareddata/xcschemes/NTKCustomFaceLauncher.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 49 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 76 | 82 | 83 | 84 | 85 | 87 | 88 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /NTKCustomFace/Hooking.h: -------------------------------------------------------------------------------- 1 | // 2 | // Hooking.h 3 | // NTKCustomFace 4 | // 5 | // Created by Michał Kałużny on 13.10.18. 6 | // 7 | 8 | #import 9 | 10 | #define Constructor(name) __attribute__((constructor)) static void name() 11 | 12 | #define GetClass(className) objc_getClass(#className) 13 | 14 | #define NoArguments 15 | #define ArgList(...) ,##__VA_ARGS__ 16 | 17 | #define HookClassMethod(returnType, className, selector, arglist, replacement) \ 18 | do { \ 19 | static returnType (*__orig)(Class self, SEL _cmd arglist); \ 20 | __orig = (void *)method_setImplementation(class_getClassMethod(objc_getClass(#className), selector), \ 21 | imp_implementationWithBlock(^(Class self arglist) { \ 22 | __attribute__ ((unused)) SEL _cmd = selector; \ 23 | replacement \ 24 | })); \ 25 | } while(0) 26 | 27 | #define HookInstanceMethod(returnType, className, selector, arglist, replacement) \ 28 | do { \ 29 | static returnType (*__orig)(className *self, SEL _cmd arglist); \ 30 | __orig = (void *)method_setImplementation(class_getInstanceMethod(objc_getClass(#className), selector), \ 31 | imp_implementationWithBlock(^(className *self arglist) { \ 32 | __attribute__ ((unused)) SEL _cmd = selector; \ 33 | replacement \ 34 | })); \ 35 | } while(0) 36 | 37 | #define Orig(...) __orig(self, _cmd, ##__VA_ARGS__) 38 | 39 | #define GetIvar(obj, name) object_getIvar(obj, class_getInstanceVariable([obj class], #name)) 40 | #define SetIvar(obj, name, value) object_setIvar(obj, class_getInstanceVariable([obj class], #name), value) 41 | -------------------------------------------------------------------------------- /NTKCustomFace/Loader.m: -------------------------------------------------------------------------------- 1 | // 2 | // SupportingHooks.m 3 | // NTKCustomFace 4 | // 5 | // Created by Michał Kałużny on 13.10.18. 6 | // 7 | 8 | #import 9 | 10 | #import "Hooking.h" 11 | 12 | #import "NanoTimeKit.h" 13 | 14 | #import "NTKCustomFace.h" 15 | #import "NTKCustomFaceView.h" 16 | 17 | 18 | UIViewController* findViewController(UIViewController *parent, Class klass) { 19 | for (UIViewController *vc in [parent childViewControllers]) { 20 | if ([vc isMemberOfClass:klass]) { 21 | return vc; 22 | } 23 | 24 | UIViewController* child = findViewController(vc, klass); 25 | 26 | if (child != nil) { 27 | return child; 28 | } 29 | } 30 | 31 | return nil; 32 | } 33 | 34 | static NTKFace* face; 35 | 36 | Constructor(SupportingHooksInit) { 37 | os_log(OS_LOG_DEFAULT, "CSTM: Magic starts here."); 38 | 39 | UIViewController* rootViewController = [[UIWindow keyWindow] rootViewController]; 40 | NTKFaceLibraryViewController* faceCollectionVC = findViewController(rootViewController, [NTKFaceLibraryViewController class]); 41 | 42 | if (faceCollectionVC == nil) { 43 | os_log(OS_LOG_DEFAULT, "CSTM: Unable to find collection VC."); 44 | 45 | return; 46 | } 47 | 48 | os_log(OS_LOG_DEFAULT, "CSTM: Found collection VC at %@.", faceCollectionVC); 49 | face = [[NTKCustomFace alloc] init]; 50 | 51 | NTKPersistentFaceCollection *faceCollection = GetIvar(faceCollectionVC, _addableFaceCollection); 52 | os_log(OS_LOG_DEFAULT, "CSTM: Will insert to the library: %@.", faceCollection); 53 | 54 | [faceCollection _addFace:face forUUID:[NTKCustomFace uuid] atIndex:0]; 55 | 56 | os_log(OS_LOG_DEFAULT, "CSTM: Inserted our face to the library."); 57 | 58 | /* 59 | Hook +[NTKFace _faceClassForStyle:] to return our custom face class when needed. 60 | */ 61 | HookClassMethod(Class, NTKFace, @selector(_faceClassForStyle:), ArgList(NTKFaceStyle faceStyle), { 62 | os_log(OS_LOG_DEFAULT, "CSTM: Requested our face!."); 63 | 64 | return (faceStyle == NTKFaceStyleCustom ? [NTKCustomFace class] : Orig(faceStyle)); 65 | }); 66 | 67 | /* 68 | Hook -[NTKFaceViewController loadView] to create our custom NTKFaceView subclass when needed. 69 | */ 70 | HookInstanceMethod(void, NTKFaceViewController, @selector(loadView), NoArguments, { 71 | Orig(); 72 | 73 | if (self.face.faceStyle == NTKFaceStyleCustom) { 74 | os_log(OS_LOG_DEFAULT, "CSTM: Loading our face!."); 75 | 76 | [self.faceView removeFromSuperview]; 77 | SetIvar(self, _faceView, nil); 78 | 79 | NTKCustomFaceView *customFaceView = [[NTKCustomFaceView alloc] init]; 80 | 81 | os_log(OS_LOG_DEFAULT, "CSTM: Frame set to: \(%@).", NSStringFromCGRect(customFaceView.frame)); 82 | 83 | [self.view addSubview:customFaceView]; 84 | self.view.backgroundColor = UIColor.redColor; 85 | SetIvar(self, _faceView, customFaceView); 86 | } 87 | }); 88 | } 89 | -------------------------------------------------------------------------------- /NTKCustomFace/NTKCustomFace.h: -------------------------------------------------------------------------------- 1 | // 2 | // NTKCustomFace.h 3 | // NTKCustomFace 4 | // 5 | // Created by Michał Kałużny on 13.10.18. 6 | // 7 | 8 | #import "NanoTimeKit.h" 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface NTKCustomFace : NTKFace 13 | 14 | + (NSUUID *)uuid; 15 | 16 | @end 17 | 18 | NS_ASSUME_NONNULL_END 19 | -------------------------------------------------------------------------------- /NTKCustomFace/NTKCustomFace.m: -------------------------------------------------------------------------------- 1 | // 2 | // NTKCustomFace.m 3 | // NTKCustomFace 4 | // 5 | // Created by Michał Kałużny on 13.10.18. 6 | // 7 | 8 | #import "NTKCustomFace.h" 9 | 10 | @implementation NTKCustomFace 11 | 12 | - (NSString *)name { 13 | return @"My Custom Face"; 14 | } 15 | 16 | - (NTKFaceStyle)faceStyle { 17 | return NTKFaceStyleCustom; 18 | } 19 | 20 | + (NSUUID *)uuid 21 | { 22 | static dispatch_once_t once; 23 | static NSUUID *uuid; 24 | dispatch_once(&once, ^{ 25 | uuid = [NSUUID UUID]; 26 | }); 27 | 28 | return uuid; 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /NTKCustomFace/NTKCustomFaceView.h: -------------------------------------------------------------------------------- 1 | // 2 | // NTKCustomFaceView.h 3 | // NTKCustomFace 4 | // 5 | // Created by Michał Kałużny on 13.10.18. 6 | // 7 | 8 | @import UIKit; 9 | 10 | #import "NanoTimeKit.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface NTKCustomFaceView : NTKDigitalFaceView 15 | 16 | @end 17 | 18 | NS_ASSUME_NONNULL_END 19 | -------------------------------------------------------------------------------- /NTKCustomFace/NTKCustomFaceView.m: -------------------------------------------------------------------------------- 1 | // 2 | // NTKCustomFaceView.m 3 | // NTKCustomFace 4 | // 5 | // Created by Michał Kałużny on 13.10.18. 6 | // 7 | 8 | @import SceneKit; 9 | @import WatchKit; 10 | 11 | #import "NTKCustomFaceView.h" 12 | 13 | @interface NTKCustomFaceView () 14 | 15 | @property (weak, nonatomic) UILabel *textLabel; 16 | @property (strong, nonatomic) NSTimer *timer; 17 | @property (strong, nonatomic) NSDateFormatter *dateFormatter; 18 | 19 | @end 20 | 21 | @implementation NTKCustomFaceView 22 | 23 | - (id)init 24 | { 25 | self = [super init]; 26 | 27 | if (self) { 28 | UILabel *label = [[UILabel alloc] init]; 29 | [self addSubview:label]; 30 | self.textLabel = label; 31 | 32 | [self setupUI]; 33 | 34 | self.dateFormatter = [[NSDateFormatter alloc] init]; 35 | self.dateFormatter.dateFormat = @"hh:mm:ss"; 36 | 37 | self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0/10.0 target:self selector:@selector(tick) userInfo:nil repeats:YES]; 38 | [self.timer fire]; 39 | } 40 | return self; 41 | } 42 | 43 | - (void)setupUI 44 | { 45 | self.textLabel.text = @"¯\\_(ツ)_/¯"; 46 | self.textLabel.textAlignment = NSTextAlignmentCenter; 47 | self.textLabel.textColor = UIColor.blackColor; 48 | self.textLabel.numberOfLines = 0; 49 | 50 | self.backgroundColor = UIColor.whiteColor; 51 | } 52 | 53 | - (void)tick 54 | { 55 | NSString *dateText = [NSString stringWithFormat:@"¯\\_(ツ)_/¯\n%@", [self.dateFormatter stringFromDate:[NSDate date]]]; 56 | self.textLabel.text = dateText; 57 | } 58 | 59 | - (void)layoutSubviews { 60 | self.textLabel.frame = self.bounds; 61 | } 62 | 63 | - (NTKFaceStyle)faceStyle { 64 | return NTKFaceStyleCustom; 65 | } 66 | 67 | @end 68 | -------------------------------------------------------------------------------- /NTKCustomFace/NanoTimeKit.h: -------------------------------------------------------------------------------- 1 | 2 | @import UIKit; 3 | 4 | typedef enum : NSUInteger { 5 | NTKFaceStyleCustom = 0xBAD1DEA 6 | } NTKFaceStyle; 7 | 8 | @interface UIView: NSObject 9 | 10 | @property (readwrite, nonatomic) UIColor *backgroundColor; 11 | @property (readwrite, nonatomic) CGRect frame; 12 | @property (readwrite, nonatomic) CGRect bounds; 13 | 14 | - (void)addSubview:(UIView *)subview; 15 | - (void)removeFromSuperview; 16 | 17 | @end 18 | 19 | @interface UILabel: UIView 20 | 21 | @property (readwrite, nonatomic) NSString *text; 22 | @property (readwrite, nonatomic) NSTextAlignment textAlignment; 23 | @property (readwrite, nonatomic) UIColor *textColor; 24 | @property (readwrite, nonatomic) NSInteger numberOfLines; 25 | 26 | 27 | @end 28 | 29 | @interface UIViewController: NSObject 30 | 31 | @property (readwrite, nonatomic) UIView *view; 32 | 33 | - (NSArray *)childViewControllers; 34 | 35 | @end 36 | 37 | @interface UIWindow : NSObject 38 | 39 | @property (readwrite, nonatomic) UIViewController* rootViewController; 40 | 41 | + (UIWindow *)keyWindow; 42 | 43 | @end 44 | 45 | @interface NTKFace : NSObject 46 | 47 | @property (readwrite, nonatomic) BOOL isEditable; 48 | @property (readwrite, nonatomic) NTKFaceStyle faceStyle; 49 | @property (readwrite, nonatomic) NSString *name; 50 | 51 | @end 52 | 53 | @interface NTKFaceView: UIView 54 | 55 | @property (readwrite, nonatomic) NTKFaceStyle faceStyle; 56 | 57 | @end 58 | 59 | @interface NTKDigitalFaceView: NTKFaceView 60 | 61 | @end 62 | 63 | @interface NTKFaceCollection: NSObject 64 | 65 | - (NSDictionary *)facesByUUID; 66 | 67 | @end 68 | 69 | @interface NTKFaceViewController : UIViewController 70 | 71 | @property (readonly, nonatomic) NTKFace *face; 72 | @property (readonly, nonatomic) NTKFaceView *faceView; 73 | 74 | @end 75 | 76 | @interface NTKPersistentFaceCollection: NSObject 77 | 78 | - (void)_addFace:(NTKFace *)face forUUID:(NSUUID *)uuid atIndex:(unsigned int)index; 79 | 80 | @end 81 | 82 | @interface NTKFaceLibraryViewController: UIViewController 83 | @end 84 | -------------------------------------------------------------------------------- /Private Frameworks/PUT_PRIVATE_FRAMEWORKS_HERE: -------------------------------------------------------------------------------- 1 | Put NanoTimeKit.framwork and RelevanceEngine.framwork in this directory. 2 | 3 | You can find them inside Xcode bundle: 4 | /Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NTKCustomFace 2 | 3 | >Your scientists were so preoccupied with whether or not they could, they didn’t stop to think if they should. 4 | > 5 | > Jeff Goldblum 6 | 7 | This project is a result of a weekend long research into the inner workings of `NanoTimeKit` framework and `Carousel` process inspired by [@steventroughtonsmith](https://github.com/steventroughtonsmith) work on his [SpriteKitWatchFace](https://github.com/steventroughtonsmith/SpriteKitWatchFace) project. 8 | 9 | It is heavily based on a similar (but outdated) [project](https://github.com/hamzasood/CustomWatchFaceTest) by [@hamzasood](https://github.com/hamzasood). 10 | 11 | # How Does this work? 12 | 13 | By hijacking the `_addableFaceCollection` property of the `NTKFaceLibraryViewController` we are able to directly modify the dictionary to also include our custom subclass of the `NTKFace`. 14 | 15 | This adds the custom face to the list of available ones, and then by swizzling the `loadView` on the `NTKFaceViewController` we can replace the standard view with our custom one. 16 | 17 | This is obviously nowhere close to how the API should be used and further research is needed to fully understand the process. 18 | 19 | # Setup: 20 | 21 | Before you will be able to compile the project you will have to move 2 private frameworks from the watchOS Simulator to the `Private Frameworks` directory. 22 | 23 | Frameworks in questions are: `NanoTimeKit` and `RelevanceEngine`, you can find them inside Xcode bundle: 24 | 25 | `/Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/` 26 | 27 | The project also depends on a watchOS Simulator with a name `Apple Watch Series 4 - 44mm` being present. This is the standard name that Xcode uses, but if you have renamed your simulated devices, you will have to modify the `launcher.sh` file (look for the `SIMULATOR_NAME` constant). 28 | 29 | # Running: 30 | 31 | In the ideal world all you should have to do is open Xcode project and hit `⌘+R`. This should compile the project, boot up the watchOS Simulator, restart the `Carousel` process and in the end inject the compiled library using some `lldb` magic. 32 | 33 | # Limitations: 34 | 35 | * Currently all of the `NanoTimeKit` and `UIKit` are forward declared in the `NanoTimeKit.h` file. This is not the ideal solution, so another way of importing those headers needs to be found. 36 | * Since we're compiling an library, assets like images are not properly supported. 37 | * This obviously doesn't work on actual hardware. 38 | 39 | # Further research: 40 | 41 | * Support for customization through official UI 42 | * Support for complications 43 | * Proper way of injecting the watch faces instead of directly modyfing the presenation model. 44 | * Better way of linking with `NanoTimeKit` and `UIKit` so all classes are available for use. 45 | -------------------------------------------------------------------------------- /launcher.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SIMULATOR_NAME="Apple Watch Series 4 - 44mm" 4 | 5 | # Launch Apple Watch Simulator 6 | echo "Starting Apple Watch Simulator" 7 | xcrun simctl boot "$SIMULATOR_NAME" >/dev/null 2>&1 8 | echo "Apple Watch Simulator succesfully started" 9 | 10 | # Make sure we attach to the fresh Carousel process. 11 | echo "Restarting Carousel Process" 12 | while true; do 13 | killall Carousel 2>/dev/null; 14 | if [ $? -eq 0 ]; then 15 | continue 16 | else 17 | echo "Carousel has been killed." 18 | break 19 | fi 20 | done 21 | 22 | # And now make sure that Carousel has finished waking up. 23 | while true; do 24 | PID=`pgrep -x "Carousel"` 25 | if [ -z $PID ]; then 26 | continue 27 | else 28 | echo "Carousel is running again." 29 | break 30 | fi 31 | done 32 | 33 | # Let's wait a while for the Carousel process to fully launch 34 | sleep 5 35 | 36 | # Attach & Inject 37 | echo "Attaching and injecting our payload." 38 | lldb -o 'command script import "lldb.py"' >/dev/null 2>&1 39 | echo "Successfully injected custom watch face." 40 | open -a Simulator 41 | -------------------------------------------------------------------------------- /lldb.py: -------------------------------------------------------------------------------- 1 | import lldb 2 | import os 3 | 4 | CURRENT_PATH = os.getcwd() 5 | PROCESS_NAME = "Carousel" 6 | PAYLOAD_NAME = "NTKCustomFace.dylib" 7 | PAYLOAD_PATH = "{}/build/{}".format(CURRENT_PATH, PAYLOAD_NAME) 8 | PAYLOAD_LOAD_EXPR = "(int)dlopen(\"{}\")".format(PAYLOAD_PATH) 9 | 10 | def __lldb_init_module(debugger, dict): 11 | target = debugger.CreateTarget('') 12 | error = SBError() 13 | 14 | process = target.AttachToProcessWithName(debugger.GetListener(), PROCESS_NAME, False, error) 15 | 16 | thread = process.GetSelectedThread() 17 | frame = thread.GetSelectedFrame() 18 | 19 | result = frame.EvaluateExpression(PAYLOAD_LOAD_EXPR) 20 | print(result) 21 | process.Detach() 22 | os._exit(1) 23 | return 24 | --------------------------------------------------------------------------------