├── .gitignore ├── Button.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── Button.xcscheme ├── Button ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── Horizontal Reversed.imageset │ │ ├── Contents.json │ │ ├── Horizontal Reversed.png │ │ ├── Horizontal Reversed@2x.png │ │ └── Horizontal Reversed@3x.png │ ├── Horizontal.imageset │ │ ├── Contents.json │ │ ├── Horizontal.png │ │ ├── Horizontal@2x.png │ │ └── Horizontal@3x.png │ ├── Vertical Reversed.imageset │ │ ├── Contents.json │ │ ├── Vertical Reversed.png │ │ ├── Vertical Reversed@2x.png │ │ └── Vertical Reversed@3x.png │ └── Vertical.imageset │ │ ├── Contents.json │ │ ├── Vertical.png │ │ ├── Vertical@2x.png │ │ └── Vertical@3x.png ├── Base.lproj │ └── LaunchScreen.storyboard ├── Button-Bridging-Header.h ├── Button.swift ├── Geometry.swift ├── Info.plist ├── SceneDelegate.swift ├── ViewController.swift └── ViewController.xib └── ButtonTests ├── ButtonTests.swift └── Info.plist /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | .DS_Store 6 | 7 | ## User settings 8 | xcuserdata/ 9 | 10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 11 | *.xcscmblueprint 12 | *.xccheckout 13 | 14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 15 | build/ 16 | DerivedData/ 17 | *.moved-aside 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | 30 | ## App packaging 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | # Package.pins 44 | # Package.resolved 45 | # *.xcodeproj 46 | # 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | # .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | # 59 | # Pods/ 60 | # 61 | # Add this line if you want to avoid checking in source code from the Xcode workspace 62 | # *.xcworkspace 63 | 64 | # Carthage 65 | # 66 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 67 | # Carthage/Checkouts 68 | 69 | Carthage/Build/ 70 | 71 | # Accio dependency management 72 | Dependencies/ 73 | .accio/ 74 | 75 | # fastlane 76 | # 77 | # It is recommended to not store the screenshots in the git repo. 78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 79 | # For more information about the recommended setup visit: 80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 81 | 82 | fastlane/report.xml 83 | fastlane/Preview.html 84 | fastlane/screenshots/**/*.png 85 | fastlane/test_output 86 | 87 | # Code Injection 88 | # 89 | # After new code Injection tools there's a generated folder /iOSInjectionProject 90 | # https://github.com/johnno1962/injectionforxcode 91 | 92 | iOSInjectionProject/ -------------------------------------------------------------------------------- /Button.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 53; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1FADE77A243CC0CA00389BCA /* Geometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FADE779243CC0CA00389BCA /* Geometry.swift */; }; 11 | 1FF4773A242D9E6200C74B68 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF47739242D9E6200C74B68 /* AppDelegate.swift */; }; 12 | 1FF4773C242D9E6200C74B68 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4773B242D9E6200C74B68 /* SceneDelegate.swift */; }; 13 | 1FF47743242D9E6600C74B68 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1FF47742242D9E6600C74B68 /* Assets.xcassets */; }; 14 | 1FF47746242D9E6600C74B68 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1FF47744242D9E6600C74B68 /* LaunchScreen.storyboard */; }; 15 | 1FF47751242D9E6600C74B68 /* ButtonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF47750242D9E6600C74B68 /* ButtonTests.swift */; }; 16 | 1FF4775C242E3B8F00C74B68 /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4775B242E3B8F00C74B68 /* Button.swift */; }; 17 | 1FF477642430EBCD00C74B68 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF477622430EBCD00C74B68 /* ViewController.swift */; }; 18 | 1FF477652430EBCD00C74B68 /* ViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1FF477632430EBCD00C74B68 /* ViewController.xib */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXContainerItemProxy section */ 22 | 1FF4774D242D9E6600C74B68 /* PBXContainerItemProxy */ = { 23 | isa = PBXContainerItemProxy; 24 | containerPortal = 1FF4772E242D9E6200C74B68 /* Project object */; 25 | proxyType = 1; 26 | remoteGlobalIDString = 1FF47735242D9E6200C74B68; 27 | remoteInfo = Button; 28 | }; 29 | /* End PBXContainerItemProxy section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 1FADE779243CC0CA00389BCA /* Geometry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Geometry.swift; sourceTree = ""; }; 33 | 1FF47736242D9E6200C74B68 /* Button Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Button Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 1FF47739242D9E6200C74B68 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 35 | 1FF4773B242D9E6200C74B68 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 36 | 1FF47742242D9E6600C74B68 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 37 | 1FF47745242D9E6600C74B68 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 38 | 1FF47747242D9E6600C74B68 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 39 | 1FF4774C242D9E6600C74B68 /* ButtonTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ButtonTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | 1FF47750242D9E6600C74B68 /* ButtonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonTests.swift; sourceTree = ""; }; 41 | 1FF47752242D9E6600C74B68 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 42 | 1FF4775B242E3B8F00C74B68 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; 43 | 1FF4775D2430361400C74B68 /* Button-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Button-Bridging-Header.h"; sourceTree = ""; }; 44 | 1FF477622430EBCD00C74B68 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 45 | 1FF477632430EBCD00C74B68 /* ViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ViewController.xib; sourceTree = ""; }; 46 | /* End PBXFileReference section */ 47 | 48 | /* Begin PBXFrameworksBuildPhase section */ 49 | 1FF47733242D9E6200C74B68 /* Frameworks */ = { 50 | isa = PBXFrameworksBuildPhase; 51 | buildActionMask = 2147483647; 52 | files = ( 53 | ); 54 | runOnlyForDeploymentPostprocessing = 0; 55 | }; 56 | 1FF47749242D9E6600C74B68 /* Frameworks */ = { 57 | isa = PBXFrameworksBuildPhase; 58 | buildActionMask = 2147483647; 59 | files = ( 60 | ); 61 | runOnlyForDeploymentPostprocessing = 0; 62 | }; 63 | /* End PBXFrameworksBuildPhase section */ 64 | 65 | /* Begin PBXGroup section */ 66 | 1FF4772D242D9E6200C74B68 = { 67 | isa = PBXGroup; 68 | children = ( 69 | 1FF47738242D9E6200C74B68 /* Button */, 70 | 1FF4774F242D9E6600C74B68 /* ButtonTests */, 71 | 1FF47737242D9E6200C74B68 /* Products */, 72 | ); 73 | sourceTree = ""; 74 | }; 75 | 1FF47737242D9E6200C74B68 /* Products */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 1FF47736242D9E6200C74B68 /* Button Demo.app */, 79 | 1FF4774C242D9E6600C74B68 /* ButtonTests.xctest */, 80 | ); 81 | name = Products; 82 | sourceTree = ""; 83 | }; 84 | 1FF47738242D9E6200C74B68 /* Button */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 1FF47739242D9E6200C74B68 /* AppDelegate.swift */, 88 | 1FF4775B242E3B8F00C74B68 /* Button.swift */, 89 | 1FADE779243CC0CA00389BCA /* Geometry.swift */, 90 | 1FF4773B242D9E6200C74B68 /* SceneDelegate.swift */, 91 | 1FF477622430EBCD00C74B68 /* ViewController.swift */, 92 | 1FF477632430EBCD00C74B68 /* ViewController.xib */, 93 | 1FF47742242D9E6600C74B68 /* Assets.xcassets */, 94 | 1FF47744242D9E6600C74B68 /* LaunchScreen.storyboard */, 95 | 1FF47747242D9E6600C74B68 /* Info.plist */, 96 | 1FF4775D2430361400C74B68 /* Button-Bridging-Header.h */, 97 | ); 98 | path = Button; 99 | sourceTree = ""; 100 | }; 101 | 1FF4774F242D9E6600C74B68 /* ButtonTests */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 1FF47750242D9E6600C74B68 /* ButtonTests.swift */, 105 | 1FF47752242D9E6600C74B68 /* Info.plist */, 106 | ); 107 | path = ButtonTests; 108 | sourceTree = ""; 109 | }; 110 | /* End PBXGroup section */ 111 | 112 | /* Begin PBXNativeTarget section */ 113 | 1FF47735242D9E6200C74B68 /* Button */ = { 114 | isa = PBXNativeTarget; 115 | buildConfigurationList = 1FF47755242D9E6600C74B68 /* Build configuration list for PBXNativeTarget "Button" */; 116 | buildPhases = ( 117 | 1FF47732242D9E6200C74B68 /* Sources */, 118 | 1FF47733242D9E6200C74B68 /* Frameworks */, 119 | 1FF47734242D9E6200C74B68 /* Resources */, 120 | ); 121 | buildRules = ( 122 | ); 123 | dependencies = ( 124 | ); 125 | name = Button; 126 | productName = Button; 127 | productReference = 1FF47736242D9E6200C74B68 /* Button Demo.app */; 128 | productType = "com.apple.product-type.application"; 129 | }; 130 | 1FF4774B242D9E6600C74B68 /* ButtonTests */ = { 131 | isa = PBXNativeTarget; 132 | buildConfigurationList = 1FF47758242D9E6600C74B68 /* Build configuration list for PBXNativeTarget "ButtonTests" */; 133 | buildPhases = ( 134 | 1FF47748242D9E6600C74B68 /* Sources */, 135 | 1FF47749242D9E6600C74B68 /* Frameworks */, 136 | 1FF4774A242D9E6600C74B68 /* Resources */, 137 | ); 138 | buildRules = ( 139 | ); 140 | dependencies = ( 141 | 1FF4774E242D9E6600C74B68 /* PBXTargetDependency */, 142 | ); 143 | name = ButtonTests; 144 | productName = ButtonTests; 145 | productReference = 1FF4774C242D9E6600C74B68 /* ButtonTests.xctest */; 146 | productType = "com.apple.product-type.bundle.unit-test"; 147 | }; 148 | /* End PBXNativeTarget section */ 149 | 150 | /* Begin PBXProject section */ 151 | 1FF4772E242D9E6200C74B68 /* Project object */ = { 152 | isa = PBXProject; 153 | attributes = { 154 | LastSwiftUpdateCheck = 1140; 155 | LastUpgradeCheck = 1140; 156 | ORGANIZATIONNAME = "Jeff Watkins"; 157 | TargetAttributes = { 158 | 1FF47735242D9E6200C74B68 = { 159 | CreatedOnToolsVersion = 11.4; 160 | LastSwiftMigration = 1140; 161 | }; 162 | 1FF4774B242D9E6600C74B68 = { 163 | CreatedOnToolsVersion = 11.4; 164 | TestTargetID = 1FF47735242D9E6200C74B68; 165 | }; 166 | }; 167 | }; 168 | buildConfigurationList = 1FF47731242D9E6200C74B68 /* Build configuration list for PBXProject "Button" */; 169 | compatibilityVersion = "Xcode 11.4"; 170 | developmentRegion = en; 171 | hasScannedForEncodings = 0; 172 | knownRegions = ( 173 | en, 174 | Base, 175 | ); 176 | mainGroup = 1FF4772D242D9E6200C74B68; 177 | productRefGroup = 1FF47737242D9E6200C74B68 /* Products */; 178 | projectDirPath = ""; 179 | projectRoot = ""; 180 | targets = ( 181 | 1FF47735242D9E6200C74B68 /* Button */, 182 | 1FF4774B242D9E6600C74B68 /* ButtonTests */, 183 | ); 184 | }; 185 | /* End PBXProject section */ 186 | 187 | /* Begin PBXResourcesBuildPhase section */ 188 | 1FF47734242D9E6200C74B68 /* Resources */ = { 189 | isa = PBXResourcesBuildPhase; 190 | buildActionMask = 2147483647; 191 | files = ( 192 | 1FF47746242D9E6600C74B68 /* LaunchScreen.storyboard in Resources */, 193 | 1FF47743242D9E6600C74B68 /* Assets.xcassets in Resources */, 194 | 1FF477652430EBCD00C74B68 /* ViewController.xib in Resources */, 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | }; 198 | 1FF4774A242D9E6600C74B68 /* Resources */ = { 199 | isa = PBXResourcesBuildPhase; 200 | buildActionMask = 2147483647; 201 | files = ( 202 | ); 203 | runOnlyForDeploymentPostprocessing = 0; 204 | }; 205 | /* End PBXResourcesBuildPhase section */ 206 | 207 | /* Begin PBXSourcesBuildPhase section */ 208 | 1FF47732242D9E6200C74B68 /* Sources */ = { 209 | isa = PBXSourcesBuildPhase; 210 | buildActionMask = 2147483647; 211 | files = ( 212 | 1FF477642430EBCD00C74B68 /* ViewController.swift in Sources */, 213 | 1FADE77A243CC0CA00389BCA /* Geometry.swift in Sources */, 214 | 1FF4773A242D9E6200C74B68 /* AppDelegate.swift in Sources */, 215 | 1FF4773C242D9E6200C74B68 /* SceneDelegate.swift in Sources */, 216 | 1FF4775C242E3B8F00C74B68 /* Button.swift in Sources */, 217 | ); 218 | runOnlyForDeploymentPostprocessing = 0; 219 | }; 220 | 1FF47748242D9E6600C74B68 /* Sources */ = { 221 | isa = PBXSourcesBuildPhase; 222 | buildActionMask = 2147483647; 223 | files = ( 224 | 1FF47751242D9E6600C74B68 /* ButtonTests.swift in Sources */, 225 | ); 226 | runOnlyForDeploymentPostprocessing = 0; 227 | }; 228 | /* End PBXSourcesBuildPhase section */ 229 | 230 | /* Begin PBXTargetDependency section */ 231 | 1FF4774E242D9E6600C74B68 /* PBXTargetDependency */ = { 232 | isa = PBXTargetDependency; 233 | target = 1FF47735242D9E6200C74B68 /* Button */; 234 | targetProxy = 1FF4774D242D9E6600C74B68 /* PBXContainerItemProxy */; 235 | }; 236 | /* End PBXTargetDependency section */ 237 | 238 | /* Begin PBXVariantGroup section */ 239 | 1FF47744242D9E6600C74B68 /* LaunchScreen.storyboard */ = { 240 | isa = PBXVariantGroup; 241 | children = ( 242 | 1FF47745242D9E6600C74B68 /* Base */, 243 | ); 244 | name = LaunchScreen.storyboard; 245 | sourceTree = ""; 246 | }; 247 | /* End PBXVariantGroup section */ 248 | 249 | /* Begin XCBuildConfiguration section */ 250 | 1FF47753242D9E6600C74B68 /* Debug */ = { 251 | isa = XCBuildConfiguration; 252 | buildSettings = { 253 | ALWAYS_SEARCH_USER_PATHS = NO; 254 | CLANG_ANALYZER_NONNULL = YES; 255 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 256 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 257 | CLANG_CXX_LIBRARY = "libc++"; 258 | CLANG_ENABLE_MODULES = YES; 259 | CLANG_ENABLE_OBJC_ARC = YES; 260 | CLANG_ENABLE_OBJC_WEAK = YES; 261 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 262 | CLANG_WARN_BOOL_CONVERSION = YES; 263 | CLANG_WARN_COMMA = YES; 264 | CLANG_WARN_CONSTANT_CONVERSION = YES; 265 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 266 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 267 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 268 | CLANG_WARN_EMPTY_BODY = YES; 269 | CLANG_WARN_ENUM_CONVERSION = YES; 270 | CLANG_WARN_INFINITE_RECURSION = YES; 271 | CLANG_WARN_INT_CONVERSION = YES; 272 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 273 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 274 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 275 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 276 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 277 | CLANG_WARN_STRICT_PROTOTYPES = YES; 278 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 279 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 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 = gnu11; 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_UNDECLARED_SELECTOR = YES; 297 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 298 | GCC_WARN_UNUSED_FUNCTION = YES; 299 | GCC_WARN_UNUSED_VARIABLE = YES; 300 | IPHONEOS_DEPLOYMENT_TARGET = 13.4; 301 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 302 | MTL_FAST_MATH = YES; 303 | ONLY_ACTIVE_ARCH = YES; 304 | SDKROOT = iphoneos; 305 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 306 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 307 | }; 308 | name = Debug; 309 | }; 310 | 1FF47754242D9E6600C74B68 /* Release */ = { 311 | isa = XCBuildConfiguration; 312 | buildSettings = { 313 | ALWAYS_SEARCH_USER_PATHS = NO; 314 | CLANG_ANALYZER_NONNULL = YES; 315 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 316 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 317 | CLANG_CXX_LIBRARY = "libc++"; 318 | CLANG_ENABLE_MODULES = YES; 319 | CLANG_ENABLE_OBJC_ARC = YES; 320 | CLANG_ENABLE_OBJC_WEAK = YES; 321 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 322 | CLANG_WARN_BOOL_CONVERSION = YES; 323 | CLANG_WARN_COMMA = YES; 324 | CLANG_WARN_CONSTANT_CONVERSION = YES; 325 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 326 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 327 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 328 | CLANG_WARN_EMPTY_BODY = YES; 329 | CLANG_WARN_ENUM_CONVERSION = YES; 330 | CLANG_WARN_INFINITE_RECURSION = YES; 331 | CLANG_WARN_INT_CONVERSION = YES; 332 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 333 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 334 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 335 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 336 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 337 | CLANG_WARN_STRICT_PROTOTYPES = YES; 338 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 339 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 340 | CLANG_WARN_UNREACHABLE_CODE = YES; 341 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 342 | COPY_PHASE_STRIP = NO; 343 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 344 | ENABLE_NS_ASSERTIONS = NO; 345 | ENABLE_STRICT_OBJC_MSGSEND = YES; 346 | GCC_C_LANGUAGE_STANDARD = gnu11; 347 | GCC_NO_COMMON_BLOCKS = YES; 348 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 349 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 350 | GCC_WARN_UNDECLARED_SELECTOR = YES; 351 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 352 | GCC_WARN_UNUSED_FUNCTION = YES; 353 | GCC_WARN_UNUSED_VARIABLE = YES; 354 | IPHONEOS_DEPLOYMENT_TARGET = 13.4; 355 | MTL_ENABLE_DEBUG_INFO = NO; 356 | MTL_FAST_MATH = YES; 357 | SDKROOT = iphoneos; 358 | SWIFT_COMPILATION_MODE = wholemodule; 359 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 360 | VALIDATE_PRODUCT = YES; 361 | }; 362 | name = Release; 363 | }; 364 | 1FF47756242D9E6600C74B68 /* Debug */ = { 365 | isa = XCBuildConfiguration; 366 | buildSettings = { 367 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 368 | CLANG_ENABLE_MODULES = YES; 369 | CODE_SIGN_STYLE = Automatic; 370 | DEVELOPMENT_TEAM = T2MLW44WP8; 371 | INFOPLIST_FILE = Button/Info.plist; 372 | LD_RUNPATH_SEARCH_PATHS = ( 373 | "$(inherited)", 374 | "@executable_path/Frameworks", 375 | ); 376 | PRODUCT_BUNDLE_IDENTIFIER = dev.jeffwatkins.Button; 377 | PRODUCT_NAME = "Button Demo"; 378 | SWIFT_OBJC_BRIDGING_HEADER = "Button/Button-Bridging-Header.h"; 379 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 380 | SWIFT_VERSION = 5.0; 381 | TARGETED_DEVICE_FAMILY = "1,2"; 382 | }; 383 | name = Debug; 384 | }; 385 | 1FF47757242D9E6600C74B68 /* Release */ = { 386 | isa = XCBuildConfiguration; 387 | buildSettings = { 388 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 389 | CLANG_ENABLE_MODULES = YES; 390 | CODE_SIGN_STYLE = Automatic; 391 | DEVELOPMENT_TEAM = T2MLW44WP8; 392 | INFOPLIST_FILE = Button/Info.plist; 393 | LD_RUNPATH_SEARCH_PATHS = ( 394 | "$(inherited)", 395 | "@executable_path/Frameworks", 396 | ); 397 | PRODUCT_BUNDLE_IDENTIFIER = dev.jeffwatkins.Button; 398 | PRODUCT_NAME = "Button Demo"; 399 | SWIFT_OBJC_BRIDGING_HEADER = "Button/Button-Bridging-Header.h"; 400 | SWIFT_VERSION = 5.0; 401 | TARGETED_DEVICE_FAMILY = "1,2"; 402 | }; 403 | name = Release; 404 | }; 405 | 1FF47759242D9E6600C74B68 /* Debug */ = { 406 | isa = XCBuildConfiguration; 407 | buildSettings = { 408 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 409 | BUNDLE_LOADER = "$(TEST_HOST)"; 410 | CODE_SIGN_STYLE = Automatic; 411 | DEVELOPMENT_TEAM = T2MLW44WP8; 412 | INFOPLIST_FILE = ButtonTests/Info.plist; 413 | IPHONEOS_DEPLOYMENT_TARGET = 13.4; 414 | LD_RUNPATH_SEARCH_PATHS = ( 415 | "$(inherited)", 416 | "@executable_path/Frameworks", 417 | "@loader_path/Frameworks", 418 | ); 419 | PRODUCT_BUNDLE_IDENTIFIER = dev.jeffwatkins.ButtonTests; 420 | PRODUCT_NAME = "$(TARGET_NAME)"; 421 | SWIFT_VERSION = 5.0; 422 | TARGETED_DEVICE_FAMILY = "1,2"; 423 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Button.app/Button"; 424 | }; 425 | name = Debug; 426 | }; 427 | 1FF4775A242D9E6600C74B68 /* Release */ = { 428 | isa = XCBuildConfiguration; 429 | buildSettings = { 430 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 431 | BUNDLE_LOADER = "$(TEST_HOST)"; 432 | CODE_SIGN_STYLE = Automatic; 433 | DEVELOPMENT_TEAM = T2MLW44WP8; 434 | INFOPLIST_FILE = ButtonTests/Info.plist; 435 | IPHONEOS_DEPLOYMENT_TARGET = 13.4; 436 | LD_RUNPATH_SEARCH_PATHS = ( 437 | "$(inherited)", 438 | "@executable_path/Frameworks", 439 | "@loader_path/Frameworks", 440 | ); 441 | PRODUCT_BUNDLE_IDENTIFIER = dev.jeffwatkins.ButtonTests; 442 | PRODUCT_NAME = "$(TARGET_NAME)"; 443 | SWIFT_VERSION = 5.0; 444 | TARGETED_DEVICE_FAMILY = "1,2"; 445 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Button.app/Button"; 446 | }; 447 | name = Release; 448 | }; 449 | /* End XCBuildConfiguration section */ 450 | 451 | /* Begin XCConfigurationList section */ 452 | 1FF47731242D9E6200C74B68 /* Build configuration list for PBXProject "Button" */ = { 453 | isa = XCConfigurationList; 454 | buildConfigurations = ( 455 | 1FF47753242D9E6600C74B68 /* Debug */, 456 | 1FF47754242D9E6600C74B68 /* Release */, 457 | ); 458 | defaultConfigurationIsVisible = 0; 459 | defaultConfigurationName = Release; 460 | }; 461 | 1FF47755242D9E6600C74B68 /* Build configuration list for PBXNativeTarget "Button" */ = { 462 | isa = XCConfigurationList; 463 | buildConfigurations = ( 464 | 1FF47756242D9E6600C74B68 /* Debug */, 465 | 1FF47757242D9E6600C74B68 /* Release */, 466 | ); 467 | defaultConfigurationIsVisible = 0; 468 | defaultConfigurationName = Release; 469 | }; 470 | 1FF47758242D9E6600C74B68 /* Build configuration list for PBXNativeTarget "ButtonTests" */ = { 471 | isa = XCConfigurationList; 472 | buildConfigurations = ( 473 | 1FF47759242D9E6600C74B68 /* Debug */, 474 | 1FF4775A242D9E6600C74B68 /* Release */, 475 | ); 476 | defaultConfigurationIsVisible = 0; 477 | defaultConfigurationName = Release; 478 | }; 479 | /* End XCConfigurationList section */ 480 | }; 481 | rootObject = 1FF4772E242D9E6200C74B68 /* Project object */; 482 | } 483 | -------------------------------------------------------------------------------- /Button.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Button.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Button.xcodeproj/xcshareddata/xcschemes/Button.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /Button/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2020 Jeff Watkins. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | @UIApplicationMain 8 | class AppDelegate: UIResponder, UIApplicationDelegate { 9 | 10 | 11 | 12 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 13 | // Override point for customization after application launch. 14 | return true 15 | } 16 | 17 | // MARK: UISceneSession Lifecycle 18 | 19 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 20 | // Called when a new scene session is being created. 21 | // Use this method to select a configuration to create the new scene with. 22 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 23 | } 24 | 25 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 26 | // Called when the user discards a scene session. 27 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 28 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 29 | } 30 | 31 | 32 | } 33 | 34 | -------------------------------------------------------------------------------- /Button/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Button/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Button/Assets.xcassets/Horizontal Reversed.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Horizontal Reversed.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Horizontal Reversed@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Horizontal Reversed@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Button/Assets.xcassets/Horizontal Reversed.imageset/Horizontal Reversed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffwatkins/ButtonControlSample/74968f1f247b14b4a63541f6738d044b22dd33ea/Button/Assets.xcassets/Horizontal Reversed.imageset/Horizontal Reversed.png -------------------------------------------------------------------------------- /Button/Assets.xcassets/Horizontal Reversed.imageset/Horizontal Reversed@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffwatkins/ButtonControlSample/74968f1f247b14b4a63541f6738d044b22dd33ea/Button/Assets.xcassets/Horizontal Reversed.imageset/Horizontal Reversed@2x.png -------------------------------------------------------------------------------- /Button/Assets.xcassets/Horizontal Reversed.imageset/Horizontal Reversed@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffwatkins/ButtonControlSample/74968f1f247b14b4a63541f6738d044b22dd33ea/Button/Assets.xcassets/Horizontal Reversed.imageset/Horizontal Reversed@3x.png -------------------------------------------------------------------------------- /Button/Assets.xcassets/Horizontal.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Horizontal.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Horizontal@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Horizontal@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Button/Assets.xcassets/Horizontal.imageset/Horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffwatkins/ButtonControlSample/74968f1f247b14b4a63541f6738d044b22dd33ea/Button/Assets.xcassets/Horizontal.imageset/Horizontal.png -------------------------------------------------------------------------------- /Button/Assets.xcassets/Horizontal.imageset/Horizontal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffwatkins/ButtonControlSample/74968f1f247b14b4a63541f6738d044b22dd33ea/Button/Assets.xcassets/Horizontal.imageset/Horizontal@2x.png -------------------------------------------------------------------------------- /Button/Assets.xcassets/Horizontal.imageset/Horizontal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffwatkins/ButtonControlSample/74968f1f247b14b4a63541f6738d044b22dd33ea/Button/Assets.xcassets/Horizontal.imageset/Horizontal@3x.png -------------------------------------------------------------------------------- /Button/Assets.xcassets/Vertical Reversed.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Vertical Reversed.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Vertical Reversed@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Vertical Reversed@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Button/Assets.xcassets/Vertical Reversed.imageset/Vertical Reversed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffwatkins/ButtonControlSample/74968f1f247b14b4a63541f6738d044b22dd33ea/Button/Assets.xcassets/Vertical Reversed.imageset/Vertical Reversed.png -------------------------------------------------------------------------------- /Button/Assets.xcassets/Vertical Reversed.imageset/Vertical Reversed@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffwatkins/ButtonControlSample/74968f1f247b14b4a63541f6738d044b22dd33ea/Button/Assets.xcassets/Vertical Reversed.imageset/Vertical Reversed@2x.png -------------------------------------------------------------------------------- /Button/Assets.xcassets/Vertical Reversed.imageset/Vertical Reversed@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffwatkins/ButtonControlSample/74968f1f247b14b4a63541f6738d044b22dd33ea/Button/Assets.xcassets/Vertical Reversed.imageset/Vertical Reversed@3x.png -------------------------------------------------------------------------------- /Button/Assets.xcassets/Vertical.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Vertical.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Vertical@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Vertical@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Button/Assets.xcassets/Vertical.imageset/Vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffwatkins/ButtonControlSample/74968f1f247b14b4a63541f6738d044b22dd33ea/Button/Assets.xcassets/Vertical.imageset/Vertical.png -------------------------------------------------------------------------------- /Button/Assets.xcassets/Vertical.imageset/Vertical@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffwatkins/ButtonControlSample/74968f1f247b14b4a63541f6738d044b22dd33ea/Button/Assets.xcassets/Vertical.imageset/Vertical@2x.png -------------------------------------------------------------------------------- /Button/Assets.xcassets/Vertical.imageset/Vertical@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffwatkins/ButtonControlSample/74968f1f247b14b4a63541f6738d044b22dd33ea/Button/Assets.xcassets/Vertical.imageset/Vertical@3x.png -------------------------------------------------------------------------------- /Button/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Button/Button-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | -------------------------------------------------------------------------------- /Button/Button.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2020 Jeff Watkins. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | @IBDesignable 8 | open class Button: UIControl { 9 | 10 | @objc 11 | public enum ContentLayout: Int { 12 | /// Arrange content horizontally with the traditional layout of icon followed by title. 13 | case horizontal 14 | /// Arrange content horizontally with the reverse of the traditional layout with the title followed by the icon. 15 | case horizontalReversed 16 | /// Arrange content vertically with the icon followed by the title. 17 | case vertical 18 | /// Arrange content vertically with the title followed by the icon. 19 | case verticalReversed 20 | } 21 | 22 | /// The corner radius of a button. 23 | let cornerRadius = CGFloat(8) 24 | 25 | var imageView: UIImageView? = nil 26 | var titleLabel: UILabel? = nil 27 | var subtitleLabel: UILabel? = nil 28 | var backgroundView: UIView? = nil 29 | 30 | /// Overridden background colour to determine the pill background colour 31 | var _buttonBackgroundColor: UIColor? 32 | public override var backgroundColor: UIColor? { 33 | get { _buttonBackgroundColor } 34 | set { 35 | _buttonBackgroundColor = newValue 36 | self.updateBackgroundView() 37 | } 38 | } 39 | 40 | @IBInspectable 41 | public var borderColor: UIColor? { 42 | didSet { 43 | self.updateBackgroundView() 44 | } 45 | } 46 | 47 | @IBInspectable 48 | public var image: UIImage? { 49 | didSet { 50 | self.updateImageView() 51 | } 52 | } 53 | 54 | @IBInspectable 55 | public var title: String? { 56 | didSet { 57 | self.updateTitleViews() 58 | } 59 | } 60 | 61 | @IBInspectable 62 | public var subtitle: String? { 63 | didSet { 64 | self.updateTitleViews() 65 | } 66 | } 67 | 68 | @IBInspectable 69 | public var contentLayout: ContentLayout = ContentLayout.horizontal { 70 | didSet { 71 | self.setNeedsUpdateConstraints() 72 | } 73 | } 74 | 75 | public override func prepareForInterfaceBuilder() { 76 | super.prepareForInterfaceBuilder() 77 | self.title = "Title" 78 | self.subtitle = "Subtitle" 79 | self.image = UIImage(systemName: "questionmark.circle.fill") 80 | self.backgroundColor = self.tintColor 81 | self.tintColor = UIColor.systemBackground 82 | } 83 | 84 | /// The current background colour based on the active state 85 | var currentBackgroundColor: UIColor? { 86 | guard let backgroundColor = self._buttonBackgroundColor else { return nil } 87 | 88 | guard self.isEnabled else { return UIColor.quaternaryLabel } 89 | if self.isHighlighted { 90 | return backgroundColor.withAlphaComponent(0.5) 91 | } 92 | return backgroundColor 93 | } 94 | 95 | var currentBorderColor: UIColor? { 96 | guard let borderColor = self.borderColor else { return nil } 97 | 98 | guard self.isEnabled else { return UIColor.tertiaryLabel } 99 | if self.isHighlighted { 100 | return borderColor.withAlphaComponent(0.75) 101 | } 102 | return borderColor 103 | } 104 | 105 | var currentTextColor: UIColor { 106 | let textColor = self.tintColor! 107 | 108 | guard self.isEnabled else { return UIColor.tertiaryLabel } 109 | if self.isHighlighted { 110 | return textColor.withAlphaComponent(0.75) 111 | } 112 | return textColor 113 | } 114 | 115 | public override init(frame: CGRect) { 116 | super.init(frame: frame) 117 | self.accessibilityTraits = UIAccessibilityTraits.button 118 | self.isAccessibilityElement = true 119 | } 120 | 121 | public required init?(coder: NSCoder) { 122 | super.init(coder: coder) 123 | if let title = coder.decodeObject(forKey: "title") as? String { 124 | self.title = title 125 | } 126 | if let subtitle = coder.decodeObject(forKey: "subtitle") as? String { 127 | self.subtitle = subtitle 128 | } 129 | if let image = coder.decodeObject(of: UIImage.self, forKey: "image") { 130 | self.image = image 131 | } 132 | if let borderColor = coder.decodeObject(of: UIColor.self, forKey: "borderColor") { 133 | self.borderColor = borderColor 134 | } 135 | self.accessibilityTraits = UIAccessibilityTraits.button 136 | self.isAccessibilityElement = true 137 | } 138 | 139 | public override func encode(with coder: NSCoder) { 140 | super.encode(with: coder) 141 | coder.encodeConditionalObject(self.title, forKey: "title") 142 | coder.encodeConditionalObject(self.subtitle, forKey: "subtitle") 143 | coder.encodeConditionalObject(self.image, forKey: "image") 144 | coder.encodeConditionalObject(self.borderColor, forKey: "borderColor") 145 | } 146 | 147 | func updateBackgroundView() { 148 | if self.backgroundView == nil { 149 | let backgroundView = UIView() 150 | backgroundView.isUserInteractionEnabled = false 151 | backgroundView.translatesAutoresizingMaskIntoConstraints = false 152 | if let last = self.subviews.last { 153 | self.insertSubview(backgroundView, belowSubview: last) 154 | } else { 155 | self.addSubview(backgroundView) 156 | } 157 | NSLayoutConstraint.activate([ 158 | backgroundView.leadingAnchor.constraint(equalTo: self.leadingAnchor), 159 | backgroundView.topAnchor.constraint(equalTo: self.topAnchor), 160 | backgroundView.trailingAnchor.constraint(equalTo: self.trailingAnchor), 161 | backgroundView.bottomAnchor.constraint(equalTo: self.bottomAnchor) 162 | ]) 163 | 164 | self.backgroundView = backgroundView 165 | } 166 | 167 | guard let backgroundView = self.backgroundView else { return } 168 | backgroundView.layer.cornerRadius = self.cornerRadius 169 | 170 | if let buttonBackgroundColor = self.currentBackgroundColor { 171 | backgroundView.backgroundColor = buttonBackgroundColor 172 | } else { 173 | backgroundView.backgroundColor = nil 174 | } 175 | 176 | if let borderColor = self.currentBorderColor { 177 | backgroundView.layer.borderWidth = 1 178 | backgroundView.layer.borderColor = borderColor.cgColor 179 | backgroundView.layer.cornerCurve = .continuous 180 | } else { 181 | backgroundView.layer.borderColor = nil 182 | } 183 | } 184 | 185 | var imageConstraints = [NSLayoutConstraint]() 186 | var imageWidthConstraint: NSLayoutConstraint? 187 | 188 | func updateImageHeightConstraint() { 189 | guard let imageView = self.imageView else { return } 190 | guard let image = imageView.image else { return } 191 | 192 | let titleFont: UIFont 193 | 194 | if let titleLabel = self.titleLabel { 195 | titleFont = titleLabel.font 196 | } else { 197 | titleFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body) 198 | } 199 | 200 | let lineHeight = titleFont.lineHeight 201 | 202 | let size = image.size 203 | let aspectRatio = size.width / size.height 204 | let width = lineHeight * aspectRatio 205 | 206 | if self.imageWidthConstraint == nil { 207 | self.imageWidthConstraint = imageView.widthAnchor.constraint(equalToConstant: width) 208 | self.imageWidthConstraint?.priority = UILayoutPriority.defaultHigh 209 | self.imageWidthConstraint?.isActive = true 210 | } else { 211 | self.imageWidthConstraint?.constant = width 212 | } 213 | } 214 | 215 | func updateImageView() { 216 | if self.imageView == nil { 217 | let imageView = UIImageView() 218 | imageView.translatesAutoresizingMaskIntoConstraints = false 219 | imageView.tintColor = self.currentTextColor 220 | imageView.setContentCompressionResistancePriority(UILayoutPriority.required, for: NSLayoutConstraint.Axis.horizontal) 221 | 222 | if let backgroundView = self.backgroundView { 223 | self.insertSubview(imageView, aboveSubview: backgroundView) 224 | } else { 225 | self.addSubview(imageView) 226 | } 227 | self.setNeedsUpdateConstraints() 228 | self.imageView = imageView 229 | } 230 | 231 | guard let imageView = self.imageView else { return } 232 | 233 | NSLayoutConstraint.deactivate(self.imageConstraints) 234 | 235 | if let image = self.image { 236 | imageView.image = image 237 | let size = image.size 238 | let aspectRatio = size.height / size.width 239 | self.imageConstraints = [ 240 | imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: aspectRatio) 241 | ] 242 | NSLayoutConstraint.activate(self.imageConstraints) 243 | self.updateImageHeightConstraint() 244 | } else { 245 | imageView.image = nil 246 | } 247 | 248 | } 249 | 250 | let largestContentSizeCategory = UITraitCollection(preferredContentSizeCategory: UIContentSizeCategory.accessibilityMedium) 251 | let normalContentSizeCategory = UITraitCollection(preferredContentSizeCategory: UIContentSizeCategory.large) 252 | 253 | var titleFont: UIFont { 254 | let largestFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body, compatibleWith: self.largestContentSizeCategory) 255 | let baseFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body, compatibleWith: self.normalContentSizeCategory) 256 | 257 | return UIFontMetrics(forTextStyle: UIFont.TextStyle.body).scaledFont(for: baseFont, maximumPointSize: largestFont.pointSize, compatibleWith: self.normalContentSizeCategory) 258 | } 259 | 260 | var subtitleFont: UIFont { 261 | let largestFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.footnote, compatibleWith: self.largestContentSizeCategory) 262 | let baseFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.footnote, compatibleWith: self.normalContentSizeCategory) 263 | 264 | return UIFontMetrics(forTextStyle: UIFont.TextStyle.footnote).scaledFont(for: baseFont, maximumPointSize: largestFont.pointSize, compatibleWith: self.normalContentSizeCategory) 265 | } 266 | 267 | func updateTitleViews() { 268 | var accessibilityParts = [String]() 269 | 270 | if self.title != nil && self.titleLabel == nil { 271 | let titleLabel = UILabel() 272 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 273 | titleLabel.font = self.titleFont 274 | titleLabel.adjustsFontForContentSizeCategory = true 275 | titleLabel.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh - 10, for: NSLayoutConstraint.Axis.horizontal) 276 | titleLabel.textColor = self.currentTextColor 277 | titleLabel.textAlignment = NSTextAlignment.center 278 | titleLabel.numberOfLines = 0 279 | self.titleLabel = titleLabel 280 | if let backgroundView = self.backgroundView { 281 | self.insertSubview(titleLabel, aboveSubview: backgroundView) 282 | } else { 283 | self.addSubview(titleLabel) 284 | } 285 | } 286 | 287 | if self.subtitle != nil && self.subtitleLabel == nil { 288 | let subtitleLabel = UILabel() 289 | subtitleLabel.translatesAutoresizingMaskIntoConstraints = false 290 | subtitleLabel.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh - 10, for: NSLayoutConstraint.Axis.horizontal) 291 | subtitleLabel.font = self.subtitleFont 292 | subtitleLabel.adjustsFontForContentSizeCategory = true 293 | subtitleLabel.textColor = self.currentTextColor 294 | subtitleLabel.textAlignment = NSTextAlignment.center 295 | subtitleLabel.numberOfLines = 0 296 | self.subtitleLabel = subtitleLabel 297 | if let backgroundView = self.backgroundView { 298 | self.insertSubview(subtitleLabel, aboveSubview: backgroundView) 299 | } else { 300 | self.addSubview(subtitleLabel) 301 | } 302 | } 303 | 304 | if let title = self.title, let titleLabel = self.titleLabel { 305 | titleLabel.text = title 306 | accessibilityParts.append(title) 307 | } 308 | 309 | if let subtitle = self.subtitle, let subtitleLabel = self.subtitleLabel { 310 | subtitleLabel.text = subtitle 311 | accessibilityParts.append(subtitle) 312 | } 313 | 314 | self.accessibilityLabel = accessibilityParts.joined(separator: "\n\n") 315 | } 316 | 317 | // MARK: - constraint handling 318 | lazy var titleLayoutGuide: UILayoutGuide = { 319 | let guide = UILayoutGuide() 320 | guide.identifier = "title layout guide" 321 | self.addLayoutGuide(guide) 322 | return guide 323 | }() 324 | 325 | lazy var contentLayoutGuide: UILayoutGuide = { 326 | let guide = UILayoutGuide() 327 | guide.identifier = "content layout guide" 328 | self.addLayoutGuide(guide) 329 | return guide 330 | }() 331 | 332 | var buttonConstraints = [NSLayoutConstraint]() 333 | 334 | public override func setNeedsUpdateConstraints() { 335 | NSLayoutConstraint.deactivate(self.buttonConstraints) 336 | self.buttonConstraints = [] 337 | super.setNeedsUpdateConstraints() 338 | } 339 | 340 | func constraintsForTitleLabelsInLayoutGuide() -> [NSLayoutConstraint] { 341 | let titleLayoutGuide = self.titleLayoutGuide 342 | var constraints: [NSLayoutConstraint] = [] 343 | 344 | if let titleLabel = self.titleLabel { 345 | constraints += [ 346 | titleLabel.topAnchor.constraint(equalTo: titleLayoutGuide.topAnchor), 347 | titleLabel.leadingAnchor.constraint(equalTo: titleLayoutGuide.leadingAnchor), 348 | titleLabel.trailingAnchor.constraint(equalTo: titleLayoutGuide.trailingAnchor), 349 | ] 350 | 351 | // When there's a title & subtitle, make them the same width and constrain their baselines. Othewise, constrain the bottom of the title label 352 | if let subtitleLabel = self.subtitleLabel { 353 | constraints += [ 354 | titleLabel.widthAnchor.constraint(equalTo: subtitleLabel.widthAnchor, multiplier: 1), 355 | subtitleLabel.firstBaselineAnchor.constraint(equalToSystemSpacingBelow: titleLabel.lastBaselineAnchor, multiplier: 1) 356 | ] 357 | } else { 358 | constraints += [ 359 | titleLabel.bottomAnchor.constraint(equalTo: titleLayoutGuide.bottomAnchor) 360 | ] 361 | } 362 | } 363 | 364 | if let subtitleLabel = self.subtitleLabel { 365 | constraints += [ 366 | subtitleLabel.leadingAnchor.constraint(equalTo: titleLayoutGuide.leadingAnchor), 367 | subtitleLabel.trailingAnchor.constraint(equalTo: titleLayoutGuide.trailingAnchor), 368 | subtitleLabel.bottomAnchor.constraint(equalTo: titleLayoutGuide.bottomAnchor) 369 | ] 370 | 371 | // If titleLabel is nil, we'll need to constrain subtitle label to the top of titleLayoutGuide 372 | if self.titleLabel == nil { 373 | constraints += [ 374 | subtitleLabel.topAnchor.constraint(equalTo: titleLayoutGuide.topAnchor) 375 | ] 376 | } 377 | } 378 | 379 | // Ensure the titleLayoutGuide is within the contentLayoutGuide and within the button. 380 | constraints += [ 381 | titleLayoutGuide.leadingAnchor.constraint(greaterThanOrEqualTo: self.leadingAnchor), 382 | titleLayoutGuide.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor), 383 | titleLayoutGuide.topAnchor.constraint(greaterThanOrEqualTo: contentLayoutGuide.topAnchor), 384 | titleLayoutGuide.bottomAnchor.constraint(lessThanOrEqualTo: contentLayoutGuide.bottomAnchor), 385 | titleLayoutGuide.topAnchor.constraint(greaterThanOrEqualTo: self.topAnchor), 386 | titleLayoutGuide.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor), 387 | ] 388 | 389 | return constraints 390 | } 391 | 392 | func constraintsForContentLayoutGuide() -> [NSLayoutConstraint] { 393 | let contentLayoutGuide = self.contentLayoutGuide 394 | return [ 395 | contentLayoutGuide.centerYAnchor.constraint(equalTo: self.centerYAnchor), 396 | contentLayoutGuide.centerXAnchor.constraint(equalTo: self.centerXAnchor), 397 | contentLayoutGuide.topAnchor.constraint(greaterThanOrEqualTo: self.topAnchor), 398 | contentLayoutGuide.leadingAnchor.constraint(greaterThanOrEqualTo: self.leadingAnchor), 399 | contentLayoutGuide.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor), 400 | contentLayoutGuide.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor) 401 | ] 402 | } 403 | 404 | func constraintsForBackgroundView() -> [NSLayoutConstraint] { 405 | guard let backgroundView = self.backgroundView else { return [] } 406 | 407 | let cornerRadius = self.cornerRadius 408 | let halfRadius = CGFloat.floorToPixel(cornerRadius / 2.0) 409 | let contentLayoutGuide = self.contentLayoutGuide 410 | 411 | return [ 412 | contentLayoutGuide.leadingAnchor.constraint(greaterThanOrEqualTo: backgroundView.leadingAnchor, constant: cornerRadius), 413 | contentLayoutGuide.topAnchor.constraint(greaterThanOrEqualTo: backgroundView.topAnchor, constant: halfRadius), 414 | backgroundView.trailingAnchor.constraint(greaterThanOrEqualTo: contentLayoutGuide.trailingAnchor, constant: cornerRadius), 415 | backgroundView.bottomAnchor.constraint(greaterThanOrEqualTo: contentLayoutGuide.bottomAnchor, constant: halfRadius) 416 | ] 417 | } 418 | 419 | func createHorizontalConstraints() { 420 | var constraints: [NSLayoutConstraint] = [] 421 | 422 | let contentLayoutGuide = self.contentLayoutGuide 423 | let reversed = (self.contentLayout == .horizontalReversed) 424 | 425 | if let imageView = self.imageView { 426 | if reversed { 427 | constraints += [ 428 | imageView.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor) 429 | ] 430 | } else { 431 | constraints += [ 432 | imageView.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor) 433 | ] 434 | } 435 | 436 | constraints += [ 437 | imageView.leadingAnchor.constraint(greaterThanOrEqualTo: self.leadingAnchor), 438 | imageView.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor), 439 | imageView.topAnchor.constraint(greaterThanOrEqualTo: self.topAnchor), 440 | imageView.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor) 441 | ] 442 | 443 | // When there's a title or subtitle, we need to constrain the image against the titleLayoutGuide, otherwise, fully constrain the image against the contentLayoutGuide 444 | if self.titleLabel != nil || self.subtitleLabel != nil { 445 | let titleLayoutGuide = self.titleLayoutGuide 446 | if reversed { 447 | constraints += [ 448 | imageView.leadingAnchor.constraint(equalToSystemSpacingAfter: titleLayoutGuide.trailingAnchor, multiplier: 1), 449 | titleLayoutGuide.centerYAnchor.constraint(equalTo: imageView.centerYAnchor) 450 | ] 451 | } else { 452 | constraints += [ 453 | titleLayoutGuide.leadingAnchor.constraint(equalToSystemSpacingAfter: imageView.trailingAnchor, multiplier: 1), 454 | titleLayoutGuide.centerYAnchor.constraint(equalTo: imageView.centerYAnchor) 455 | ] 456 | } 457 | } else { 458 | constraints += [ 459 | imageView.centerYAnchor.constraint(equalTo: contentLayoutGuide.centerYAnchor), 460 | imageView.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor) 461 | ] 462 | } 463 | } 464 | 465 | if self.titleLabel != nil || self.subtitleLabel != nil { 466 | let titleLayoutGuide = self.titleLayoutGuide 467 | constraints += self.constraintsForTitleLabelsInLayoutGuide() 468 | 469 | // Now constrain titleLayoutGuide against contentLayoutGuide 470 | constraints += [ 471 | titleLayoutGuide.centerYAnchor.constraint(greaterThanOrEqualTo: contentLayoutGuide.centerYAnchor) 472 | ] 473 | 474 | if reversed { 475 | constraints += [ 476 | titleLayoutGuide.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor) 477 | ] 478 | } else { 479 | constraints += [ 480 | titleLayoutGuide.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor) 481 | ] 482 | } 483 | 484 | // If there's no image then constrain against the remaining edge to the content layout guide 485 | if self.imageView == nil { 486 | if reversed { 487 | constraints += [ 488 | titleLayoutGuide.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor) 489 | ] 490 | } else { 491 | constraints += [ 492 | titleLayoutGuide.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor) 493 | ] 494 | } 495 | } 496 | } 497 | 498 | constraints += self.constraintsForContentLayoutGuide() 499 | constraints += self.constraintsForBackgroundView() 500 | 501 | NSLayoutConstraint.activate(constraints) 502 | self.buttonConstraints = constraints 503 | } 504 | 505 | func createVerticalConstraints() { 506 | var constraints: [NSLayoutConstraint] = [] 507 | 508 | let contentLayoutGuide = self.contentLayoutGuide 509 | let reversed = (self.contentLayout == .verticalReversed) 510 | 511 | if let imageView = self.imageView { 512 | constraints += [ 513 | imageView.leadingAnchor.constraint(greaterThanOrEqualTo: self.leadingAnchor), 514 | imageView.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor), 515 | imageView.topAnchor.constraint(greaterThanOrEqualTo: self.topAnchor), 516 | imageView.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor), 517 | imageView.centerXAnchor.constraint(equalTo: contentLayoutGuide.centerXAnchor) 518 | ] 519 | 520 | if reversed { 521 | constraints += [ 522 | imageView.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor) 523 | ] 524 | } else { 525 | constraints += [ 526 | imageView.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor) 527 | ] 528 | } 529 | 530 | // Apply constraints appropriate to an image with a title and/or subtitle 531 | if self.titleLabel != nil || self.subtitleLabel != nil { 532 | let titleLayoutGuide = self.titleLayoutGuide 533 | 534 | constraints += [ 535 | imageView.centerXAnchor.constraint(equalTo: titleLayoutGuide.centerXAnchor) 536 | ] 537 | 538 | if reversed { 539 | if let lastBaselineLabel = self.subtitleLabel ?? self.titleLabel { 540 | constraints += [ 541 | imageView.topAnchor.constraint(equalToSystemSpacingBelow: lastBaselineLabel.lastBaselineAnchor, multiplier: 1) 542 | ] 543 | } 544 | } else { 545 | if let firstBaselineLabel = self.titleLabel ?? self.subtitleLabel { 546 | constraints += [ 547 | firstBaselineLabel.firstBaselineAnchor.constraint(equalToSystemSpacingBelow: imageView.bottomAnchor, multiplier: 1) 548 | ] 549 | } 550 | } 551 | } 552 | } 553 | 554 | if self.titleLabel != nil || self.subtitleLabel != nil { 555 | let titleLayoutGuide = self.titleLayoutGuide 556 | constraints += self.constraintsForTitleLabelsInLayoutGuide() 557 | 558 | // Now constrain titleLayoutGuide against contentLayoutGuide 559 | constraints += [ 560 | titleLayoutGuide.centerXAnchor.constraint(greaterThanOrEqualTo: contentLayoutGuide.centerXAnchor), 561 | titleLayoutGuide.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor), 562 | titleLayoutGuide.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor) 563 | ] 564 | 565 | if reversed { 566 | constraints += [ 567 | titleLayoutGuide.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor) 568 | ] 569 | } else { 570 | constraints += [ 571 | titleLayoutGuide.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor) 572 | ] 573 | } 574 | 575 | if imageView == nil { 576 | if reversed { 577 | constraints += [ 578 | titleLayoutGuide.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor) 579 | ] 580 | } else { 581 | constraints += [ 582 | titleLayoutGuide.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor) 583 | ] 584 | } 585 | } 586 | } 587 | 588 | constraints += self.constraintsForContentLayoutGuide() 589 | constraints += self.constraintsForBackgroundView() 590 | 591 | NSLayoutConstraint.activate(constraints) 592 | self.buttonConstraints = constraints 593 | } 594 | 595 | public override func updateConstraints() { 596 | guard self.buttonConstraints.isEmpty else { super.updateConstraints(); return } 597 | 598 | // Update constraints 599 | switch self.contentLayout { 600 | case .horizontal, .horizontalReversed: 601 | self.createHorizontalConstraints() 602 | case .vertical, .verticalReversed: 603 | self.createVerticalConstraints() 604 | } 605 | 606 | super.updateConstraints() 607 | } 608 | 609 | 610 | func updateColors() { 611 | let textColor = self.currentTextColor 612 | if let imageView = self.imageView { 613 | imageView.tintColor = textColor 614 | } 615 | if let titleLabel = self.titleLabel { 616 | titleLabel.textColor = textColor 617 | } 618 | if let subtitleLabel = self.subtitleLabel { 619 | subtitleLabel.textColor = textColor 620 | } 621 | if self.backgroundView != nil { 622 | self.updateBackgroundView() 623 | } 624 | } 625 | 626 | public override func tintColorDidChange() { 627 | super.tintColorDidChange() 628 | self.updateColors() 629 | } 630 | 631 | public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 632 | guard previousTraitCollection?.preferredContentSizeCategory != self.traitCollection.preferredContentSizeCategory else { return } 633 | self.updateImageHeightConstraint() 634 | } 635 | 636 | // MARK: - control overrides 637 | public override var isHighlighted: Bool { 638 | didSet { 639 | self.updateColors() 640 | } 641 | } 642 | 643 | public override var isEnabled: Bool { 644 | didSet { 645 | self.updateColors() 646 | } 647 | } 648 | 649 | @objc public var _controlEventsForActionTriggered: UIControl.Event { 650 | return [UIControl.Event.touchUpInside] 651 | } 652 | 653 | } 654 | -------------------------------------------------------------------------------- /Button/Geometry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2020 Jeff Watkins. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | extension UIEdgeInsets { 8 | init(all dimension: CGFloat) { 9 | self.init(top: dimension, left: dimension, bottom: dimension, right: dimension) 10 | } 11 | } 12 | 13 | extension CGFloat { 14 | static var onePixel: CGFloat = { 15 | let screenScale = UIScreen.main.scale 16 | return 1.0 / screenScale 17 | }() 18 | 19 | static func floorToPixel(_ value: CGFloat) -> CGFloat { 20 | let screenScale = UIScreen.main.scale 21 | return floor(value * screenScale) / screenScale 22 | } 23 | 24 | static func ceilToPixel(_ value: CGFloat) -> CGFloat { 25 | let screenScale = UIScreen.main.scale 26 | return ceil(value * screenScale) / screenScale 27 | } 28 | } 29 | 30 | extension CGSize { 31 | static var onePixel: CGSize = { 32 | return CGSize(width: .onePixel, height: .onePixel) 33 | }() 34 | 35 | func floorToPixel() -> CGSize { 36 | return CGSize(width: CGFloat.floorToPixel(self.width), height: CGFloat.floorToPixel(self.height)) 37 | } 38 | 39 | func ceilToPixel() -> CGSize { 40 | return CGSize(width: CGFloat.ceilToPixel(self.width), height: CGFloat.ceilToPixel(self.height)) 41 | } 42 | } 43 | 44 | extension CGPoint { 45 | static var onePixel: CGPoint = { 46 | return CGPoint(x: .onePixel, y: .onePixel) 47 | }() 48 | 49 | func floorToPixel() -> CGPoint { 50 | return CGPoint(x: CGFloat.floorToPixel(self.x), y: CGFloat.floorToPixel(self.y)) 51 | } 52 | 53 | func ceilToPixel() -> CGPoint { 54 | return CGPoint(x: CGFloat.ceilToPixel(self.x), y: CGFloat.ceilToPixel(self.y)) 55 | } 56 | } 57 | 58 | extension CGRect { 59 | static var onePixel: CGRect = { 60 | return CGRect(x: .onePixel, y: .onePixel, width: .onePixel, height: .onePixel) 61 | }() 62 | 63 | func floorToPixel() -> CGRect { 64 | return CGRect(x: CGFloat.floorToPixel(self.minX), y: CGFloat.floorToPixel(self.minY), width: CGFloat.floorToPixel(self.width), height: CGFloat.floorToPixel(self.height)) 65 | } 66 | 67 | func ceilToPixel() -> CGRect { 68 | return CGRect(x: CGFloat.ceilToPixel(self.minX), y: CGFloat.ceilToPixel(self.minY), width: CGFloat.ceilToPixel(self.width), height: CGFloat.ceilToPixel(self.height)) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Button/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Button Demo 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIMainStoryboardFile 43 | 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UISupportedInterfaceOrientations~ipad 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationPortraitUpsideDown 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Button/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2020 Jeff Watkins. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 8 | 9 | var window: UIWindow? 10 | 11 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 12 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 13 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 14 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 15 | guard let windowScene = (scene as? UIWindowScene) else { return } 16 | let controller = ViewController() 17 | controller.title = "Button Demo" 18 | let navigationController = UINavigationController(rootViewController: controller) 19 | self.window = UIWindow(windowScene: windowScene) 20 | self.window?.rootViewController = navigationController 21 | self.window?.makeKeyAndVisible() 22 | } 23 | 24 | func sceneDidDisconnect(_ scene: UIScene) { 25 | // Called as the scene is being released by the system. 26 | // This occurs shortly after the scene enters the background, or when its session is discarded. 27 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 28 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 29 | } 30 | 31 | func sceneDidBecomeActive(_ scene: UIScene) { 32 | // Called when the scene has moved from an inactive state to an active state. 33 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 34 | } 35 | 36 | func sceneWillResignActive(_ scene: UIScene) { 37 | // Called when the scene will move from an active state to an inactive state. 38 | // This may occur due to temporary interruptions (ex. an incoming phone call). 39 | } 40 | 41 | func sceneWillEnterForeground(_ scene: UIScene) { 42 | // Called as the scene transitions from the background to the foreground. 43 | // Use this method to undo the changes made on entering the background. 44 | } 45 | 46 | func sceneDidEnterBackground(_ scene: UIScene) { 47 | // Called as the scene transitions from the foreground to the background. 48 | // Use this method to save data, release shared resources, and store enough scene-specific state information 49 | // to restore the scene back to its current state. 50 | } 51 | 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /Button/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2020 Jeff Watkins. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | class ViewController: UIViewController { 8 | 9 | @IBOutlet var button: Button! 10 | @IBOutlet var buttonTypeSelector: UIStackView! 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | self.showContentLayout(.horizontal) 15 | 16 | self.button.addTarget(self, action: #selector(doSomething), for: UIControl.Event.primaryActionTriggered) 17 | } 18 | 19 | func showContentLayout(_ contentLayout: Button.ContentLayout) { 20 | let previousButtonIndex = self.button.contentLayout.rawValue 21 | self.buttonTypeSelector.arrangedSubviews[previousButtonIndex].tintColor = self.view.tintColor 22 | let newButtonIndex = contentLayout.rawValue 23 | self.buttonTypeSelector.arrangedSubviews[newButtonIndex].tintColor = UIColor.label 24 | self.button.contentLayout = contentLayout 25 | } 26 | 27 | func contentLayoutFromButtonSelector(_ button: UIButton) -> Button.ContentLayout { 28 | guard let buttonIndex = self.buttonTypeSelector.arrangedSubviews.firstIndex(of: button) else { return .horizontal } 29 | if let contentLayout = Button.ContentLayout(rawValue: buttonIndex) { 30 | return contentLayout 31 | } 32 | return .horizontal 33 | } 34 | 35 | @IBAction func changeButtonType(_ sender: UIButton) { 36 | let contentLayout = self.contentLayoutFromButtonSelector(sender) 37 | self.showContentLayout(contentLayout) 38 | } 39 | 40 | @IBAction func doSomething(_ sender: AnyObject) { 41 | print("WOOP") 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Button/ViewController.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 | 47 | 48 | 49 | 50 | 57 | 64 | 71 | 78 | 79 | 80 | 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 | -------------------------------------------------------------------------------- /ButtonTests/ButtonTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2020 Jeff Watkins. All rights reserved. 3 | // 4 | 5 | import XCTest 6 | @testable import Button 7 | 8 | class ButtonTests: XCTestCase { 9 | 10 | override func setUpWithError() throws { 11 | // Put setup code here. This method is called before the invocation of each test method in the class. 12 | } 13 | 14 | override func tearDownWithError() throws { 15 | // Put teardown code here. This method is called after the invocation of each test method in the class. 16 | } 17 | 18 | func testExample() throws { 19 | // This is an example of a functional test case. 20 | // Use XCTAssert and related functions to verify your tests produce the correct results. 21 | } 22 | 23 | func testPerformanceExample() throws { 24 | // This is an example of a performance test case. 25 | self.measure { 26 | // Put the code you want to measure the time of here. 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /ButtonTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | --------------------------------------------------------------------------------