├── .editorconfig ├── .gitattributes ├── .github └── funding.yml ├── .gitignore ├── Example ├── Example.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── Example.xcscheme └── Example │ ├── AppDelegate.swift │ ├── Base.lproj │ └── MainMenu.xib │ └── Example.entitlements ├── Package.swift ├── Sources └── CustomButton │ ├── CustomButton.swift │ └── Utilities.swift ├── license ├── readme.md └── screenshot.gif /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: [sindresorhus, boyvanamstel] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.build 2 | /Packages 3 | /.swiftpm 4 | /*.xcodeproj 5 | /Example/**/xcuserdata 6 | /Example/**/project.xcworkspace 7 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 60; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2228A52F2501343A00981867 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2228A52E2501343A00981867 /* Utilities.swift */; }; 11 | E30303402336989400B8ED1F /* CustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E303033E2336989400B8ED1F /* CustomButton.swift */; }; 12 | E3A01E872336904F00C54787 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3A01E862336904F00C54787 /* AppDelegate.swift */; }; 13 | E3A01E8C2336905000C54787 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = E3A01E8A2336905000C54787 /* MainMenu.xib */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | 2228A52E2501343A00981867 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; 18 | E303033E2336989400B8ED1F /* CustomButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = ""; }; 19 | E3A01E832336904F00C54787 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | E3A01E862336904F00C54787 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 21 | E3A01E8B2336905000C54787 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 22 | E3A01E8E2336905000C54787 /* Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Example.entitlements; sourceTree = ""; }; 23 | /* End PBXFileReference section */ 24 | 25 | /* Begin PBXFrameworksBuildPhase section */ 26 | E3A01E802336904F00C54787 /* Frameworks */ = { 27 | isa = PBXFrameworksBuildPhase; 28 | buildActionMask = 2147483647; 29 | files = ( 30 | ); 31 | runOnlyForDeploymentPostprocessing = 0; 32 | }; 33 | /* End PBXFrameworksBuildPhase section */ 34 | 35 | /* Begin PBXGroup section */ 36 | E303033D2336989400B8ED1F /* CustomButton */ = { 37 | isa = PBXGroup; 38 | children = ( 39 | E303033E2336989400B8ED1F /* CustomButton.swift */, 40 | 2228A52E2501343A00981867 /* Utilities.swift */, 41 | ); 42 | name = CustomButton; 43 | path = ../../Sources/CustomButton; 44 | sourceTree = ""; 45 | }; 46 | E3A01E7A2336904F00C54787 = { 47 | isa = PBXGroup; 48 | children = ( 49 | E3A01E852336904F00C54787 /* Example */, 50 | E3A01E842336904F00C54787 /* Products */, 51 | ); 52 | sourceTree = ""; 53 | }; 54 | E3A01E842336904F00C54787 /* Products */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | E3A01E832336904F00C54787 /* Example.app */, 58 | ); 59 | name = Products; 60 | sourceTree = ""; 61 | }; 62 | E3A01E852336904F00C54787 /* Example */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | E3A01E862336904F00C54787 /* AppDelegate.swift */, 66 | E3A01E8A2336905000C54787 /* MainMenu.xib */, 67 | E3A01E8E2336905000C54787 /* Example.entitlements */, 68 | E303033D2336989400B8ED1F /* CustomButton */, 69 | ); 70 | path = Example; 71 | sourceTree = ""; 72 | }; 73 | /* End PBXGroup section */ 74 | 75 | /* Begin PBXNativeTarget section */ 76 | E3A01E822336904F00C54787 /* Example */ = { 77 | isa = PBXNativeTarget; 78 | buildConfigurationList = E3A01E912336905000C54787 /* Build configuration list for PBXNativeTarget "Example" */; 79 | buildPhases = ( 80 | E3A01E7F2336904F00C54787 /* Sources */, 81 | E3A01E802336904F00C54787 /* Frameworks */, 82 | E3A01E812336904F00C54787 /* Resources */, 83 | ); 84 | buildRules = ( 85 | ); 86 | dependencies = ( 87 | ); 88 | name = Example; 89 | packageProductDependencies = ( 90 | ); 91 | productName = Example; 92 | productReference = E3A01E832336904F00C54787 /* Example.app */; 93 | productType = "com.apple.product-type.application"; 94 | }; 95 | /* End PBXNativeTarget section */ 96 | 97 | /* Begin PBXProject section */ 98 | E3A01E7B2336904F00C54787 /* Project object */ = { 99 | isa = PBXProject; 100 | attributes = { 101 | BuildIndependentTargetsInParallel = YES; 102 | LastSwiftUpdateCheck = 1100; 103 | LastUpgradeCheck = 1500; 104 | ORGANIZATIONNAME = "Sindre Sorhus"; 105 | TargetAttributes = { 106 | E3A01E822336904F00C54787 = { 107 | CreatedOnToolsVersion = 11.0; 108 | }; 109 | }; 110 | }; 111 | buildConfigurationList = E3A01E7E2336904F00C54787 /* Build configuration list for PBXProject "Example" */; 112 | compatibilityVersion = "Xcode 15.0"; 113 | developmentRegion = en; 114 | hasScannedForEncodings = 0; 115 | knownRegions = ( 116 | en, 117 | Base, 118 | ); 119 | mainGroup = E3A01E7A2336904F00C54787; 120 | packageReferences = ( 121 | ); 122 | productRefGroup = E3A01E842336904F00C54787 /* Products */; 123 | projectDirPath = ""; 124 | projectRoot = ""; 125 | targets = ( 126 | E3A01E822336904F00C54787 /* Example */, 127 | ); 128 | }; 129 | /* End PBXProject section */ 130 | 131 | /* Begin PBXResourcesBuildPhase section */ 132 | E3A01E812336904F00C54787 /* Resources */ = { 133 | isa = PBXResourcesBuildPhase; 134 | buildActionMask = 2147483647; 135 | files = ( 136 | E3A01E8C2336905000C54787 /* MainMenu.xib in Resources */, 137 | ); 138 | runOnlyForDeploymentPostprocessing = 0; 139 | }; 140 | /* End PBXResourcesBuildPhase section */ 141 | 142 | /* Begin PBXSourcesBuildPhase section */ 143 | E3A01E7F2336904F00C54787 /* Sources */ = { 144 | isa = PBXSourcesBuildPhase; 145 | buildActionMask = 2147483647; 146 | files = ( 147 | 2228A52F2501343A00981867 /* Utilities.swift in Sources */, 148 | E30303402336989400B8ED1F /* CustomButton.swift in Sources */, 149 | E3A01E872336904F00C54787 /* AppDelegate.swift in Sources */, 150 | ); 151 | runOnlyForDeploymentPostprocessing = 0; 152 | }; 153 | /* End PBXSourcesBuildPhase section */ 154 | 155 | /* Begin PBXVariantGroup section */ 156 | E3A01E8A2336905000C54787 /* MainMenu.xib */ = { 157 | isa = PBXVariantGroup; 158 | children = ( 159 | E3A01E8B2336905000C54787 /* Base */, 160 | ); 161 | name = MainMenu.xib; 162 | sourceTree = ""; 163 | }; 164 | /* End PBXVariantGroup section */ 165 | 166 | /* Begin XCBuildConfiguration section */ 167 | E3A01E8F2336905000C54787 /* Debug */ = { 168 | isa = XCBuildConfiguration; 169 | buildSettings = { 170 | ALWAYS_SEARCH_USER_PATHS = NO; 171 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 172 | CLANG_ANALYZER_NONNULL = YES; 173 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 174 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 175 | CLANG_CXX_LIBRARY = "libc++"; 176 | CLANG_ENABLE_MODULES = YES; 177 | CLANG_ENABLE_OBJC_ARC = YES; 178 | CLANG_ENABLE_OBJC_WEAK = YES; 179 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 180 | CLANG_WARN_BOOL_CONVERSION = YES; 181 | CLANG_WARN_COMMA = YES; 182 | CLANG_WARN_CONSTANT_CONVERSION = YES; 183 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 184 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 185 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 186 | CLANG_WARN_EMPTY_BODY = YES; 187 | CLANG_WARN_ENUM_CONVERSION = YES; 188 | CLANG_WARN_INFINITE_RECURSION = YES; 189 | CLANG_WARN_INT_CONVERSION = YES; 190 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 191 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 192 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 193 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 194 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 195 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 196 | CLANG_WARN_STRICT_PROTOTYPES = YES; 197 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 198 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 199 | CLANG_WARN_UNREACHABLE_CODE = YES; 200 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 201 | COPY_PHASE_STRIP = NO; 202 | DEAD_CODE_STRIPPING = YES; 203 | DEBUG_INFORMATION_FORMAT = dwarf; 204 | ENABLE_STRICT_OBJC_MSGSEND = YES; 205 | ENABLE_TESTABILITY = YES; 206 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 207 | GCC_C_LANGUAGE_STANDARD = gnu11; 208 | GCC_DYNAMIC_NO_PIC = NO; 209 | GCC_NO_COMMON_BLOCKS = YES; 210 | GCC_OPTIMIZATION_LEVEL = 0; 211 | GCC_PREPROCESSOR_DEFINITIONS = ( 212 | "DEBUG=1", 213 | "$(inherited)", 214 | ); 215 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 216 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 217 | GCC_WARN_UNDECLARED_SELECTOR = YES; 218 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 219 | GCC_WARN_UNUSED_FUNCTION = YES; 220 | GCC_WARN_UNUSED_VARIABLE = YES; 221 | MACOSX_DEPLOYMENT_TARGET = 13.5; 222 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 223 | MTL_FAST_MATH = YES; 224 | ONLY_ACTIVE_ARCH = YES; 225 | SDKROOT = macosx; 226 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 227 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 228 | }; 229 | name = Debug; 230 | }; 231 | E3A01E902336905000C54787 /* Release */ = { 232 | isa = XCBuildConfiguration; 233 | buildSettings = { 234 | ALWAYS_SEARCH_USER_PATHS = NO; 235 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 236 | CLANG_ANALYZER_NONNULL = YES; 237 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 238 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 239 | CLANG_CXX_LIBRARY = "libc++"; 240 | CLANG_ENABLE_MODULES = YES; 241 | CLANG_ENABLE_OBJC_ARC = YES; 242 | CLANG_ENABLE_OBJC_WEAK = YES; 243 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 244 | CLANG_WARN_BOOL_CONVERSION = YES; 245 | CLANG_WARN_COMMA = YES; 246 | CLANG_WARN_CONSTANT_CONVERSION = YES; 247 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 248 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 249 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 250 | CLANG_WARN_EMPTY_BODY = YES; 251 | CLANG_WARN_ENUM_CONVERSION = YES; 252 | CLANG_WARN_INFINITE_RECURSION = YES; 253 | CLANG_WARN_INT_CONVERSION = YES; 254 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 255 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 256 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 257 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 258 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 259 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 260 | CLANG_WARN_STRICT_PROTOTYPES = YES; 261 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 262 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 263 | CLANG_WARN_UNREACHABLE_CODE = YES; 264 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 265 | COPY_PHASE_STRIP = NO; 266 | DEAD_CODE_STRIPPING = YES; 267 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 268 | ENABLE_NS_ASSERTIONS = NO; 269 | ENABLE_STRICT_OBJC_MSGSEND = YES; 270 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 271 | GCC_C_LANGUAGE_STANDARD = gnu11; 272 | GCC_NO_COMMON_BLOCKS = YES; 273 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 274 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 275 | GCC_WARN_UNDECLARED_SELECTOR = YES; 276 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 277 | GCC_WARN_UNUSED_FUNCTION = YES; 278 | GCC_WARN_UNUSED_VARIABLE = YES; 279 | MACOSX_DEPLOYMENT_TARGET = 13.5; 280 | MTL_ENABLE_DEBUG_INFO = NO; 281 | MTL_FAST_MATH = YES; 282 | SDKROOT = macosx; 283 | SWIFT_COMPILATION_MODE = wholemodule; 284 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 285 | }; 286 | name = Release; 287 | }; 288 | E3A01E922336905000C54787 /* Debug */ = { 289 | isa = XCBuildConfiguration; 290 | buildSettings = { 291 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; 292 | CODE_SIGN_IDENTITY = "-"; 293 | CODE_SIGN_STYLE = Manual; 294 | COMBINE_HIDPI_IMAGES = YES; 295 | CURRENT_PROJECT_VERSION = 1; 296 | DEAD_CODE_STRIPPING = YES; 297 | DEVELOPMENT_TEAM = ""; 298 | ENABLE_HARDENED_RUNTIME = YES; 299 | GENERATE_INFOPLIST_FILE = YES; 300 | INFOPLIST_KEY_NSMainNibFile = MainMenu; 301 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 302 | LD_RUNPATH_SEARCH_PATHS = ( 303 | "$(inherited)", 304 | "@executable_path/../Frameworks", 305 | ); 306 | MARKETING_VERSION = 1.0.0; 307 | PRODUCT_BUNDLE_IDENTIFIER = com.sindresorhus.Example; 308 | PRODUCT_NAME = "$(TARGET_NAME)"; 309 | PROVISIONING_PROFILE_SPECIFIER = ""; 310 | SWIFT_VERSION = 5.0; 311 | }; 312 | name = Debug; 313 | }; 314 | E3A01E932336905000C54787 /* Release */ = { 315 | isa = XCBuildConfiguration; 316 | buildSettings = { 317 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; 318 | CODE_SIGN_IDENTITY = "-"; 319 | CODE_SIGN_STYLE = Manual; 320 | COMBINE_HIDPI_IMAGES = YES; 321 | CURRENT_PROJECT_VERSION = 1; 322 | DEAD_CODE_STRIPPING = YES; 323 | DEVELOPMENT_TEAM = ""; 324 | ENABLE_HARDENED_RUNTIME = YES; 325 | GENERATE_INFOPLIST_FILE = YES; 326 | INFOPLIST_KEY_NSMainNibFile = MainMenu; 327 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 328 | LD_RUNPATH_SEARCH_PATHS = ( 329 | "$(inherited)", 330 | "@executable_path/../Frameworks", 331 | ); 332 | MARKETING_VERSION = 1.0.0; 333 | PRODUCT_BUNDLE_IDENTIFIER = com.sindresorhus.Example; 334 | PRODUCT_NAME = "$(TARGET_NAME)"; 335 | PROVISIONING_PROFILE_SPECIFIER = ""; 336 | SWIFT_VERSION = 5.0; 337 | }; 338 | name = Release; 339 | }; 340 | /* End XCBuildConfiguration section */ 341 | 342 | /* Begin XCConfigurationList section */ 343 | E3A01E7E2336904F00C54787 /* Build configuration list for PBXProject "Example" */ = { 344 | isa = XCConfigurationList; 345 | buildConfigurations = ( 346 | E3A01E8F2336905000C54787 /* Debug */, 347 | E3A01E902336905000C54787 /* Release */, 348 | ); 349 | defaultConfigurationIsVisible = 0; 350 | defaultConfigurationName = Release; 351 | }; 352 | E3A01E912336905000C54787 /* Build configuration list for PBXNativeTarget "Example" */ = { 353 | isa = XCConfigurationList; 354 | buildConfigurations = ( 355 | E3A01E922336905000C54787 /* Debug */, 356 | E3A01E932336905000C54787 /* Release */, 357 | ); 358 | defaultConfigurationIsVisible = 0; 359 | defaultConfigurationName = Release; 360 | }; 361 | /* End XCConfigurationList section */ 362 | }; 363 | rootObject = E3A01E7B2336904F00C54787 /* Project object */; 364 | } 365 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 58 | 59 | 63 | 64 | 65 | 66 | 72 | 74 | 80 | 81 | 82 | 83 | 85 | 86 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /Example/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | @main 4 | final class AppDelegate: NSObject, NSApplicationDelegate { 5 | @IBOutlet private var window: NSWindow! 6 | 7 | func applicationDidFinishLaunching(_ notification: Notification) { 8 | let button = CustomButton() 9 | button.translatesAutoresizingMaskIntoConstraints = false 10 | button.title = "CustomButton" 11 | button.activeBackgroundColor = .systemPink 12 | button.borderWidth = 1 13 | button.borderColor = .systemPink 14 | button.cornerRadius = 5 15 | button.textColor = .systemPink 16 | button.activeTextColor = .white 17 | 18 | let contentView = window.contentView! 19 | contentView.addSubview(button) 20 | 21 | NSLayoutConstraint.activate([ 22 | button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), 23 | button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), 24 | button.widthAnchor.constraint(equalToConstant: 100), 25 | button.heightAnchor.constraint(equalToConstant: 30) 26 | ]) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Example/Example/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | Default 541 | 542 | 543 | 544 | 545 | 546 | 547 | Left to Right 548 | 549 | 550 | 551 | 552 | 553 | 554 | Right to Left 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | Default 566 | 567 | 568 | 569 | 570 | 571 | 572 | Left to Right 573 | 574 | 575 | 576 | 577 | 578 | 579 | Right to Left 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | -------------------------------------------------------------------------------- /Example/Example/Example.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "CustomButton", 6 | platforms: [ 7 | .macOS(.v10_13) 8 | ], 9 | products: [ 10 | .library( 11 | name: "CustomButton", 12 | targets: [ 13 | "CustomButton" 14 | ] 15 | ) 16 | ], 17 | targets: [ 18 | .target( 19 | name: "CustomButton" 20 | ) 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Sources/CustomButton/CustomButton.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | @IBDesignable 4 | open class CustomButton: NSButton { 5 | private let titleLayer = CATextLayer() 6 | private var isMouseDown = false 7 | 8 | public static func circularButton(title: String, radius: Double, center: CGPoint) -> CustomButton { 9 | with(CustomButton()) { 10 | $0.title = title 11 | $0.frame = CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2) 12 | $0.cornerRadius = radius 13 | $0.font = .systemFont(ofSize: radius * 2 / 3) 14 | } 15 | } 16 | 17 | override open var wantsUpdateLayer: Bool { true } 18 | 19 | @IBInspectable override public var title: String { 20 | didSet { 21 | setTitle() 22 | } 23 | } 24 | 25 | @IBInspectable public var textColor: NSColor = .labelColor { 26 | didSet { 27 | titleLayer.foregroundColor = textColor.cgColor 28 | } 29 | } 30 | 31 | @IBInspectable public var activeTextColor: NSColor = .labelColor { 32 | didSet { 33 | if state == .on { 34 | titleLayer.foregroundColor = textColor.cgColor 35 | } 36 | } 37 | } 38 | 39 | @IBInspectable public var cornerRadius: Double = 0 { 40 | didSet { 41 | layer?.cornerRadius = cornerRadius 42 | } 43 | } 44 | 45 | @IBInspectable public var hasContinuousCorners: Bool = true { 46 | didSet { 47 | if #available(macOS 10.15, *) { 48 | layer?.cornerCurve = hasContinuousCorners ? .continuous : .circular 49 | } 50 | } 51 | } 52 | 53 | @IBInspectable public var borderWidth: Double = 0 { 54 | didSet { 55 | layer?.borderWidth = borderWidth 56 | } 57 | } 58 | 59 | @IBInspectable public var borderColor: NSColor = .clear { 60 | didSet { 61 | layer?.borderColor = borderColor.cgColor 62 | } 63 | } 64 | 65 | @IBInspectable public var activeBorderColor: NSColor = .clear { 66 | didSet { 67 | if state == .on { 68 | layer?.borderColor = activeBorderColor.cgColor 69 | } 70 | } 71 | } 72 | 73 | @IBInspectable public var backgroundColor: NSColor = .clear { 74 | didSet { 75 | layer?.backgroundColor = backgroundColor.cgColor 76 | } 77 | } 78 | 79 | @IBInspectable public var activeBackgroundColor: NSColor = .clear { 80 | didSet { 81 | if state == .on { 82 | layer?.backgroundColor = activeBackgroundColor.cgColor 83 | } 84 | } 85 | } 86 | 87 | @IBInspectable public var shadowRadius: Double = 0 { 88 | didSet { 89 | layer?.shadowRadius = shadowRadius 90 | } 91 | } 92 | 93 | @IBInspectable public var activeShadowRadius: Double = -1 { 94 | didSet { 95 | if state == .on { 96 | layer?.shadowRadius = activeShadowRadius 97 | } 98 | } 99 | } 100 | 101 | @IBInspectable public var shadowOpacity: Double = 0 { 102 | didSet { 103 | layer?.shadowOpacity = Float(shadowOpacity) 104 | } 105 | } 106 | 107 | @IBInspectable public var activeShadowOpacity: Double = -1 { 108 | didSet { 109 | if state == .on { 110 | layer?.shadowOpacity = Float(activeShadowOpacity) 111 | } 112 | } 113 | } 114 | 115 | @IBInspectable public var shadowColor: NSColor = .clear { 116 | didSet { 117 | layer?.shadowColor = shadowColor.cgColor 118 | } 119 | } 120 | 121 | @IBInspectable public var activeShadowColor: NSColor? { 122 | didSet { 123 | if state == .on, let activeShadowColor { 124 | layer?.shadowColor = activeShadowColor.cgColor 125 | } 126 | } 127 | } 128 | 129 | override public var font: NSFont? { 130 | didSet { 131 | setTitle() 132 | } 133 | } 134 | 135 | override public var isEnabled: Bool { 136 | didSet { 137 | alphaValue = isEnabled ? 1 : 0.6 138 | } 139 | } 140 | 141 | public convenience init() { 142 | self.init(frame: .zero) 143 | } 144 | 145 | public required init?(coder: NSCoder) { 146 | super.init(coder: coder) 147 | setup() 148 | } 149 | 150 | override init(frame: CGRect) { 151 | super.init(frame: frame) 152 | setup() 153 | } 154 | 155 | // Ensure the button doesn't draw its default contents. 156 | override open func draw(_ dirtyRect: CGRect) {} 157 | override open func drawFocusRingMask() {} 158 | 159 | override open func layout() { 160 | super.layout() 161 | positionTitle() 162 | } 163 | 164 | override open func viewDidChangeBackingProperties() { 165 | super.viewDidChangeBackingProperties() 166 | 167 | if let scale = window?.backingScaleFactor { 168 | layer?.contentsScale = scale 169 | titleLayer.contentsScale = scale 170 | } 171 | } 172 | 173 | private lazy var trackingArea = TrackingArea( 174 | for: self, 175 | options: [ 176 | .mouseEnteredAndExited, 177 | .activeInActiveApp 178 | ] 179 | ) 180 | 181 | override open func updateTrackingAreas() { 182 | super.updateTrackingAreas() 183 | trackingArea.update() 184 | } 185 | 186 | private func setup() { 187 | let isOn = state == .on 188 | 189 | wantsLayer = true 190 | 191 | layer?.masksToBounds = false 192 | 193 | layer?.cornerRadius = cornerRadius 194 | layer?.borderWidth = borderWidth 195 | layer?.shadowRadius = isOn && activeShadowRadius != -1 ? activeShadowRadius : shadowRadius 196 | layer?.shadowOpacity = Float(isOn && activeShadowOpacity != -1 ? activeShadowOpacity : shadowOpacity) 197 | layer?.backgroundColor = isOn ? activeBackgroundColor.cgColor : backgroundColor.cgColor 198 | layer?.borderColor = isOn ? activeBorderColor.cgColor : borderColor.cgColor 199 | layer?.shadowColor = isOn ? (activeShadowColor?.cgColor ?? shadowColor.cgColor) : shadowColor.cgColor 200 | 201 | if #available(macOS 10.15, *) { 202 | layer?.cornerCurve = hasContinuousCorners ? .continuous : .circular 203 | } 204 | 205 | titleLayer.alignmentMode = .center 206 | titleLayer.contentsScale = window?.backingScaleFactor ?? 2 207 | titleLayer.foregroundColor = isOn ? activeTextColor.cgColor : textColor.cgColor 208 | layer?.addSublayer(titleLayer) 209 | setTitle() 210 | 211 | needsDisplay = true 212 | } 213 | 214 | public typealias ColorGenerator = () -> NSColor 215 | 216 | private var colorGenerators = [KeyPath: ColorGenerator]() 217 | 218 | /** 219 | Gets or sets the color generation closure for the provided key path. 220 | 221 | - Parameter keyPath: The key path that specifies the color related property. 222 | */ 223 | public subscript(colorGenerator keyPath: KeyPath) -> ColorGenerator? { 224 | get { colorGenerators[keyPath] } 225 | set { 226 | colorGenerators[keyPath] = newValue 227 | } 228 | } 229 | 230 | private func color(for keyPath: KeyPath) -> NSColor { 231 | colorGenerators[keyPath]?() ?? self[keyPath: keyPath] 232 | } 233 | 234 | override open func updateLayer() { 235 | animateColor() 236 | } 237 | 238 | private func setTitle() { 239 | titleLayer.string = title 240 | 241 | if let font { 242 | titleLayer.font = font 243 | titleLayer.fontSize = font.pointSize 244 | } 245 | 246 | needsLayout = true 247 | } 248 | 249 | private func positionTitle() { 250 | let titleSize = title.size(withAttributes: [.font: font as Any]) 251 | titleLayer.frame = titleSize.centered(in: bounds).roundedOrigin() 252 | } 253 | 254 | private func animateColor() { 255 | let isOn = state == .on 256 | let duration = isOn ? 0.2 : 0.1 257 | let backgroundColor = isOn ? color(for: \.activeBackgroundColor) : color(for: \.backgroundColor) 258 | let textColor = isOn ? color(for: \.activeTextColor) : color(for: \.textColor) 259 | let borderColor = isOn ? color(for: \.activeBorderColor) : color(for: \.borderColor) 260 | let shadowColor = isOn ? (activeShadowColor ?? color(for: \.shadowColor)) : color(for: \.shadowColor) 261 | 262 | layer?.animate(\.backgroundColor, to: backgroundColor, duration: duration) 263 | layer?.animate(\.borderColor, to: borderColor, duration: duration) 264 | layer?.animate(\.shadowColor, to: shadowColor, duration: duration) 265 | titleLayer.animate(\.foregroundColor, to: textColor, duration: duration) 266 | } 267 | 268 | private func toggleState() { 269 | state = state == .off ? .on : .off 270 | animateColor() 271 | } 272 | 273 | override open func hitTest(_ point: CGPoint) -> NSView? { 274 | isEnabled ? super.hitTest(point) : nil 275 | } 276 | 277 | override open func mouseDown(with event: NSEvent) { 278 | isMouseDown = true 279 | toggleState() 280 | } 281 | 282 | override open func mouseEntered(with event: NSEvent) { 283 | if isMouseDown { 284 | toggleState() 285 | } 286 | } 287 | 288 | override open func mouseExited(with event: NSEvent) { 289 | if isMouseDown { 290 | toggleState() 291 | isMouseDown = false 292 | } 293 | } 294 | 295 | override open func mouseUp(with event: NSEvent) { 296 | if isMouseDown { 297 | isMouseDown = false 298 | toggleState() 299 | _ = target?.perform(action, with: self) 300 | } 301 | } 302 | } 303 | 304 | extension CustomButton: NSViewLayerContentScaleDelegate { 305 | public func layer(_ layer: CALayer, shouldInheritContentsScale newScale: CGFloat, from window: NSWindow) -> Bool { true } 306 | } 307 | -------------------------------------------------------------------------------- /Sources/CustomButton/Utilities.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | 4 | /** 5 | Convenience function for initializing an object and modifying its properties. 6 | 7 | ``` 8 | let label = with(NSTextField()) { 9 | $0.stringValue = "Foo" 10 | $0.textColor = .systemBlue 11 | view.addSubview($0) 12 | } 13 | ``` 14 | */ 15 | @discardableResult 16 | func with(_ item: T, update: (inout T) throws -> Void) rethrows -> T { 17 | var this = item 18 | try update(&this) 19 | return this 20 | } 21 | 22 | 23 | /** 24 | Convenience class for adding a tracking area to a view. 25 | 26 | ``` 27 | final class HoverView: NSView { 28 | private lazy var trackingArea = TrackingArea( 29 | for: self, 30 | options: [ 31 | .mouseEnteredAndExited, 32 | .activeInActiveApp 33 | ] 34 | ) 35 | 36 | override func updateTrackingAreas() { 37 | super.updateTrackingAreas() 38 | trackingArea.update() 39 | } 40 | } 41 | ``` 42 | */ 43 | final class TrackingArea { 44 | private weak var view: NSView? 45 | private let rect: CGRect 46 | private let options: NSTrackingArea.Options 47 | private weak var trackingArea: NSTrackingArea? 48 | 49 | /** 50 | - Parameters: 51 | - view: The view to add tracking to. 52 | - rect: The area inside the view to track. Defaults to the whole view (`view.bounds`). 53 | */ 54 | init( 55 | for view: NSView, 56 | rect: CGRect? = nil, 57 | options: NSTrackingArea.Options = [] 58 | ) { 59 | self.view = view 60 | self.rect = rect ?? view.bounds 61 | self.options = options 62 | } 63 | 64 | /** 65 | Updates the tracking area. 66 | - Note: This should be called in your `NSView#updateTrackingAreas()` method. 67 | */ 68 | func update() { 69 | if let trackingArea { 70 | view?.removeTrackingArea(trackingArea) 71 | } 72 | 73 | let newTrackingArea = NSTrackingArea( 74 | rect: rect, 75 | options: [ 76 | .mouseEnteredAndExited, 77 | .activeInActiveApp 78 | ], 79 | owner: view, 80 | userInfo: nil 81 | ) 82 | 83 | view?.addTrackingArea(newTrackingArea) 84 | trackingArea = newTrackingArea 85 | } 86 | } 87 | 88 | 89 | final class AnimationDelegate: NSObject, CAAnimationDelegate { 90 | var didStopHandler: ((Bool) -> Void)? 91 | 92 | func animationDidStop(_ animation: CAAnimation, finished flag: Bool) { 93 | didStopHandler?(flag) 94 | } 95 | } 96 | 97 | 98 | protocol LayerColorAnimation: AnyObject {} 99 | extension CALayer: LayerColorAnimation {} 100 | 101 | extension LayerColorAnimation where Self: CALayer { 102 | /** 103 | Animate colors. 104 | */ 105 | func animate(_ keyPath: ReferenceWritableKeyPath, to color: CGColor, duration: Double) { 106 | let animation = CABasicAnimation(keyPath: keyPath.toString) 107 | animation.fromValue = self[keyPath: keyPath] 108 | animation.toValue = color 109 | animation.duration = duration 110 | animation.fillMode = .forwards 111 | animation.isRemovedOnCompletion = false 112 | 113 | add(animation, forKeyPath: keyPath) { [weak self] _ in 114 | self?[keyPath: keyPath] = color 115 | } 116 | } 117 | 118 | /** 119 | Animate colors. 120 | */ 121 | func animate(_ keyPath: ReferenceWritableKeyPath, to color: NSColor, duration: Double) { 122 | animate(keyPath, to: color.cgColor, duration: duration) 123 | } 124 | 125 | /** 126 | Add color animation. 127 | */ 128 | func add(_ animation: CAAnimation, forKeyPath keyPath: ReferenceWritableKeyPath, completion: @escaping ((Bool) -> Void)) { 129 | let animationDelegate = AnimationDelegate() 130 | animationDelegate.didStopHandler = completion 131 | animation.delegate = animationDelegate 132 | add(animation, forKey: keyPath.toString) 133 | } 134 | } 135 | 136 | 137 | extension CGPoint { 138 | func rounded(_ rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) -> Self { 139 | Self(x: x.rounded(rule), y: y.rounded(rule)) 140 | } 141 | } 142 | 143 | 144 | extension CGRect { 145 | func roundedOrigin(_ rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) -> Self { 146 | var rect = self 147 | rect.origin = rect.origin.rounded(rule) 148 | return rect 149 | } 150 | } 151 | 152 | 153 | extension CGSize { 154 | /** 155 | Returns a CGRect with `self` centered in it. 156 | */ 157 | func centered(in rect: CGRect) -> CGRect { 158 | CGRect( 159 | x: (rect.width - width) / 2, 160 | y: (rect.height - height) / 2, 161 | width: width, 162 | height: height 163 | ) 164 | } 165 | } 166 | 167 | 168 | extension KeyPath where Root: NSObject { 169 | /** 170 | Get the string version of the key path when the root is an `NSObject`. 171 | */ 172 | var toString: String { 173 | NSExpression(forKeyPath: self).keyPath 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # CustomButton 2 | 3 | > Customizable button for your macOS app 4 | 5 | 6 | 7 | It's a layer-based NSButton subclass that animates the styles and colors between normal and active (pressed) state. 8 | 9 | This package is used in production by [Gifski](https://github.com/sindresorhus/Gifski). 10 | 11 | ## Requirements 12 | 13 | - macOS 10.13+ 14 | 15 | ## Install 16 | 17 | Add `https://github.com/sindresorhus/CustomButton` in the [“Swift Package Manager” tab in Xcode](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app). 18 | 19 | ## Usage 20 | 21 | See the [source](Sources/CustomButton/CustomButton.swift) for what properties you can change and play with it in the [example app](Example). By default, it has no style. 22 | 23 | ```swift 24 | import Cocoa 25 | import CustomButton 26 | 27 | @main 28 | final class AppDelegate: NSObject, NSApplicationDelegate { 29 | @IBOutlet weak var window: NSWindow! 30 | 31 | func applicationDidFinishLaunching(_ notification: Notification) { 32 | let button = CustomButton() 33 | button.translatesAutoresizingMaskIntoConstraints = false 34 | button.title = "CustomButton" 35 | button.activeBackgroundColor = .systemPink 36 | button.borderWidth = 1 37 | button.borderColor = .systemPink 38 | button.cornerRadius = 5 39 | button.textColor = .systemPink 40 | button.activeTextColor = .white 41 | 42 | let contentView = window.contentView! 43 | contentView.addSubview(button) 44 | 45 | NSLayoutConstraint.activate([ 46 | button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), 47 | button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), 48 | button.widthAnchor.constraint(equalToConstant: 100), 49 | button.heightAnchor.constraint(equalToConstant: 30) 50 | ]) 51 | } 52 | } 53 | ``` 54 | 55 | The button can also be edited in Interface Builder, but Xcode is very buggy with `@IBDesignable`, so I would recommend using it only programmatically for now. 56 | 57 | ## FAQ 58 | 59 | #### Can you support Carthage and CocoaPods? 60 | 61 | No, but you can still use Swift Package Manager for this package even though you mainly use Carthage or CocoaPods. 62 | 63 | #### Won't SwiftUI make this moot? 64 | 65 | SwiftUI does indeed make it much easier to create custom-looking buttons, but SwiftUI is still immature and most companies will not be able to require macOS 10.15 for a long time. So this package will still be useful for multiple years to come. 66 | 67 | ## Related 68 | 69 | - [Defaults](https://github.com/sindresorhus/Defaults) - Swifty and modern UserDefaults 70 | - [KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts) - Add user-customizable global keyboard shortcuts to your macOS app 71 | - [LaunchAtLogin](https://github.com/sindresorhus/LaunchAtLogin) - Add "Launch at Login" functionality to your macOS app 72 | - [DockProgress](https://github.com/sindresorhus/DockProgress) - Show progress in your app's Dock icon 73 | - [More…](https://github.com/search?q=user%3Asindresorhus+language%3Aswift+archived%3Afalse&type=repositories) 74 | 75 | You might also like my [apps](https://sindresorhus.com/apps). 76 | -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sindresorhus/CustomButton/e7c72500005ae4f3ca29c102ad7f8175c68c396b/screenshot.gif --------------------------------------------------------------------------------