├── README.md ├── TopNotch.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── shg.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── TopNotch ├── TopNotch.docc │ ├── Resources │ │ ├── banner.png │ │ ├── demo.mp4 │ │ └── poster.jpg │ └── TopNotch.md ├── TopNotch.h ├── TopNotchConfiguration.swift ├── TopNotchManager.swift ├── TopNotchModifier.swift └── UIDevice+Model.swift └── TopNotchDemo ├── AppDelegate.swift ├── Assets.xcassets ├── AccentColor.colorset │ └── Contents.json ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── FontCollectionViewController.swift ├── Info.plist ├── ModalSheetViewController.swift └── SceneDelegate.swift /README.md: -------------------------------------------------------------------------------- 1 | ./TopNotch/TopNotch.docc/TopNotch.md -------------------------------------------------------------------------------- /TopNotch.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | EAB4C7DE2D5AAA26000CE23F /* TopNotch.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAB4C7AF2D5AA7EB000CE23F /* TopNotch.framework */; }; 11 | EAB4C7DF2D5AAA26000CE23F /* TopNotch.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EAB4C7AF2D5AA7EB000CE23F /* TopNotch.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXContainerItemProxy section */ 15 | EAB4C7E02D5AAA26000CE23F /* PBXContainerItemProxy */ = { 16 | isa = PBXContainerItemProxy; 17 | containerPortal = EAB4C7A62D5AA7EB000CE23F /* Project object */; 18 | proxyType = 1; 19 | remoteGlobalIDString = EAB4C7AE2D5AA7EB000CE23F; 20 | remoteInfo = TopNotch; 21 | }; 22 | /* End PBXContainerItemProxy section */ 23 | 24 | /* Begin PBXCopyFilesBuildPhase section */ 25 | EAB4C7E22D5AAA26000CE23F /* Embed Frameworks */ = { 26 | isa = PBXCopyFilesBuildPhase; 27 | buildActionMask = 2147483647; 28 | dstPath = ""; 29 | dstSubfolderSpec = 10; 30 | files = ( 31 | EAB4C7DF2D5AAA26000CE23F /* TopNotch.framework in Embed Frameworks */, 32 | ); 33 | name = "Embed Frameworks"; 34 | runOnlyForDeploymentPostprocessing = 0; 35 | }; 36 | /* End PBXCopyFilesBuildPhase section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | EAB4C7AF2D5AA7EB000CE23F /* TopNotch.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TopNotch.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | EAB4C7C82D5AA990000CE23F /* TopNotchDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TopNotchDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | /* End PBXFileReference section */ 42 | 43 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 44 | EAB4C7B62D5AA7EB000CE23F /* Exceptions for "TopNotch" folder in "TopNotch" target */ = { 45 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 46 | publicHeaders = ( 47 | TopNotch.h, 48 | ); 49 | target = EAB4C7AE2D5AA7EB000CE23F /* TopNotch */; 50 | }; 51 | EAB4C7D92D5AA992000CE23F /* Exceptions for "TopNotchDemo" folder in "TopNotchDemo" target */ = { 52 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 53 | membershipExceptions = ( 54 | Info.plist, 55 | ); 56 | target = EAB4C7C72D5AA990000CE23F /* TopNotchDemo */; 57 | }; 58 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ 59 | 60 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 61 | EAB4C7B12D5AA7EB000CE23F /* TopNotch */ = { 62 | isa = PBXFileSystemSynchronizedRootGroup; 63 | exceptions = ( 64 | EAB4C7B62D5AA7EB000CE23F /* Exceptions for "TopNotch" folder in "TopNotch" target */, 65 | ); 66 | path = TopNotch; 67 | sourceTree = ""; 68 | }; 69 | EAB4C7C92D5AA990000CE23F /* TopNotchDemo */ = { 70 | isa = PBXFileSystemSynchronizedRootGroup; 71 | exceptions = ( 72 | EAB4C7D92D5AA992000CE23F /* Exceptions for "TopNotchDemo" folder in "TopNotchDemo" target */, 73 | ); 74 | path = TopNotchDemo; 75 | sourceTree = ""; 76 | }; 77 | /* End PBXFileSystemSynchronizedRootGroup section */ 78 | 79 | /* Begin PBXFrameworksBuildPhase section */ 80 | EAB4C7AC2D5AA7EB000CE23F /* Frameworks */ = { 81 | isa = PBXFrameworksBuildPhase; 82 | buildActionMask = 2147483647; 83 | files = ( 84 | ); 85 | runOnlyForDeploymentPostprocessing = 0; 86 | }; 87 | EAB4C7C52D5AA990000CE23F /* Frameworks */ = { 88 | isa = PBXFrameworksBuildPhase; 89 | buildActionMask = 2147483647; 90 | files = ( 91 | EAB4C7DE2D5AAA26000CE23F /* TopNotch.framework in Frameworks */, 92 | ); 93 | runOnlyForDeploymentPostprocessing = 0; 94 | }; 95 | /* End PBXFrameworksBuildPhase section */ 96 | 97 | /* Begin PBXGroup section */ 98 | EAB4C7A52D5AA7EB000CE23F = { 99 | isa = PBXGroup; 100 | children = ( 101 | EAB4C7B12D5AA7EB000CE23F /* TopNotch */, 102 | EAB4C7C92D5AA990000CE23F /* TopNotchDemo */, 103 | EAB4C7DD2D5AAA26000CE23F /* Frameworks */, 104 | EAB4C7B02D5AA7EB000CE23F /* Products */, 105 | ); 106 | sourceTree = ""; 107 | }; 108 | EAB4C7B02D5AA7EB000CE23F /* Products */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | EAB4C7AF2D5AA7EB000CE23F /* TopNotch.framework */, 112 | EAB4C7C82D5AA990000CE23F /* TopNotchDemo.app */, 113 | ); 114 | name = Products; 115 | sourceTree = ""; 116 | }; 117 | EAB4C7DD2D5AAA26000CE23F /* Frameworks */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | ); 121 | name = Frameworks; 122 | sourceTree = ""; 123 | }; 124 | /* End PBXGroup section */ 125 | 126 | /* Begin PBXHeadersBuildPhase section */ 127 | EAB4C7AA2D5AA7EB000CE23F /* Headers */ = { 128 | isa = PBXHeadersBuildPhase; 129 | buildActionMask = 2147483647; 130 | files = ( 131 | ); 132 | runOnlyForDeploymentPostprocessing = 0; 133 | }; 134 | /* End PBXHeadersBuildPhase section */ 135 | 136 | /* Begin PBXNativeTarget section */ 137 | EAB4C7AE2D5AA7EB000CE23F /* TopNotch */ = { 138 | isa = PBXNativeTarget; 139 | buildConfigurationList = EAB4C7B72D5AA7EB000CE23F /* Build configuration list for PBXNativeTarget "TopNotch" */; 140 | buildPhases = ( 141 | EAB4C7AA2D5AA7EB000CE23F /* Headers */, 142 | EAB4C7AB2D5AA7EB000CE23F /* Sources */, 143 | EAB4C7AC2D5AA7EB000CE23F /* Frameworks */, 144 | EAB4C7AD2D5AA7EB000CE23F /* Resources */, 145 | ); 146 | buildRules = ( 147 | ); 148 | dependencies = ( 149 | ); 150 | fileSystemSynchronizedGroups = ( 151 | EAB4C7B12D5AA7EB000CE23F /* TopNotch */, 152 | ); 153 | name = TopNotch; 154 | packageProductDependencies = ( 155 | ); 156 | productName = TopNotch; 157 | productReference = EAB4C7AF2D5AA7EB000CE23F /* TopNotch.framework */; 158 | productType = "com.apple.product-type.framework"; 159 | }; 160 | EAB4C7C72D5AA990000CE23F /* TopNotchDemo */ = { 161 | isa = PBXNativeTarget; 162 | buildConfigurationList = EAB4C7DA2D5AA992000CE23F /* Build configuration list for PBXNativeTarget "TopNotchDemo" */; 163 | buildPhases = ( 164 | EAB4C7C42D5AA990000CE23F /* Sources */, 165 | EAB4C7C52D5AA990000CE23F /* Frameworks */, 166 | EAB4C7C62D5AA990000CE23F /* Resources */, 167 | EAB4C7E22D5AAA26000CE23F /* Embed Frameworks */, 168 | ); 169 | buildRules = ( 170 | ); 171 | dependencies = ( 172 | EAB4C7E12D5AAA26000CE23F /* PBXTargetDependency */, 173 | ); 174 | fileSystemSynchronizedGroups = ( 175 | EAB4C7C92D5AA990000CE23F /* TopNotchDemo */, 176 | ); 177 | name = TopNotchDemo; 178 | packageProductDependencies = ( 179 | ); 180 | productName = TopNotchDemo; 181 | productReference = EAB4C7C82D5AA990000CE23F /* TopNotchDemo.app */; 182 | productType = "com.apple.product-type.application"; 183 | }; 184 | /* End PBXNativeTarget section */ 185 | 186 | /* Begin PBXProject section */ 187 | EAB4C7A62D5AA7EB000CE23F /* Project object */ = { 188 | isa = PBXProject; 189 | attributes = { 190 | BuildIndependentTargetsInParallel = 1; 191 | LastSwiftUpdateCheck = 1610; 192 | LastUpgradeCheck = 1610; 193 | TargetAttributes = { 194 | EAB4C7AE2D5AA7EB000CE23F = { 195 | CreatedOnToolsVersion = 16.1; 196 | }; 197 | EAB4C7C72D5AA990000CE23F = { 198 | CreatedOnToolsVersion = 16.1; 199 | }; 200 | }; 201 | }; 202 | buildConfigurationList = EAB4C7A92D5AA7EB000CE23F /* Build configuration list for PBXProject "TopNotch" */; 203 | developmentRegion = en; 204 | hasScannedForEncodings = 0; 205 | knownRegions = ( 206 | en, 207 | Base, 208 | ); 209 | mainGroup = EAB4C7A52D5AA7EB000CE23F; 210 | minimizedProjectReferenceProxies = 1; 211 | preferredProjectObjectVersion = 77; 212 | productRefGroup = EAB4C7B02D5AA7EB000CE23F /* Products */; 213 | projectDirPath = ""; 214 | projectRoot = ""; 215 | targets = ( 216 | EAB4C7AE2D5AA7EB000CE23F /* TopNotch */, 217 | EAB4C7C72D5AA990000CE23F /* TopNotchDemo */, 218 | ); 219 | }; 220 | /* End PBXProject section */ 221 | 222 | /* Begin PBXResourcesBuildPhase section */ 223 | EAB4C7AD2D5AA7EB000CE23F /* Resources */ = { 224 | isa = PBXResourcesBuildPhase; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | ); 228 | runOnlyForDeploymentPostprocessing = 0; 229 | }; 230 | EAB4C7C62D5AA990000CE23F /* Resources */ = { 231 | isa = PBXResourcesBuildPhase; 232 | buildActionMask = 2147483647; 233 | files = ( 234 | ); 235 | runOnlyForDeploymentPostprocessing = 0; 236 | }; 237 | /* End PBXResourcesBuildPhase section */ 238 | 239 | /* Begin PBXSourcesBuildPhase section */ 240 | EAB4C7AB2D5AA7EB000CE23F /* Sources */ = { 241 | isa = PBXSourcesBuildPhase; 242 | buildActionMask = 2147483647; 243 | files = ( 244 | ); 245 | runOnlyForDeploymentPostprocessing = 0; 246 | }; 247 | EAB4C7C42D5AA990000CE23F /* Sources */ = { 248 | isa = PBXSourcesBuildPhase; 249 | buildActionMask = 2147483647; 250 | files = ( 251 | ); 252 | runOnlyForDeploymentPostprocessing = 0; 253 | }; 254 | /* End PBXSourcesBuildPhase section */ 255 | 256 | /* Begin PBXTargetDependency section */ 257 | EAB4C7E12D5AAA26000CE23F /* PBXTargetDependency */ = { 258 | isa = PBXTargetDependency; 259 | target = EAB4C7AE2D5AA7EB000CE23F /* TopNotch */; 260 | targetProxy = EAB4C7E02D5AAA26000CE23F /* PBXContainerItemProxy */; 261 | }; 262 | /* End PBXTargetDependency section */ 263 | 264 | /* Begin XCBuildConfiguration section */ 265 | EAB4C7B82D5AA7EB000CE23F /* Debug */ = { 266 | isa = XCBuildConfiguration; 267 | buildSettings = { 268 | BUILD_LIBRARY_FOR_DISTRIBUTION = YES; 269 | CODE_SIGN_STYLE = Automatic; 270 | CURRENT_PROJECT_VERSION = 1; 271 | DEFINES_MODULE = YES; 272 | DEVELOPMENT_TEAM = CG56CG5WCQ; 273 | DYLIB_COMPATIBILITY_VERSION = 1; 274 | DYLIB_CURRENT_VERSION = 1; 275 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 276 | ENABLE_MODULE_VERIFIER = YES; 277 | GENERATE_INFOPLIST_FILE = YES; 278 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 279 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 280 | IPHONEOS_DEPLOYMENT_TARGET = 15.6; 281 | LD_RUNPATH_SEARCH_PATHS = ( 282 | "$(inherited)", 283 | "@executable_path/Frameworks", 284 | "@loader_path/Frameworks", 285 | ); 286 | MARKETING_VERSION = 1.0; 287 | MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; 288 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; 289 | PRODUCT_BUNDLE_IDENTIFIER = gold.samhenri.TopNotch; 290 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 291 | SKIP_INSTALL = YES; 292 | SWIFT_EMIT_LOC_STRINGS = YES; 293 | SWIFT_INSTALL_OBJC_HEADER = NO; 294 | SWIFT_PACKAGE_NAME = "$(TARGET_NAME:c99extidentifier)"; 295 | SWIFT_VERSION = 5.0; 296 | TARGETED_DEVICE_FAMILY = "1,2"; 297 | }; 298 | name = Debug; 299 | }; 300 | EAB4C7B92D5AA7EB000CE23F /* Release */ = { 301 | isa = XCBuildConfiguration; 302 | buildSettings = { 303 | BUILD_LIBRARY_FOR_DISTRIBUTION = YES; 304 | CODE_SIGN_STYLE = Automatic; 305 | CURRENT_PROJECT_VERSION = 1; 306 | DEFINES_MODULE = YES; 307 | DEVELOPMENT_TEAM = CG56CG5WCQ; 308 | DYLIB_COMPATIBILITY_VERSION = 1; 309 | DYLIB_CURRENT_VERSION = 1; 310 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 311 | ENABLE_MODULE_VERIFIER = YES; 312 | GENERATE_INFOPLIST_FILE = YES; 313 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 314 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 315 | IPHONEOS_DEPLOYMENT_TARGET = 15.6; 316 | LD_RUNPATH_SEARCH_PATHS = ( 317 | "$(inherited)", 318 | "@executable_path/Frameworks", 319 | "@loader_path/Frameworks", 320 | ); 321 | MARKETING_VERSION = 1.0; 322 | MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; 323 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; 324 | PRODUCT_BUNDLE_IDENTIFIER = gold.samhenri.TopNotch; 325 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 326 | SKIP_INSTALL = YES; 327 | SWIFT_EMIT_LOC_STRINGS = YES; 328 | SWIFT_INSTALL_OBJC_HEADER = NO; 329 | SWIFT_PACKAGE_NAME = "$(TARGET_NAME:c99extidentifier)"; 330 | SWIFT_VERSION = 5.0; 331 | TARGETED_DEVICE_FAMILY = "1,2"; 332 | }; 333 | name = Release; 334 | }; 335 | EAB4C7BA2D5AA7EB000CE23F /* Debug */ = { 336 | isa = XCBuildConfiguration; 337 | buildSettings = { 338 | ALWAYS_SEARCH_USER_PATHS = NO; 339 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 340 | CLANG_ANALYZER_NONNULL = YES; 341 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 342 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 343 | CLANG_ENABLE_MODULES = YES; 344 | CLANG_ENABLE_OBJC_ARC = YES; 345 | CLANG_ENABLE_OBJC_WEAK = YES; 346 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 347 | CLANG_WARN_BOOL_CONVERSION = YES; 348 | CLANG_WARN_COMMA = YES; 349 | CLANG_WARN_CONSTANT_CONVERSION = YES; 350 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 351 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 352 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 353 | CLANG_WARN_EMPTY_BODY = YES; 354 | CLANG_WARN_ENUM_CONVERSION = YES; 355 | CLANG_WARN_INFINITE_RECURSION = YES; 356 | CLANG_WARN_INT_CONVERSION = YES; 357 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 358 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 359 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 360 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 361 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 362 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 363 | CLANG_WARN_STRICT_PROTOTYPES = YES; 364 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 365 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 366 | CLANG_WARN_UNREACHABLE_CODE = YES; 367 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 368 | COPY_PHASE_STRIP = NO; 369 | CURRENT_PROJECT_VERSION = 1; 370 | DEBUG_INFORMATION_FORMAT = dwarf; 371 | ENABLE_STRICT_OBJC_MSGSEND = YES; 372 | ENABLE_TESTABILITY = YES; 373 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 374 | GCC_C_LANGUAGE_STANDARD = gnu17; 375 | GCC_DYNAMIC_NO_PIC = NO; 376 | GCC_NO_COMMON_BLOCKS = YES; 377 | GCC_OPTIMIZATION_LEVEL = 0; 378 | GCC_PREPROCESSOR_DEFINITIONS = ( 379 | "DEBUG=1", 380 | "$(inherited)", 381 | ); 382 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 383 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 384 | GCC_WARN_UNDECLARED_SELECTOR = YES; 385 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 386 | GCC_WARN_UNUSED_FUNCTION = YES; 387 | GCC_WARN_UNUSED_VARIABLE = YES; 388 | IPHONEOS_DEPLOYMENT_TARGET = 18.1; 389 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 390 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 391 | MTL_FAST_MATH = YES; 392 | ONLY_ACTIVE_ARCH = YES; 393 | SDKROOT = iphoneos; 394 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 395 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 396 | VERSIONING_SYSTEM = "apple-generic"; 397 | VERSION_INFO_PREFIX = ""; 398 | }; 399 | name = Debug; 400 | }; 401 | EAB4C7BB2D5AA7EB000CE23F /* Release */ = { 402 | isa = XCBuildConfiguration; 403 | buildSettings = { 404 | ALWAYS_SEARCH_USER_PATHS = NO; 405 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 406 | CLANG_ANALYZER_NONNULL = YES; 407 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 408 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 409 | CLANG_ENABLE_MODULES = YES; 410 | CLANG_ENABLE_OBJC_ARC = YES; 411 | CLANG_ENABLE_OBJC_WEAK = YES; 412 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 413 | CLANG_WARN_BOOL_CONVERSION = YES; 414 | CLANG_WARN_COMMA = YES; 415 | CLANG_WARN_CONSTANT_CONVERSION = YES; 416 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 417 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 418 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 419 | CLANG_WARN_EMPTY_BODY = YES; 420 | CLANG_WARN_ENUM_CONVERSION = YES; 421 | CLANG_WARN_INFINITE_RECURSION = YES; 422 | CLANG_WARN_INT_CONVERSION = YES; 423 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 424 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 425 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 426 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 427 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 428 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 429 | CLANG_WARN_STRICT_PROTOTYPES = YES; 430 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 431 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 432 | CLANG_WARN_UNREACHABLE_CODE = YES; 433 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 434 | COPY_PHASE_STRIP = NO; 435 | CURRENT_PROJECT_VERSION = 1; 436 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 437 | ENABLE_NS_ASSERTIONS = NO; 438 | ENABLE_STRICT_OBJC_MSGSEND = YES; 439 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 440 | GCC_C_LANGUAGE_STANDARD = gnu17; 441 | GCC_NO_COMMON_BLOCKS = YES; 442 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 443 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 444 | GCC_WARN_UNDECLARED_SELECTOR = YES; 445 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 446 | GCC_WARN_UNUSED_FUNCTION = YES; 447 | GCC_WARN_UNUSED_VARIABLE = YES; 448 | IPHONEOS_DEPLOYMENT_TARGET = 18.1; 449 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 450 | MTL_ENABLE_DEBUG_INFO = NO; 451 | MTL_FAST_MATH = YES; 452 | SDKROOT = iphoneos; 453 | SWIFT_COMPILATION_MODE = wholemodule; 454 | VALIDATE_PRODUCT = YES; 455 | VERSIONING_SYSTEM = "apple-generic"; 456 | VERSION_INFO_PREFIX = ""; 457 | }; 458 | name = Release; 459 | }; 460 | EAB4C7DB2D5AA992000CE23F /* Debug */ = { 461 | isa = XCBuildConfiguration; 462 | buildSettings = { 463 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 464 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 465 | CODE_SIGN_STYLE = Automatic; 466 | CURRENT_PROJECT_VERSION = 1; 467 | DEVELOPMENT_TEAM = CG56CG5WCQ; 468 | GENERATE_INFOPLIST_FILE = YES; 469 | INFOPLIST_FILE = TopNotchDemo/Info.plist; 470 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 471 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 472 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 473 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 474 | LD_RUNPATH_SEARCH_PATHS = ( 475 | "$(inherited)", 476 | "@executable_path/Frameworks", 477 | ); 478 | MARKETING_VERSION = 1.0; 479 | PRODUCT_BUNDLE_IDENTIFIER = gold.samhenri.TopNotchDemo; 480 | PRODUCT_NAME = "$(TARGET_NAME)"; 481 | SWIFT_EMIT_LOC_STRINGS = YES; 482 | SWIFT_VERSION = 5.0; 483 | TARGETED_DEVICE_FAMILY = "1,2"; 484 | }; 485 | name = Debug; 486 | }; 487 | EAB4C7DC2D5AA992000CE23F /* Release */ = { 488 | isa = XCBuildConfiguration; 489 | buildSettings = { 490 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 491 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 492 | CODE_SIGN_STYLE = Automatic; 493 | CURRENT_PROJECT_VERSION = 1; 494 | DEVELOPMENT_TEAM = CG56CG5WCQ; 495 | GENERATE_INFOPLIST_FILE = YES; 496 | INFOPLIST_FILE = TopNotchDemo/Info.plist; 497 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 498 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 499 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 500 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 501 | LD_RUNPATH_SEARCH_PATHS = ( 502 | "$(inherited)", 503 | "@executable_path/Frameworks", 504 | ); 505 | MARKETING_VERSION = 1.0; 506 | PRODUCT_BUNDLE_IDENTIFIER = gold.samhenri.TopNotchDemo; 507 | PRODUCT_NAME = "$(TARGET_NAME)"; 508 | SWIFT_EMIT_LOC_STRINGS = YES; 509 | SWIFT_VERSION = 5.0; 510 | TARGETED_DEVICE_FAMILY = "1,2"; 511 | }; 512 | name = Release; 513 | }; 514 | /* End XCBuildConfiguration section */ 515 | 516 | /* Begin XCConfigurationList section */ 517 | EAB4C7A92D5AA7EB000CE23F /* Build configuration list for PBXProject "TopNotch" */ = { 518 | isa = XCConfigurationList; 519 | buildConfigurations = ( 520 | EAB4C7BA2D5AA7EB000CE23F /* Debug */, 521 | EAB4C7BB2D5AA7EB000CE23F /* Release */, 522 | ); 523 | defaultConfigurationIsVisible = 0; 524 | defaultConfigurationName = Release; 525 | }; 526 | EAB4C7B72D5AA7EB000CE23F /* Build configuration list for PBXNativeTarget "TopNotch" */ = { 527 | isa = XCConfigurationList; 528 | buildConfigurations = ( 529 | EAB4C7B82D5AA7EB000CE23F /* Debug */, 530 | EAB4C7B92D5AA7EB000CE23F /* Release */, 531 | ); 532 | defaultConfigurationIsVisible = 0; 533 | defaultConfigurationName = Release; 534 | }; 535 | EAB4C7DA2D5AA992000CE23F /* Build configuration list for PBXNativeTarget "TopNotchDemo" */ = { 536 | isa = XCConfigurationList; 537 | buildConfigurations = ( 538 | EAB4C7DB2D5AA992000CE23F /* Debug */, 539 | EAB4C7DC2D5AA992000CE23F /* Release */, 540 | ); 541 | defaultConfigurationIsVisible = 0; 542 | defaultConfigurationName = Release; 543 | }; 544 | /* End XCConfigurationList section */ 545 | }; 546 | rootObject = EAB4C7A62D5AA7EB000CE23F /* Project object */; 547 | } 548 | -------------------------------------------------------------------------------- /TopNotch.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TopNotch.xcodeproj/xcuserdata/shg.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | TopNotch.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | TopNotchDemo.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /TopNotch/TopNotch.docc/Resources/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samhenrigold/TopNotch/2bcd0795a65da55c9be47a93b6e05996d1b913a8/TopNotch/TopNotch.docc/Resources/banner.png -------------------------------------------------------------------------------- /TopNotch/TopNotch.docc/Resources/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samhenrigold/TopNotch/2bcd0795a65da55c9be47a93b6e05996d1b913a8/TopNotch/TopNotch.docc/Resources/demo.mp4 -------------------------------------------------------------------------------- /TopNotch/TopNotch.docc/Resources/poster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samhenrigold/TopNotch/2bcd0795a65da55c9be47a93b6e05996d1b913a8/TopNotch/TopNotch.docc/Resources/poster.jpg -------------------------------------------------------------------------------- /TopNotch/TopNotch.docc/TopNotch.md: -------------------------------------------------------------------------------- 1 | # ``TopNotch`` 2 | 3 | ![TopNotch. Hide a message under the notch. It'll be our little secret.](./TopNotch/TopNotch.docc/Resources/banner.png) 4 | 5 | ## Overview 6 | 7 | TopNotch is a lil Swift package that lets you **hide a custom view underneath the device’s notch**. Since it’ll only be visible in screenshots and screen recordings, you can have some fun. **Put a version string in there**, maybe use it as a branding moment to **stick your logo there**, write your darkest secrets, whatever your little heart desires. 8 | 9 | If you opt to actually write your little secrets there, you can set `shouldHideForTaskSwitcher` on `TopNotchConfiguration` to hide the view when `sceneWillDeactivateNotification` gets called. It'll come back after `sceneDidActivateNotification` is called or if you ask nicely. 10 | 11 | It automatically calculates the notch’s exclusion area (using an undocumented `_exclusionArea` property on UIScreen). Since it doesn't always return the right values for older notch styles, I'm applying some manual, device-specific adjustments to make sure it stays hidden on all devices. 12 | 13 | > [!WARNING] 14 | > Because TopNotch relies on undocumented APIs, it may not be App Store safe. Give it a shot though. I dare you. 15 | 16 | https://github.com/user-attachments/assets/61dfcdc5-868a-45d6-91f1-aa0898211357 17 | 18 | 19 | ## TODO 20 | - Reduce logging pollution 21 | - Make the SwiftUI adapter useful 22 | 23 | ## Usage 24 | 25 | I've attached a little demo project if that's how you roll. TL;DR: 26 | 27 | ```swift 28 | // Create a custom view (or use your own) to display behind the notch. 29 | let notchLabel = UILabel() 30 | notchLabel.text = "Hi Mom" 31 | notchLabel.textAlignment = .center 32 | notchLabel.textColor = .white 33 | notchLabel.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.7) 34 | 35 | // Show the notch view. 36 | TopNotchManager.shared.show(customView: notchLabel, with: TopNotchConfiguration(animationDuration: 0.3, 37 | shouldAnimate: true, 38 | shouldHideForTaskSwitcher: true)) 39 | 40 | // To hide it: 41 | TopNotchManager.shared.hide() 42 | ``` 43 | -------------------------------------------------------------------------------- /TopNotch/TopNotch.h: -------------------------------------------------------------------------------- 1 | // 2 | // TopNotch.h 3 | // TopNotch 4 | // 5 | // Created by Sam Gold on 2025-02-10. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for TopNotch. 11 | FOUNDATION_EXPORT double TopNotchVersionNumber; 12 | 13 | //! Project version string for TopNotch. 14 | FOUNDATION_EXPORT const unsigned char TopNotchVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /TopNotch/TopNotchConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TopNotchConfiguration.swift 3 | // TopNotch 4 | // 5 | // Created by Sam Gold on 2025-02-10. 6 | // 7 | 8 | import UIKit 9 | 10 | /// A configuration object for TopNotch behavior. 11 | public struct TopNotchConfiguration { 12 | /// The duration of the show/hide animation. 13 | public var animationDuration: TimeInterval 14 | /// Whether to animate frame changes. 15 | public var shouldAnimate: Bool 16 | /// If true, the watermark will hide when the app is about to show the entire screen (e.g. task switcher). 17 | public var shouldHideForTaskSwitcher: Bool 18 | 19 | public init(animationDuration: TimeInterval = 0.3, 20 | shouldAnimate: Bool = true, 21 | shouldHideForTaskSwitcher: Bool = true) { 22 | self.animationDuration = animationDuration 23 | self.shouldAnimate = shouldAnimate 24 | self.shouldHideForTaskSwitcher = shouldHideForTaskSwitcher 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /TopNotch/TopNotchManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TopNotchManager.swift 3 | // TopNotch 4 | // 5 | // Created by Sam Gold on 2025-02-10. 6 | // 7 | 8 | import UIKit 9 | 10 | /// `TopNotchManager` is responsible for displaying a view behind the device's notch. 11 | /// It retrieves the exclusion area via KVC, applies device-specific (or series-specific) 12 | /// adjustments, and presents the view in a dedicated window that remains fixed in portrait. 13 | public final class TopNotchManager { 14 | 15 | // MARK: Public API 16 | 17 | /// A shared instance of `TopNotchManager`. 18 | public static let shared = TopNotchManager() 19 | 20 | /// The computed exclusion area (the “notch”) retrieved via KVC. 21 | public static var exclusionRect: CGRect = { 22 | let screen = UIScreen.main 23 | // Shhhh it'll be our little secret. 24 | guard let exclusionAreaMethod = screen.value(forKey: "_" + "exclusion" + "Area") as? NSObject, 25 | let rect = exclusionAreaMethod.value(forKey: "rect") as? CGRect else { 26 | print("[TopNotchManager] Exclusion area not available; returning .zero") 27 | return .zero 28 | } 29 | // If you saw that and you work at Apple, no you didn't. 30 | print("[TopNotchManager] Retrieved exclusionRect: \(rect)") 31 | return rect 32 | }() 33 | 34 | /// Indicates whether the notch view is currently visible. 35 | public private(set) var isNotchVisible: Bool = false 36 | /// The exclusion area used when the notch view was shown. 37 | public private(set) var currentExclusionRect: CGRect = .zero 38 | /// A human-readable reason if the notch cannot be shown. 39 | public private(set) var cannotShowReason: String? = nil 40 | 41 | // MARK: Private Properties 42 | 43 | private var notchView: UIView? 44 | private var config: TopNotchConfiguration = TopNotchConfiguration() 45 | private var notchWindow: UIWindow? 46 | /// The raw exclusion rectangle stored when showing; not updated on rotation. 47 | private var storedExclusionRect: CGRect? 48 | 49 | // MARK: Model Overrides 50 | 51 | /// Individual model overrides can be specified here. 52 | /// For example: 53 | // private let modelOverrides: [String: (scale: CGFloat, heightFactor: CGFloat, radius: CGFloat)] = [ 54 | // "iPhoneXX": (scale: 0.80, heightFactor: 0.80, radius: 25) 55 | // ] 56 | private let modelOverrides: [String: (scale: CGFloat, heightFactor: CGFloat, radius: CGFloat)] = [:] 57 | 58 | /// Series overrides for devices. Any device whose model identifier begins with the key 59 | /// will use these settings. 60 | private let modelSeriesOverrides: [String: (scale: CGFloat, heightFactor: CGFloat, radius: CGFloat)] = [ 61 | "iPhone13": (scale: 0.95, heightFactor: 1.0, radius: 27), // iPhone 12 series 62 | "iPhone14": (scale: 0.75, heightFactor: 0.75, radius: 24) // iPhone 13/14 series 63 | ] 64 | 65 | // MARK: Orientation-Locking Container 66 | 67 | /// A container view controller that locks orientation to portrait. 68 | private final class NoRotationViewController: UIViewController { 69 | override var supportedInterfaceOrientations: UIInterfaceOrientationMask { 70 | return .portrait 71 | } 72 | 73 | override var shouldAutorotate: Bool { 74 | return false 75 | } 76 | 77 | override func viewDidLoad() { 78 | super.viewDidLoad() 79 | view.backgroundColor = .clear 80 | print("[NoRotationViewController] View did load") 81 | } 82 | } 83 | 84 | // MARK: Public Methods 85 | 86 | /// Displays the notch view using an optional custom view and configuration. 87 | /// 88 | /// - Parameters: 89 | /// - customView: A custom view to use for the notch. If `nil`, a default red‑tinted view is used. 90 | /// - configuration: A configuration object that controls animation duration and task switcher behavior. 91 | public func show(customView: UIView? = nil, 92 | with configuration: TopNotchConfiguration = TopNotchConfiguration()) { 93 | print("[TopNotchManager] Showing notch view") 94 | config = configuration 95 | 96 | // Retrieve the exclusion area. 97 | let exclusion = TopNotchManager.exclusionRect 98 | guard exclusion != .zero else { 99 | print("[TopNotchManager] Cannot show notch: No exclusion area detected.") 100 | cannotShowReason = "No exclusion area detected." 101 | hide() 102 | return 103 | } 104 | cannotShowReason = nil 105 | storedExclusionRect = exclusion 106 | currentExclusionRect = exclusion 107 | 108 | // Use the provided custom view or create a default one. 109 | if let custom = customView { 110 | print("[TopNotchManager] Using custom notch view") 111 | notchView = custom 112 | } else if notchView == nil { 113 | print("[TopNotchManager] Creating default notch view") 114 | notchView = createDefaultNotchView() 115 | } 116 | notchView?.isUserInteractionEnabled = false 117 | 118 | // Create (or update) the dedicated top-level window. 119 | attachNotchViewToWindow() 120 | 121 | // Set initial state and update the frame. 122 | notchView?.alpha = 0 123 | updateNotchFrame() 124 | 125 | // Animate fade-in. 126 | UIView.animate(withDuration: config.animationDuration) { 127 | self.notchView?.alpha = 1.0 128 | } 129 | print("[TopNotchManager] Notch view fading in") 130 | isNotchVisible = true 131 | 132 | // Register for orientation changes. 133 | NotificationCenter.default.addObserver(self, 134 | selector: #selector(updateNotchFrame), 135 | name: UIDevice.orientationDidChangeNotification, 136 | object: nil) 137 | // Optionally observe task switcher notifications. 138 | if config.shouldHideForTaskSwitcher { 139 | NotificationCenter.default.addObserver(self, 140 | selector: #selector(sceneWillDeactivateNotification(_:)), 141 | name: UIScene.willDeactivateNotification, 142 | object: nil) 143 | NotificationCenter.default.addObserver(self, 144 | selector: #selector(sceneDidActivateNotification(_:)), 145 | name: UIScene.didActivateNotification, 146 | object: nil) 147 | print("[TopNotchManager] Observing scene de/activation for task switcher hiding") 148 | } 149 | } 150 | 151 | /// Hides the notch view by fading it out and removing it. 152 | public func hide() { 153 | print("[TopNotchManager] Hiding notch view") 154 | NotificationCenter.default.removeObserver(self) 155 | UIView.animate(withDuration: config.animationDuration, animations: { 156 | self.notchView?.alpha = 0 157 | }) { _ in 158 | print("[TopNotchManager] Notch view removed from superview") 159 | self.notchView?.removeFromSuperview() 160 | self.isNotchVisible = false 161 | } 162 | } 163 | 164 | // MARK: Private Helpers 165 | 166 | /// Creates a default notch view with a red tint. 167 | /// 168 | /// - Returns: A default `UIView` instance to be used as the notch view. 169 | private func createDefaultNotchView() -> UIView { 170 | print("[TopNotchManager] Creating default notch view with red tint") 171 | let view = UIView() 172 | view.backgroundColor = UIColor.red.withAlphaComponent(0.5) 173 | return view 174 | } 175 | 176 | /// Creates (if needed) and attaches the notch view to a dedicated top-level window. 177 | private func attachNotchViewToWindow() { 178 | if notchWindow == nil { 179 | print("[TopNotchManager] Creating new UIWindow for notch view") 180 | if let windowScene = UIApplication.shared.connectedScenes 181 | .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { 182 | let window = UIWindow(windowScene: windowScene) 183 | window.frame = windowScene.coordinateSpace.bounds 184 | window.backgroundColor = .clear 185 | window.windowLevel = UIWindow.Level.statusBar + 100 186 | let containerVC = NoRotationViewController() 187 | window.rootViewController = containerVC 188 | window.isUserInteractionEnabled = false 189 | window.isHidden = false 190 | window.makeKeyAndVisible() 191 | notchWindow = window 192 | print("[TopNotchManager] UIWindow created and visible") 193 | } else { 194 | print("[TopNotchManager] Warning: No active window scene found") 195 | } 196 | } 197 | notchView?.removeFromSuperview() 198 | if let container = notchWindow?.rootViewController?.view, let view = notchView { 199 | container.addSubview(view) 200 | container.bringSubviewToFront(view) 201 | print("[TopNotchManager] Notch view attached to window") 202 | } 203 | } 204 | 205 | /// Updates the notch view’s frame using the stored exclusion rectangle and applies any model-specific adjustments. 206 | @objc public func updateNotchFrame() { 207 | guard let rawRect = storedExclusionRect, rawRect != .zero else { 208 | notchView?.frame = .zero 209 | print("[TopNotchManager] Notch frame set to .zero") 210 | return 211 | } 212 | 213 | let modelId = UIDevice.modelIdentifier 214 | var adjustedFrame = rawRect 215 | 216 | // First, check for individual model overrides. 217 | if let override = modelOverrides[modelId] { 218 | let newWidth = rawRect.width * override.scale 219 | let newX = rawRect.origin.x + (rawRect.width - newWidth) / 2 220 | let newHeight = rawRect.height * override.heightFactor 221 | adjustedFrame = CGRect(x: newX, y: rawRect.origin.y, width: newWidth, height: newHeight) 222 | print("[TopNotchManager] Applied individual override for \(modelId): \(adjustedFrame)") 223 | applyCornerStyling(with: override.radius, roundBottomOnly: true) 224 | } 225 | // Otherwise, check for series overrides. 226 | else if let seriesOverride = modelSeriesOverrides.first(where: { modelId.hasPrefix($0.key) }) { 227 | let override = seriesOverride.value 228 | let newWidth = rawRect.width * override.scale 229 | let newX = rawRect.origin.x + (rawRect.width - newWidth) / 2 230 | let newHeight = rawRect.height * override.heightFactor 231 | adjustedFrame = CGRect(x: newX, y: rawRect.origin.y, width: newWidth, height: newHeight) 232 | print("[TopNotchManager] Applied series override for \(modelId) (prefix \(seriesOverride.key)): \(adjustedFrame)") 233 | applyCornerStyling(with: override.radius, roundBottomOnly: true) 234 | } 235 | // Otherwise, use default styling. 236 | else { 237 | adjustedFrame = rawRect 238 | if rawRect.origin.y > 0 { 239 | let capsuleRadius = rawRect.height / 2 240 | applyCornerStyling(with: capsuleRadius) 241 | print("[TopNotchManager] Dynamic island default styling (radius \(capsuleRadius))") 242 | } else { 243 | // Use a slightly increased radius for original notches to prevent clipping. 244 | applyCornerStyling(with: 21, roundBottomOnly: true) 245 | print("[TopNotchManager] Original notch default styling (radius 21)") 246 | } 247 | } 248 | 249 | notchView?.frame = adjustedFrame 250 | print("[TopNotchManager] Updated notch view frame to: \(adjustedFrame)") 251 | } 252 | 253 | /// Applies corner styling using a CAShapeLayer mask so that the rounded corners are drawn fully. 254 | /// 255 | /// - Parameters: 256 | /// - radius: The corner radius to apply. 257 | /// - roundBottomOnly: If `true`, only the bottom corners will be rounded. 258 | private func applyCornerStyling(with radius: CGFloat, roundBottomOnly: Bool = false) { 259 | guard let view = notchView else { return } 260 | // Disable default clipping. 261 | view.clipsToBounds = false 262 | 263 | let path: UIBezierPath 264 | if roundBottomOnly { 265 | path = UIBezierPath(roundedRect: view.bounds, 266 | byRoundingCorners: [.bottomLeft, .bottomRight], 267 | cornerRadii: CGSize(width: radius, height: radius)) 268 | } else { 269 | path = UIBezierPath(roundedRect: view.bounds, cornerRadius: radius) 270 | } 271 | 272 | let maskLayer = CAShapeLayer() 273 | maskLayer.frame = view.bounds 274 | maskLayer.path = path.cgPath 275 | view.layer.mask = maskLayer 276 | } 277 | 278 | // MARK: Task Switcher Notifications 279 | 280 | /// Called when the scene is about to deactivate (e.g. when the task switcher is invoked). 281 | @objc private func sceneWillDeactivateNotification(_ notification: Notification) { 282 | if config.shouldHideForTaskSwitcher && isNotchVisible { 283 | print("[TopNotchManager] Hiding notch view for task switcher (willDeactivate)") 284 | UIView.animate(withDuration: config.animationDuration) { 285 | self.notchView?.alpha = 0 286 | } 287 | } 288 | } 289 | 290 | /// Called when the scene did activate (e.g. when the task switcher is dismissed). 291 | @objc private func sceneDidActivateNotification(_ notification: Notification) { 292 | if config.shouldHideForTaskSwitcher && isNotchVisible { 293 | print("[TopNotchManager] Showing notch view after task switcher (didActivate)") 294 | UIView.animate(withDuration: config.animationDuration) { 295 | self.notchView?.alpha = 1.0 296 | } 297 | } 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /TopNotch/TopNotchModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TopNotchModifier.swift 3 | // TopNotch 4 | // 5 | // Created by Sam Gold on 2025-02-10. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A view modifier that adds the TopNotch watermark to a SwiftUI view. 11 | @available(iOS 13.0, *) 12 | public struct TopNotchModifier: ViewModifier { 13 | public func body(content: Content) -> some View { 14 | content.background(TopNotchRepresentable()) 15 | } 16 | } 17 | 18 | /// A representable that triggers the TopNotch view in a SwiftUI hierarchy. 19 | @available(iOS 13.0, *) 20 | public struct TopNotchRepresentable: UIViewRepresentable { 21 | public func makeUIView(context: Context) -> UIView { 22 | // Trigger TopNotch. 23 | TopNotchManager.shared.show() 24 | return UIView(frame: .zero) 25 | } 26 | 27 | public func updateUIView(_ uiView: UIView, context: Context) { 28 | // No updates required. 29 | } 30 | } 31 | 32 | /// An extension to easily apply the TopNotch modifier to SwiftUI views. 33 | @available(iOS 13.0, *) 34 | public extension View { 35 | /// Adds the TopNotch watermark to the view. 36 | func topNotch() -> some View { 37 | self.modifier(TopNotchModifier()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /TopNotch/UIDevice+Model.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDevice+Model.swift 3 | // TopNotch 4 | // 5 | // Created by Sam Gold on 2025-02-10. 6 | // 7 | 8 | import UIKit 9 | 10 | package extension UIDevice { 11 | /// The device's model identifier (e.g., "iPhone14,4"). 12 | static let modelIdentifier: String = { 13 | if let simulatorModelIdentifier = ProcessInfo.processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] { 14 | return simulatorModelIdentifier 15 | } 16 | var sysinfo = utsname() 17 | uname(&sysinfo) 18 | let machineData = Data(bytes: &sysinfo.machine, count: Int(_SYS_NAMELEN)) 19 | return String(bytes: machineData, encoding: .ascii)? 20 | .trimmingCharacters(in: .controlCharacters) ?? "unknown" 21 | }() 22 | } 23 | -------------------------------------------------------------------------------- /TopNotchDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // TopNotchDemo 4 | // 5 | // Created by Sam Gold on 2025-02-10. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 13 | return true 14 | } 15 | 16 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 17 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /TopNotchDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TopNotchDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | } 30 | ], 31 | "info" : { 32 | "author" : "xcode", 33 | "version" : 1 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /TopNotchDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TopNotchDemo/FontCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FontTableViewController.swift 3 | // TopNotchDemo 4 | // 5 | // Created by Sam Gold on 2025-02-10. 6 | // 7 | 8 | import UIKit 9 | import TopNotch 10 | 11 | class FontTableViewController: UITableViewController { 12 | 13 | private let fontFamilies = UIFont.familyNames 14 | 15 | override init(style: UITableView.Style = .insetGrouped) { 16 | super.init(style: style) 17 | } 18 | 19 | required init?(coder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | self.title = "Font Families" 26 | 27 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "FontCell") 28 | 29 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Show Modal", style: .plain, target: self, action: #selector(showModal)) 30 | } 31 | 32 | override func viewDidAppear(_ animated: Bool) { 33 | super.viewDidAppear(animated) 34 | 35 | let watermarkConfig = TopNotchConfiguration(animationDuration: 0.3, 36 | shouldAnimate: true, 37 | shouldHideForTaskSwitcher: false) 38 | 39 | let notchLabel = UILabel() 40 | notchLabel.text = "Top Notch!" 41 | notchLabel.textColor = .white 42 | notchLabel.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.7) 43 | notchLabel.textAlignment = .center 44 | 45 | TopNotchManager.shared.show(customView: notchLabel, with: watermarkConfig) 46 | } 47 | 48 | 49 | // MARK: - Table view data source 50 | 51 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 52 | return fontFamilies.count 53 | } 54 | 55 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 56 | 57 | let cell = tableView.dequeueReusableCell(withIdentifier: "FontCell", for: indexPath) 58 | cell.textLabel?.text = fontFamilies[indexPath.row] 59 | return cell 60 | } 61 | 62 | // MARK: - Actions 63 | 64 | @objc private func showModal() { 65 | let modalVC = ModalSheetViewController() 66 | let nav = UINavigationController(rootViewController: modalVC) 67 | nav.modalPresentationStyle = .automatic 68 | present(nav, animated: true, completion: nil) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /TopNotchDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /TopNotchDemo/ModalSheetViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModalSheetViewController.swift 3 | // TopNotchDemo 4 | // 5 | // Created by Sam Gold on 2025-02-10. 6 | // 7 | 8 | import UIKit 9 | 10 | class ModalSheetViewController: UIViewController { 11 | override func viewDidLoad() { 12 | super.viewDidLoad() 13 | view.backgroundColor = .systemGroupedBackground 14 | title = "Modal Sheet" 15 | navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, 16 | target: self, 17 | action: #selector(dismissModal)) 18 | } 19 | 20 | @objc private func dismissModal() { 21 | dismiss(animated: true, completion: nil) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /TopNotchDemo/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // TopNotchDemo 4 | // 5 | // Created by Sam Gold on 2025-02-10. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | func scene(_ scene: UIScene, 15 | willConnectTo session: UISceneSession, 16 | options connectionOptions: UIScene.ConnectionOptions) { 17 | guard let windowScene = (scene as? UIWindowScene) else { return } 18 | 19 | let window = UIWindow(windowScene: windowScene) 20 | // Use the insetGrouped table view controller. 21 | let fontTableVC = FontTableViewController() 22 | let navController = UINavigationController(rootViewController: fontTableVC) 23 | 24 | window.rootViewController = navController 25 | self.window = window 26 | window.makeKeyAndVisible() 27 | } 28 | } 29 | --------------------------------------------------------------------------------