├── README.md ├── Sports-UI-Demo.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── sebvidal.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist └── Sports-UI-Demo ├── Base ├── SUIAppDelegate │ └── SUIAppDelegate.swift └── SUISceneDelegate │ └── SUISceneDelegate.swift ├── Resources ├── Assets │ └── Assets.xcassets │ │ ├── 1.imageset │ │ ├── 1.png │ │ └── Contents.json │ │ ├── 2.imageset │ │ ├── 2.png │ │ └── Contents.json │ │ ├── 3.imageset │ │ ├── 3.png │ │ └── Contents.json │ │ ├── 4.imageset │ │ ├── 4.png │ │ └── Contents.json │ │ ├── 5.imageset │ │ ├── 5.png │ │ └── Contents.json │ │ ├── AccentColor.colorset │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ └── Contents.json │ │ └── Contents.json ├── Extensions │ ├── Comparable │ │ └── Comparable+ClampedTo.swift │ ├── UIButton │ │ └── Configuration │ │ │ └── UIButton.Configuration+SetFont.swift │ └── UIColor │ │ └── UIColor+Colours.swift ├── Protocols │ └── SUICardViewDelegate │ │ └── SUICardViewDelegate.swift └── Storyboards │ └── Base.lproj │ └── LaunchScreen.storyboard ├── Supporting Files └── Info.plist ├── View Controllers ├── SUIDetailViewController │ └── SUIDetailViewController.swift └── SUIMainViewController │ └── SUIMainViewController.swift └── Views ├── SUICardView └── SUICardView.swift └── SUINavigationBar └── SUINavigationBar.swift /README.md: -------------------------------------------------------------------------------- 1 | # Sports UI Demo 2 | 3 | This repository contains an Xcode project demonstrating how to recreate the card (or sheet) UI found in Apple's new Sports app. 4 | 5 | https://github.com/user-attachments/assets/401a57b1-bf43-40a6-a084-e60783b39852 6 | -------------------------------------------------------------------------------- /Sports-UI-Demo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C2E548D12B9A32E5000215D9 /* SUIAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E548D02B9A32E5000215D9 /* SUIAppDelegate.swift */; }; 11 | C2E548D32B9A32E5000215D9 /* SUISceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E548D22B9A32E5000215D9 /* SUISceneDelegate.swift */; }; 12 | C2E548D52B9A32E5000215D9 /* SUIMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E548D42B9A32E5000215D9 /* SUIMainViewController.swift */; }; 13 | C2E548DA2B9A32E6000215D9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C2E548D92B9A32E6000215D9 /* Assets.xcassets */; }; 14 | C2E548DD2B9A32E6000215D9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C2E548DB2B9A32E6000215D9 /* LaunchScreen.storyboard */; }; 15 | C2E548E52B9A33BB000215D9 /* SUIDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E548E42B9A33BA000215D9 /* SUIDetailViewController.swift */; }; 16 | C2E548EB2B9B1281000215D9 /* SUICardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E548EA2B9B1281000215D9 /* SUICardView.swift */; }; 17 | C2E548ED2B9B129A000215D9 /* SUINavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E548EC2B9B129A000215D9 /* SUINavigationBar.swift */; }; 18 | C2E548F42B9B4285000215D9 /* Comparable+ClampedTo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E548F32B9B4285000215D9 /* Comparable+ClampedTo.swift */; }; 19 | C2E548FB2B9B42BF000215D9 /* SUICardViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E548FA2B9B42BF000215D9 /* SUICardViewDelegate.swift */; }; 20 | C2E548FF2B9B4B16000215D9 /* UIColor+Colours.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E548FE2B9B4B16000215D9 /* UIColor+Colours.swift */; }; 21 | C2E549022B9B4B55000215D9 /* UIButton.Configuration+SetFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E549012B9B4B55000215D9 /* UIButton.Configuration+SetFont.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | C2E548CD2B9A32E5000215D9 /* Sports-UI-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Sports-UI-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | C2E548D02B9A32E5000215D9 /* SUIAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUIAppDelegate.swift; sourceTree = ""; }; 27 | C2E548D22B9A32E5000215D9 /* SUISceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUISceneDelegate.swift; sourceTree = ""; }; 28 | C2E548D42B9A32E5000215D9 /* SUIMainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUIMainViewController.swift; sourceTree = ""; }; 29 | C2E548D92B9A32E6000215D9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 30 | C2E548DC2B9A32E6000215D9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 31 | C2E548DE2B9A32E6000215D9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 32 | C2E548E42B9A33BA000215D9 /* SUIDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUIDetailViewController.swift; sourceTree = ""; }; 33 | C2E548EA2B9B1281000215D9 /* SUICardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUICardView.swift; sourceTree = ""; }; 34 | C2E548EC2B9B129A000215D9 /* SUINavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUINavigationBar.swift; sourceTree = ""; }; 35 | C2E548F32B9B4285000215D9 /* Comparable+ClampedTo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Comparable+ClampedTo.swift"; sourceTree = ""; }; 36 | C2E548FA2B9B42BF000215D9 /* SUICardViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUICardViewDelegate.swift; sourceTree = ""; }; 37 | C2E548FE2B9B4B16000215D9 /* UIColor+Colours.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Colours.swift"; sourceTree = ""; }; 38 | C2E549012B9B4B55000215D9 /* UIButton.Configuration+SetFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton.Configuration+SetFont.swift"; sourceTree = ""; }; 39 | /* End PBXFileReference section */ 40 | 41 | /* Begin PBXFrameworksBuildPhase section */ 42 | C2E548CA2B9A32E5000215D9 /* Frameworks */ = { 43 | isa = PBXFrameworksBuildPhase; 44 | buildActionMask = 2147483647; 45 | files = ( 46 | ); 47 | runOnlyForDeploymentPostprocessing = 0; 48 | }; 49 | /* End PBXFrameworksBuildPhase section */ 50 | 51 | /* Begin PBXGroup section */ 52 | C2E548C42B9A32E5000215D9 = { 53 | isa = PBXGroup; 54 | children = ( 55 | C2E548CF2B9A32E5000215D9 /* Sports-UI-Demo */, 56 | C2E548CE2B9A32E5000215D9 /* Products */, 57 | ); 58 | sourceTree = ""; 59 | }; 60 | C2E548CE2B9A32E5000215D9 /* Products */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | C2E548CD2B9A32E5000215D9 /* Sports-UI-Demo.app */, 64 | ); 65 | name = Products; 66 | sourceTree = ""; 67 | }; 68 | C2E548CF2B9A32E5000215D9 /* Sports-UI-Demo */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | C2E548E62B9B122D000215D9 /* Base */, 72 | C2E548EE2B9B12B7000215D9 /* Views */, 73 | C2E548E92B9B1247000215D9 /* View Controllers */, 74 | C2E548F72B9B429D000215D9 /* Resources */, 75 | C2E549052B9B5D8E000215D9 /* Supporting Files */, 76 | ); 77 | path = "Sports-UI-Demo"; 78 | sourceTree = ""; 79 | }; 80 | C2E548E62B9B122D000215D9 /* Base */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | C2E548E72B9B1239000215D9 /* SUIAppDelegate */, 84 | C2E548E82B9B123D000215D9 /* SUISceneDelegate */, 85 | ); 86 | path = Base; 87 | sourceTree = ""; 88 | }; 89 | C2E548E72B9B1239000215D9 /* SUIAppDelegate */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | C2E548D02B9A32E5000215D9 /* SUIAppDelegate.swift */, 93 | ); 94 | path = SUIAppDelegate; 95 | sourceTree = ""; 96 | }; 97 | C2E548E82B9B123D000215D9 /* SUISceneDelegate */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | C2E548D22B9A32E5000215D9 /* SUISceneDelegate.swift */, 101 | ); 102 | path = SUISceneDelegate; 103 | sourceTree = ""; 104 | }; 105 | C2E548E92B9B1247000215D9 /* View Controllers */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | C2E548F12B9B12C9000215D9 /* SUIMainViewController */, 109 | C2E548F22B9B12CF000215D9 /* SUIDetailViewController */, 110 | ); 111 | path = "View Controllers"; 112 | sourceTree = ""; 113 | }; 114 | C2E548EE2B9B12B7000215D9 /* Views */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | C2E548EF2B9B12BF000215D9 /* SUICardView */, 118 | C2E548F02B9B12C5000215D9 /* SUINavigationBar */, 119 | ); 120 | path = Views; 121 | sourceTree = ""; 122 | }; 123 | C2E548EF2B9B12BF000215D9 /* SUICardView */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | C2E548EA2B9B1281000215D9 /* SUICardView.swift */, 127 | ); 128 | path = SUICardView; 129 | sourceTree = ""; 130 | }; 131 | C2E548F02B9B12C5000215D9 /* SUINavigationBar */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | C2E548EC2B9B129A000215D9 /* SUINavigationBar.swift */, 135 | ); 136 | path = SUINavigationBar; 137 | sourceTree = ""; 138 | }; 139 | C2E548F12B9B12C9000215D9 /* SUIMainViewController */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | C2E548D42B9A32E5000215D9 /* SUIMainViewController.swift */, 143 | ); 144 | path = SUIMainViewController; 145 | sourceTree = ""; 146 | }; 147 | C2E548F22B9B12CF000215D9 /* SUIDetailViewController */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | C2E548E42B9A33BA000215D9 /* SUIDetailViewController.swift */, 151 | ); 152 | path = SUIDetailViewController; 153 | sourceTree = ""; 154 | }; 155 | C2E548F52B9B428F000215D9 /* Comparable */ = { 156 | isa = PBXGroup; 157 | children = ( 158 | C2E548F32B9B4285000215D9 /* Comparable+ClampedTo.swift */, 159 | ); 160 | path = Comparable; 161 | sourceTree = ""; 162 | }; 163 | C2E548F62B9B4295000215D9 /* Extensions */ = { 164 | isa = PBXGroup; 165 | children = ( 166 | C2E548F52B9B428F000215D9 /* Comparable */, 167 | C2E549032B9B4B68000215D9 /* UIButton */, 168 | C2E549002B9B4B49000215D9 /* UIColor */, 169 | ); 170 | path = Extensions; 171 | sourceTree = ""; 172 | }; 173 | C2E548F72B9B429D000215D9 /* Resources */ = { 174 | isa = PBXGroup; 175 | children = ( 176 | C2E548F82B9B42A8000215D9 /* Assets */, 177 | C2E548F62B9B4295000215D9 /* Extensions */, 178 | C2E548FD2B9B42D2000215D9 /* Protocols */, 179 | C2E548F92B9B42AE000215D9 /* Storyboards */, 180 | ); 181 | path = Resources; 182 | sourceTree = ""; 183 | }; 184 | C2E548F82B9B42A8000215D9 /* Assets */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | C2E548D92B9A32E6000215D9 /* Assets.xcassets */, 188 | ); 189 | path = Assets; 190 | sourceTree = ""; 191 | }; 192 | C2E548F92B9B42AE000215D9 /* Storyboards */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | C2E548DB2B9A32E6000215D9 /* LaunchScreen.storyboard */, 196 | ); 197 | path = Storyboards; 198 | sourceTree = ""; 199 | }; 200 | C2E548FC2B9B42CD000215D9 /* SUICardViewDelegate */ = { 201 | isa = PBXGroup; 202 | children = ( 203 | C2E548FA2B9B42BF000215D9 /* SUICardViewDelegate.swift */, 204 | ); 205 | path = SUICardViewDelegate; 206 | sourceTree = ""; 207 | }; 208 | C2E548FD2B9B42D2000215D9 /* Protocols */ = { 209 | isa = PBXGroup; 210 | children = ( 211 | C2E548FC2B9B42CD000215D9 /* SUICardViewDelegate */, 212 | ); 213 | path = Protocols; 214 | sourceTree = ""; 215 | }; 216 | C2E549002B9B4B49000215D9 /* UIColor */ = { 217 | isa = PBXGroup; 218 | children = ( 219 | C2E548FE2B9B4B16000215D9 /* UIColor+Colours.swift */, 220 | ); 221 | path = UIColor; 222 | sourceTree = ""; 223 | }; 224 | C2E549032B9B4B68000215D9 /* UIButton */ = { 225 | isa = PBXGroup; 226 | children = ( 227 | C2E549042B9B4B6D000215D9 /* Configuration */, 228 | ); 229 | path = UIButton; 230 | sourceTree = ""; 231 | }; 232 | C2E549042B9B4B6D000215D9 /* Configuration */ = { 233 | isa = PBXGroup; 234 | children = ( 235 | C2E549012B9B4B55000215D9 /* UIButton.Configuration+SetFont.swift */, 236 | ); 237 | path = Configuration; 238 | sourceTree = ""; 239 | }; 240 | C2E549052B9B5D8E000215D9 /* Supporting Files */ = { 241 | isa = PBXGroup; 242 | children = ( 243 | C2E548DE2B9A32E6000215D9 /* Info.plist */, 244 | ); 245 | path = "Supporting Files"; 246 | sourceTree = ""; 247 | }; 248 | /* End PBXGroup section */ 249 | 250 | /* Begin PBXNativeTarget section */ 251 | C2E548CC2B9A32E5000215D9 /* Sports-UI-Demo */ = { 252 | isa = PBXNativeTarget; 253 | buildConfigurationList = C2E548E12B9A32E6000215D9 /* Build configuration list for PBXNativeTarget "Sports-UI-Demo" */; 254 | buildPhases = ( 255 | C2E548C92B9A32E5000215D9 /* Sources */, 256 | C2E548CA2B9A32E5000215D9 /* Frameworks */, 257 | C2E548CB2B9A32E5000215D9 /* Resources */, 258 | ); 259 | buildRules = ( 260 | ); 261 | dependencies = ( 262 | ); 263 | name = "Sports-UI-Demo"; 264 | productName = "Sports-UI-Demo"; 265 | productReference = C2E548CD2B9A32E5000215D9 /* Sports-UI-Demo.app */; 266 | productType = "com.apple.product-type.application"; 267 | }; 268 | /* End PBXNativeTarget section */ 269 | 270 | /* Begin PBXProject section */ 271 | C2E548C52B9A32E5000215D9 /* Project object */ = { 272 | isa = PBXProject; 273 | attributes = { 274 | BuildIndependentTargetsInParallel = 1; 275 | LastSwiftUpdateCheck = 1500; 276 | LastUpgradeCheck = 1500; 277 | TargetAttributes = { 278 | C2E548CC2B9A32E5000215D9 = { 279 | CreatedOnToolsVersion = 15.0; 280 | }; 281 | }; 282 | }; 283 | buildConfigurationList = C2E548C82B9A32E5000215D9 /* Build configuration list for PBXProject "Sports-UI-Demo" */; 284 | compatibilityVersion = "Xcode 14.0"; 285 | developmentRegion = en; 286 | hasScannedForEncodings = 0; 287 | knownRegions = ( 288 | en, 289 | Base, 290 | ); 291 | mainGroup = C2E548C42B9A32E5000215D9; 292 | productRefGroup = C2E548CE2B9A32E5000215D9 /* Products */; 293 | projectDirPath = ""; 294 | projectRoot = ""; 295 | targets = ( 296 | C2E548CC2B9A32E5000215D9 /* Sports-UI-Demo */, 297 | ); 298 | }; 299 | /* End PBXProject section */ 300 | 301 | /* Begin PBXResourcesBuildPhase section */ 302 | C2E548CB2B9A32E5000215D9 /* Resources */ = { 303 | isa = PBXResourcesBuildPhase; 304 | buildActionMask = 2147483647; 305 | files = ( 306 | C2E548DD2B9A32E6000215D9 /* LaunchScreen.storyboard in Resources */, 307 | C2E548DA2B9A32E6000215D9 /* Assets.xcassets in Resources */, 308 | ); 309 | runOnlyForDeploymentPostprocessing = 0; 310 | }; 311 | /* End PBXResourcesBuildPhase section */ 312 | 313 | /* Begin PBXSourcesBuildPhase section */ 314 | C2E548C92B9A32E5000215D9 /* Sources */ = { 315 | isa = PBXSourcesBuildPhase; 316 | buildActionMask = 2147483647; 317 | files = ( 318 | C2E548D52B9A32E5000215D9 /* SUIMainViewController.swift in Sources */, 319 | C2E548D12B9A32E5000215D9 /* SUIAppDelegate.swift in Sources */, 320 | C2E548D32B9A32E5000215D9 /* SUISceneDelegate.swift in Sources */, 321 | C2E548F42B9B4285000215D9 /* Comparable+ClampedTo.swift in Sources */, 322 | C2E548EB2B9B1281000215D9 /* SUICardView.swift in Sources */, 323 | C2E549022B9B4B55000215D9 /* UIButton.Configuration+SetFont.swift in Sources */, 324 | C2E548FB2B9B42BF000215D9 /* SUICardViewDelegate.swift in Sources */, 325 | C2E548E52B9A33BB000215D9 /* SUIDetailViewController.swift in Sources */, 326 | C2E548ED2B9B129A000215D9 /* SUINavigationBar.swift in Sources */, 327 | C2E548FF2B9B4B16000215D9 /* UIColor+Colours.swift in Sources */, 328 | ); 329 | runOnlyForDeploymentPostprocessing = 0; 330 | }; 331 | /* End PBXSourcesBuildPhase section */ 332 | 333 | /* Begin PBXVariantGroup section */ 334 | C2E548DB2B9A32E6000215D9 /* LaunchScreen.storyboard */ = { 335 | isa = PBXVariantGroup; 336 | children = ( 337 | C2E548DC2B9A32E6000215D9 /* Base */, 338 | ); 339 | name = LaunchScreen.storyboard; 340 | sourceTree = ""; 341 | }; 342 | /* End PBXVariantGroup section */ 343 | 344 | /* Begin XCBuildConfiguration section */ 345 | C2E548DF2B9A32E6000215D9 /* Debug */ = { 346 | isa = XCBuildConfiguration; 347 | buildSettings = { 348 | ALWAYS_SEARCH_USER_PATHS = NO; 349 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 350 | CLANG_ANALYZER_NONNULL = YES; 351 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 352 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 353 | CLANG_ENABLE_MODULES = YES; 354 | CLANG_ENABLE_OBJC_ARC = YES; 355 | CLANG_ENABLE_OBJC_WEAK = YES; 356 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 357 | CLANG_WARN_BOOL_CONVERSION = YES; 358 | CLANG_WARN_COMMA = YES; 359 | CLANG_WARN_CONSTANT_CONVERSION = YES; 360 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 361 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 362 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 363 | CLANG_WARN_EMPTY_BODY = YES; 364 | CLANG_WARN_ENUM_CONVERSION = YES; 365 | CLANG_WARN_INFINITE_RECURSION = YES; 366 | CLANG_WARN_INT_CONVERSION = YES; 367 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 368 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 369 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 370 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 371 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 372 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 373 | CLANG_WARN_STRICT_PROTOTYPES = YES; 374 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 375 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 376 | CLANG_WARN_UNREACHABLE_CODE = YES; 377 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 378 | COPY_PHASE_STRIP = NO; 379 | DEBUG_INFORMATION_FORMAT = dwarf; 380 | ENABLE_STRICT_OBJC_MSGSEND = YES; 381 | ENABLE_TESTABILITY = YES; 382 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 383 | GCC_C_LANGUAGE_STANDARD = gnu17; 384 | GCC_DYNAMIC_NO_PIC = NO; 385 | GCC_NO_COMMON_BLOCKS = YES; 386 | GCC_OPTIMIZATION_LEVEL = 0; 387 | GCC_PREPROCESSOR_DEFINITIONS = ( 388 | "DEBUG=1", 389 | "$(inherited)", 390 | ); 391 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 392 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 393 | GCC_WARN_UNDECLARED_SELECTOR = YES; 394 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 395 | GCC_WARN_UNUSED_FUNCTION = YES; 396 | GCC_WARN_UNUSED_VARIABLE = YES; 397 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 398 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 399 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 400 | MTL_FAST_MATH = YES; 401 | ONLY_ACTIVE_ARCH = YES; 402 | SDKROOT = iphoneos; 403 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 404 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 405 | }; 406 | name = Debug; 407 | }; 408 | C2E548E02B9A32E6000215D9 /* Release */ = { 409 | isa = XCBuildConfiguration; 410 | buildSettings = { 411 | ALWAYS_SEARCH_USER_PATHS = NO; 412 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 413 | CLANG_ANALYZER_NONNULL = YES; 414 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 415 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 416 | CLANG_ENABLE_MODULES = YES; 417 | CLANG_ENABLE_OBJC_ARC = YES; 418 | CLANG_ENABLE_OBJC_WEAK = YES; 419 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 420 | CLANG_WARN_BOOL_CONVERSION = YES; 421 | CLANG_WARN_COMMA = YES; 422 | CLANG_WARN_CONSTANT_CONVERSION = YES; 423 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 424 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 425 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 426 | CLANG_WARN_EMPTY_BODY = YES; 427 | CLANG_WARN_ENUM_CONVERSION = YES; 428 | CLANG_WARN_INFINITE_RECURSION = YES; 429 | CLANG_WARN_INT_CONVERSION = YES; 430 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 431 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 432 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 433 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 434 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 435 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 436 | CLANG_WARN_STRICT_PROTOTYPES = YES; 437 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 438 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 439 | CLANG_WARN_UNREACHABLE_CODE = YES; 440 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 441 | COPY_PHASE_STRIP = NO; 442 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 443 | ENABLE_NS_ASSERTIONS = NO; 444 | ENABLE_STRICT_OBJC_MSGSEND = YES; 445 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 446 | GCC_C_LANGUAGE_STANDARD = gnu17; 447 | GCC_NO_COMMON_BLOCKS = YES; 448 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 449 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 450 | GCC_WARN_UNDECLARED_SELECTOR = YES; 451 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 452 | GCC_WARN_UNUSED_FUNCTION = YES; 453 | GCC_WARN_UNUSED_VARIABLE = YES; 454 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 455 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 456 | MTL_ENABLE_DEBUG_INFO = NO; 457 | MTL_FAST_MATH = YES; 458 | SDKROOT = iphoneos; 459 | SWIFT_COMPILATION_MODE = wholemodule; 460 | VALIDATE_PRODUCT = YES; 461 | }; 462 | name = Release; 463 | }; 464 | C2E548E22B9A32E6000215D9 /* Debug */ = { 465 | isa = XCBuildConfiguration; 466 | buildSettings = { 467 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 468 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 469 | CODE_SIGN_STYLE = Automatic; 470 | CURRENT_PROJECT_VERSION = 1; 471 | DEVELOPMENT_TEAM = DY2GQFY855; 472 | GENERATE_INFOPLIST_FILE = YES; 473 | INFOPLIST_FILE = "Sports-UI-Demo/Supporting Files/Info.plist"; 474 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 475 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 476 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 477 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 478 | LD_RUNPATH_SEARCH_PATHS = ( 479 | "$(inherited)", 480 | "@executable_path/Frameworks", 481 | ); 482 | MARKETING_VERSION = 1.0; 483 | PRODUCT_BUNDLE_IDENTIFIER = "com.sebvidal.Sports-UI-Demo"; 484 | PRODUCT_NAME = "$(TARGET_NAME)"; 485 | SWIFT_EMIT_LOC_STRINGS = YES; 486 | SWIFT_VERSION = 5.0; 487 | TARGETED_DEVICE_FAMILY = "1,2"; 488 | }; 489 | name = Debug; 490 | }; 491 | C2E548E32B9A32E6000215D9 /* Release */ = { 492 | isa = XCBuildConfiguration; 493 | buildSettings = { 494 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 495 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 496 | CODE_SIGN_STYLE = Automatic; 497 | CURRENT_PROJECT_VERSION = 1; 498 | DEVELOPMENT_TEAM = DY2GQFY855; 499 | GENERATE_INFOPLIST_FILE = YES; 500 | INFOPLIST_FILE = "Sports-UI-Demo/Supporting Files/Info.plist"; 501 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 502 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 503 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 504 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 505 | LD_RUNPATH_SEARCH_PATHS = ( 506 | "$(inherited)", 507 | "@executable_path/Frameworks", 508 | ); 509 | MARKETING_VERSION = 1.0; 510 | PRODUCT_BUNDLE_IDENTIFIER = "com.sebvidal.Sports-UI-Demo"; 511 | PRODUCT_NAME = "$(TARGET_NAME)"; 512 | SWIFT_EMIT_LOC_STRINGS = YES; 513 | SWIFT_VERSION = 5.0; 514 | TARGETED_DEVICE_FAMILY = "1,2"; 515 | }; 516 | name = Release; 517 | }; 518 | /* End XCBuildConfiguration section */ 519 | 520 | /* Begin XCConfigurationList section */ 521 | C2E548C82B9A32E5000215D9 /* Build configuration list for PBXProject "Sports-UI-Demo" */ = { 522 | isa = XCConfigurationList; 523 | buildConfigurations = ( 524 | C2E548DF2B9A32E6000215D9 /* Debug */, 525 | C2E548E02B9A32E6000215D9 /* Release */, 526 | ); 527 | defaultConfigurationIsVisible = 0; 528 | defaultConfigurationName = Release; 529 | }; 530 | C2E548E12B9A32E6000215D9 /* Build configuration list for PBXNativeTarget "Sports-UI-Demo" */ = { 531 | isa = XCConfigurationList; 532 | buildConfigurations = ( 533 | C2E548E22B9A32E6000215D9 /* Debug */, 534 | C2E548E32B9A32E6000215D9 /* Release */, 535 | ); 536 | defaultConfigurationIsVisible = 0; 537 | defaultConfigurationName = Release; 538 | }; 539 | /* End XCConfigurationList section */ 540 | }; 541 | rootObject = C2E548C52B9A32E5000215D9 /* Project object */; 542 | } 543 | -------------------------------------------------------------------------------- /Sports-UI-Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sports-UI-Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sports-UI-Demo.xcodeproj/xcuserdata/sebvidal.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /Sports-UI-Demo.xcodeproj/xcuserdata/sebvidal.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Sports-UI-Demo.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Base/SUIAppDelegate/SUIAppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUIAppDelegate.swift 3 | // Sports-UI-Demo 4 | // 5 | // Created by Seb Vidal on 07/03/2024. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class SUIAppDelegate: UIResponder, UIApplicationDelegate { 12 | // MARK: - UIApplicationDelegate 13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 14 | return true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Base/SUISceneDelegate/SUISceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUISceneDelegate.swift 3 | // Sports-UI-Demo 4 | // 5 | // Created by Seb Vidal on 07/03/2024. 6 | // 7 | 8 | import UIKit 9 | 10 | class SUISceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | // MARK: - Public Properties 12 | var window: UIWindow? 13 | 14 | // MARK: - UIWindowSceneDelegate 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let windowScene = (scene as? UIWindowScene) else { return } 20 | let window = UIWindow(windowScene: windowScene) 21 | window.rootViewController = SUIMainViewController() 22 | window.makeKeyAndVisible() 23 | window.overrideUserInterfaceStyle = .dark 24 | self.window = window 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Assets/Assets.xcassets/1.imageset/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebjvidal/Sports-UI-Demo/74090502545ac17c70673ba2a8ce92110bc0ae62/Sports-UI-Demo/Resources/Assets/Assets.xcassets/1.imageset/1.png -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Assets/Assets.xcassets/1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "1.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Assets/Assets.xcassets/2.imageset/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebjvidal/Sports-UI-Demo/74090502545ac17c70673ba2a8ce92110bc0ae62/Sports-UI-Demo/Resources/Assets/Assets.xcassets/2.imageset/2.png -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Assets/Assets.xcassets/2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "2.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Assets/Assets.xcassets/3.imageset/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebjvidal/Sports-UI-Demo/74090502545ac17c70673ba2a8ce92110bc0ae62/Sports-UI-Demo/Resources/Assets/Assets.xcassets/3.imageset/3.png -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Assets/Assets.xcassets/3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "3.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Assets/Assets.xcassets/4.imageset/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebjvidal/Sports-UI-Demo/74090502545ac17c70673ba2a8ce92110bc0ae62/Sports-UI-Demo/Resources/Assets/Assets.xcassets/4.imageset/4.png -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Assets/Assets.xcassets/4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "4.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Assets/Assets.xcassets/5.imageset/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebjvidal/Sports-UI-Demo/74090502545ac17c70673ba2a8ce92110bc0ae62/Sports-UI-Demo/Resources/Assets/Assets.xcassets/5.imageset/5.png -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Assets/Assets.xcassets/5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "5.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Assets/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Assets/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Extensions/Comparable/Comparable+ClampedTo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Comparable+ClampedTo.swift 3 | // Sports-UI-Demo 4 | // 5 | // Created by Seb Vidal on 08/03/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Comparable { 11 | func clamped(to limit: ClosedRange) -> Self { 12 | return min(max(self, limit.lowerBound), limit.upperBound) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Extensions/UIButton/Configuration/UIButton.Configuration+SetFont.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIButton.Configuration+SetFont.swift 3 | // Sports-UI-Demo 4 | // 5 | // Created by Seb Vidal on 08/03/2024. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIButton.Configuration { 11 | mutating func setFont(_ font: UIFont) { 12 | titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { container in 13 | var container = container 14 | container.font = font 15 | 16 | return container 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Extensions/UIColor/UIColor+Colours.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Colours.swift 3 | // Sports-UI-Demo 4 | // 5 | // Created by Seb Vidal on 08/03/2024. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIColor { 11 | static var sportsGreen: UIColor { 12 | return UIColor(red: 13 / 255, green: 91 / 255, blue: 21 / 255, alpha: 1) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Protocols/SUICardViewDelegate/SUICardViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUICardViewDelegate.swift 3 | // Sports-UI-Demo 4 | // 5 | // Created by Seb Vidal on 08/03/2024. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol SUICardViewDelegate: NSObject { 11 | func cardView(_ cardView: SUICardView, scrollViewDidScroll scrollView: UIScrollView) 12 | func cardView(_ cardView: SUICardView, didRequestDismissal scrollView: UIScrollView) 13 | } 14 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Resources/Storyboards/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 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SUISceneDelegate 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Sports-UI-Demo/View Controllers/SUIDetailViewController/SUIDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUIDetailViewController.swift 3 | // Sports-UI-Demo 4 | // 5 | // Created by Seb Vidal on 07/03/2024. 6 | // 7 | 8 | import UIKit 9 | 10 | class SUIDetailViewController: UIViewController, UIScrollViewDelegate, SUICardViewDelegate, UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning { 11 | // MARK: - Private Properties 12 | private var titleLabel: UILabel! 13 | private var scrollView: UIScrollView! 14 | private var cardViews: [SUICardView] = [] 15 | 16 | private var horizontalPagePadding: CGFloat = 20 17 | private var interPageSpacing: CGFloat = 10 18 | private var _currentPage: Int = 0 19 | 20 | // MARK: - Public Properties 21 | var currentPage: Int { 22 | get { 23 | return _currentPage 24 | } set { 25 | _currentPage = newValue 26 | updateScrollViewCurrentPage() 27 | } 28 | } 29 | 30 | // MARK: - init(nibName:bundle:) 31 | override init(nibName: String?, bundle: Bundle?) { 32 | super.init(nibName: nibName, bundle: bundle) 33 | setupViewController() 34 | setupTitleLabel() 35 | setupScrollView() 36 | setupCardViews() 37 | } 38 | 39 | // MARK: - init(coder:) 40 | required init?(coder: NSCoder) { 41 | fatalError("init(coder:) has not been implemented") 42 | } 43 | 44 | // MARK: - Private Methods 45 | private func setupViewController() { 46 | view.backgroundColor = .black.withAlphaComponent(0.9) 47 | transitioningDelegate = self 48 | } 49 | 50 | private func setupTitleLabel() { 51 | titleLabel = UILabel() 52 | titleLabel.text = "Today" 53 | titleLabel.textColor = .white 54 | titleLabel.font = .systemFont(ofSize: 15, weight: .semibold) 55 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 56 | 57 | view.addSubview(titleLabel) 58 | 59 | NSLayoutConstraint.activate([ 60 | titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 6), 61 | titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor) 62 | ]) 63 | } 64 | 65 | private func setupScrollView() { 66 | scrollView = UIScrollView() 67 | scrollView.delegate = self 68 | scrollView.decelerationRate = .fast 69 | scrollView.alwaysBounceHorizontal = true 70 | scrollView.showsHorizontalScrollIndicator = false 71 | scrollView.translatesAutoresizingMaskIntoConstraints = false 72 | scrollView.contentInset.left = horizontalPagePadding 73 | scrollView.contentInset.right = horizontalPagePadding 74 | 75 | view.addSubview(scrollView) 76 | 77 | NSLayoutConstraint.activate([ 78 | scrollView.topAnchor.constraint(equalTo: view.topAnchor), 79 | scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 80 | scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 81 | scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 82 | ]) 83 | } 84 | 85 | private func setupCardViews() { 86 | for index in 1...5 { 87 | let cardView = SUICardView() 88 | cardView.delegate = self 89 | cardView.image = UIImage(named: "\(index)") 90 | 91 | cardViews.append(cardView) 92 | scrollView.addSubview(cardView) 93 | } 94 | } 95 | 96 | private func updateCardViewInsets() { 97 | for cardView in cardViews where cardView.contentInset.top == 0 { 98 | let inset = titleLabel.frame.maxY - view.safeAreaInsets.top + 17 99 | cardView.contentInset.top = inset 100 | } 101 | } 102 | 103 | private func layoutScrollViewSubviews(fullscreenFraction: CGFloat = 0) { 104 | let horizontalOffset = horizontalPagePadding * fullscreenFraction 105 | let additionalWidth = (horizontalOffset * 2) 106 | let commonWidth = view.frame.width - (horizontalPagePadding * 2) 107 | let commonHeight = view.frame.height - scrollView.contentInset.top 108 | 109 | for (index, subview) in cardViews.enumerated() { 110 | if index == _currentPage { 111 | let index = CGFloat(index) 112 | let x = (commonWidth + 10) * index - horizontalOffset 113 | let width = commonWidth + additionalWidth 114 | subview.frame = CGRect(x: x, y: 0, width: width, height: commonHeight) 115 | } else if index < _currentPage { 116 | let x = (commonWidth + 10) * CGFloat(index) - (horizontalOffset) 117 | subview.frame = CGRect(x: x, y: 0, width: commonWidth, height: commonHeight) 118 | } else { 119 | let x = cardViews[index - 1].frame.maxX + interPageSpacing 120 | subview.frame = CGRect(x: x, y: 0, width: commonWidth, height: commonHeight) 121 | } 122 | } 123 | } 124 | 125 | private func updateScrollViewContentSize() { 126 | let width = cardViews[cardViews.count - 1].frame.maxX 127 | let height = scrollView.frame.height - scrollView.contentInset.top 128 | scrollView.contentSize = CGSize(width: width, height: height) 129 | } 130 | 131 | private func setCardViewsEnabled(_ enabled: Bool, except cardViewException: SUICardView) { 132 | for cardView in cardViews where cardView != cardViewException { 133 | cardView.isUserInteractionEnabled = enabled 134 | } 135 | } 136 | 137 | private func setHorizontalScrollEnabled(_ enabled: Bool) { 138 | scrollView.isScrollEnabled = enabled 139 | } 140 | 141 | private func updateScrollViewCurrentPage() { 142 | let width = view.frame.width - horizontalPagePadding - interPageSpacing 143 | let inset = scrollView.contentInset.left 144 | scrollView.contentOffset.x = width * CGFloat(currentPage) - inset 145 | } 146 | 147 | // MARK: - viewDidLayoutSubviews() 148 | override func viewDidLayoutSubviews() { 149 | super.viewDidLayoutSubviews() 150 | updateCardViewInsets() 151 | layoutScrollViewSubviews() 152 | updateScrollViewContentSize() 153 | updateScrollViewCurrentPage() 154 | } 155 | 156 | // MARK: - UIScrollViewDelegate 157 | func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { 158 | let pageWidth = view.frame.width - (horizontalPagePadding * 2) + interPageSpacing 159 | let approximatePage = (scrollView.contentOffset.x + scrollView.contentInset.left) / pageWidth 160 | let currentPage = velocity.x == 0 ? round(approximatePage) : (velocity.x < 0.0 ? floor(approximatePage) : ceil(approximatePage)) 161 | let clampedPage = currentPage.clamped(to: 0...CGFloat(cardViews.count - 1)) 162 | _currentPage = Int(clampedPage) 163 | 164 | targetContentOffset.pointee.x = pageWidth * clampedPage - scrollView.contentInset.left 165 | } 166 | 167 | // MARK: - CardViewDelegate 168 | func cardView(_ cardView: SUICardView, scrollViewDidScroll scrollView: UIScrollView) { 169 | let offset = scrollView.contentOffset.y 170 | let inset = scrollView.contentInset.top + view.safeAreaInsets.top 171 | let fraction = ((offset + inset) / inset).clamped(to: 0...1) 172 | setCardViewsEnabled(fraction == 0, except: cardView) 173 | setHorizontalScrollEnabled(fraction == 0) 174 | layoutScrollViewSubviews(fullscreenFraction: fraction) 175 | } 176 | 177 | func cardView(_ cardView: SUICardView, didRequestDismissal scrollView: UIScrollView) { 178 | dismiss(animated: true) 179 | } 180 | 181 | // MARK: - UIViewControllerTransitioningDelegate 182 | func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 183 | return self 184 | } 185 | 186 | func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 187 | return self 188 | } 189 | 190 | // MARK: - UIViewControllerAnimatedTransitioning 191 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 192 | return 0.6 193 | } 194 | 195 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 196 | if transitionContext.viewController(forKey: .to) == self { 197 | animatePresentation(using: transitionContext) 198 | } else { 199 | animateDismissal(using: transitionContext) 200 | } 201 | } 202 | 203 | private func animatePresentation(using transitionContext: UIViewControllerContextTransitioning) { 204 | let destination = transitionContext.viewController(forKey: .to) as! SUIDetailViewController 205 | destination.scrollView.alpha = 0 206 | destination.view.alpha = 0 207 | 208 | let transitionView = SUIDetailViewController() 209 | transitionView.view.layoutIfNeeded() 210 | transitionView.currentPage = destination.currentPage 211 | transitionView.titleLabel.isHidden = true 212 | transitionView.view.backgroundColor = .clear 213 | transitionView.view.frame.origin.y = destination.view.frame.height 214 | 215 | let containerView = transitionContext.containerView 216 | containerView.addSubview(destination.view) 217 | containerView.addSubview(transitionView.view) 218 | 219 | let animator = UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) { 220 | destination.view.alpha = 1 221 | transitionView.view.frame = containerView.frame 222 | } 223 | 224 | animator.addCompletion { position in 225 | destination.scrollView.alpha = 1 226 | transitionView.view.removeFromSuperview() 227 | transitionContext.completeTransition(true) 228 | } 229 | 230 | animator.startAnimation() 231 | } 232 | 233 | private func animateDismissal(using transitionContext: UIViewControllerContextTransitioning) { 234 | let origin = transitionContext.viewController(forKey: .from) as! SUIDetailViewController 235 | origin.scrollView.alpha = 0 236 | 237 | let inset = origin.cardViews[origin._currentPage].contentInset.top 238 | let offset = origin.cardViews[origin._currentPage].contentOffset.y 239 | let value = inset + offset 240 | 241 | let transitionView = SUIDetailViewController() 242 | transitionView.currentPage = origin.currentPage 243 | transitionView.titleLabel.isHidden = true 244 | transitionView.view.backgroundColor = .clear 245 | transitionView.view.frame = origin.view.frame 246 | transitionView.additionalSafeAreaInsets = origin.view.safeAreaInsets 247 | transitionView.cardViews[origin._currentPage].contentInset.top = -value 248 | transitionView.cardViews[origin._currentPage].contentOffset.y = value 249 | transitionView.scrollView.contentOffset = origin.scrollView.contentOffset 250 | 251 | let containerView = transitionContext.containerView 252 | containerView.addSubview(origin.view) 253 | containerView.addSubview(transitionView.view) 254 | 255 | let animator = UIViewPropertyAnimator(duration: 0.35, dampingRatio: 1) { 256 | origin.view.alpha = 0 257 | transitionView.view.frame.origin.y = containerView.frame.height 258 | } 259 | 260 | animator.addCompletion { position in 261 | transitionContext.completeTransition(true) 262 | } 263 | 264 | animator.startAnimation() 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /Sports-UI-Demo/View Controllers/SUIMainViewController/SUIMainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUIMainViewController.swift 3 | // Sports-UI-Demo 4 | // 5 | // Created by Seb Vidal on 07/03/2024. 6 | // 7 | 8 | import UIKit 9 | 10 | class SUIMainViewController: UIViewController { 11 | // MARK: - Private Properties 12 | private var gradientLayer: CAGradientLayer! 13 | private var titleLabel: UILabel! 14 | private var moreButton: UIButton! 15 | private var stackView: UIStackView! 16 | private var detailLabel: UILabel! 17 | private var getStartedButton: UIButton! 18 | private var featuredLabel: UILabel! 19 | private var containerView: UIView! 20 | 21 | // MARK: - init(nibName:bundle:) 22 | override init(nibName: String?, bundle: Bundle?) { 23 | super.init(nibName: nibName, bundle: bundle) 24 | setupGradientLayer() 25 | setupTitleLabel() 26 | setupMoreButton() 27 | setupStackView() 28 | setupDetailLabel() 29 | setupGetStartedButton() 30 | setupFeaturedLabel() 31 | setupContainerView() 32 | } 33 | 34 | // MARK: - init(coder:) 35 | required init?(coder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | // MARK: - Private Methods 40 | private func setupGradientLayer() { 41 | gradientLayer = CAGradientLayer() 42 | gradientLayer.frame = view.frame 43 | gradientLayer.startPoint = CGPoint(x: 0, y: 0) 44 | gradientLayer.endPoint = CGPoint(x: 0, y: 1) 45 | gradientLayer.colors = [UIColor.sportsGreen, UIColor.black].map(\.cgColor) 46 | gradientLayer.locations = [0, 0.25] 47 | 48 | view.layer.addSublayer(gradientLayer) 49 | } 50 | 51 | private func setupTitleLabel() { 52 | titleLabel = UILabel() 53 | titleLabel.text = " Sports" 54 | titleLabel.textColor = .white 55 | titleLabel.font = .systemFont(ofSize: 34, weight: .bold) 56 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 57 | 58 | view.addSubview(titleLabel) 59 | 60 | NSLayoutConstraint.activate([ 61 | titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8), 62 | titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20) 63 | ]) 64 | } 65 | 66 | private func setupMoreButton() { 67 | moreButton = UIButton() 68 | moreButton.configuration = .filled() 69 | moreButton.configuration?.baseBackgroundColor = .tertiarySystemFill 70 | moreButton.configuration?.cornerStyle = .capsule 71 | moreButton.configuration?.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(pointSize: 9) 72 | moreButton.configuration?.contentInsets = NSDirectionalEdgeInsets(top: 13, leading: 10, bottom: 12, trailing: 10) 73 | moreButton.setImage(UIImage(systemName: "line.3.horizontal"), for: .normal) 74 | moreButton.translatesAutoresizingMaskIntoConstraints = false 75 | 76 | view.addSubview(moreButton) 77 | 78 | NSLayoutConstraint.activate([ 79 | moreButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor, constant: 2), 80 | moreButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24) 81 | ]) 82 | } 83 | 84 | private func setupStackView() { 85 | stackView = UIStackView() 86 | stackView.spacing = 4 87 | stackView.axis = .horizontal 88 | stackView.alignment = .center 89 | stackView.translatesAutoresizingMaskIntoConstraints = false 90 | 91 | view.addSubview(stackView) 92 | 93 | NSLayoutConstraint.activate([ 94 | stackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 54), 95 | stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor) 96 | ]) 97 | 98 | let constants: [CGFloat] = [49, 60, 71, 82, 71, 60, 49] 99 | 100 | for constant in constants { 101 | let circleView = UIView() 102 | circleView.clipsToBounds = true 103 | circleView.layer.cornerCurve = .circular 104 | circleView.layer.cornerRadius = constant / 2 105 | circleView.backgroundColor = .tertiarySystemFill 106 | circleView.translatesAutoresizingMaskIntoConstraints = false 107 | 108 | stackView.addArrangedSubview(circleView) 109 | 110 | NSLayoutConstraint.activate([ 111 | circleView.widthAnchor.constraint(equalToConstant: constant), 112 | circleView.heightAnchor.constraint(equalToConstant: constant) 113 | ]) 114 | } 115 | } 116 | 117 | private func setupDetailLabel() { 118 | detailLabel = UILabel() 119 | detailLabel.numberOfLines = 0 120 | detailLabel.textColor = .white 121 | detailLabel.textAlignment = .center 122 | detailLabel.font = .systemFont(ofSize: 17) 123 | detailLabel.translatesAutoresizingMaskIntoConstraints = false 124 | detailLabel.text = "Choose your favourite leagues and teams." 125 | 126 | view.addSubview(detailLabel) 127 | 128 | NSLayoutConstraint.activate([ 129 | detailLabel.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 20), 130 | detailLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), 131 | detailLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.6) 132 | ]) 133 | } 134 | 135 | private func setupGetStartedButton() { 136 | getStartedButton = UIButton() 137 | getStartedButton.configuration = .filled() 138 | getStartedButton.configuration?.baseBackgroundColor = .white 139 | getStartedButton.configuration?.baseForegroundColor = .black 140 | getStartedButton.configuration?.buttonSize = .large 141 | getStartedButton.configuration?.cornerStyle = .large 142 | getStartedButton.configuration?.contentInsets = NSDirectionalEdgeInsets(top: 15, leading: 33, bottom: 15, trailing: 33) 143 | getStartedButton.configuration?.setFont(.systemFont(ofSize: 17, weight: .semibold)) 144 | getStartedButton.setTitle("Get Started", for: .normal) 145 | getStartedButton.translatesAutoresizingMaskIntoConstraints = false 146 | 147 | view.addSubview(getStartedButton) 148 | 149 | NSLayoutConstraint.activate([ 150 | getStartedButton.topAnchor.constraint(equalTo: detailLabel.bottomAnchor, constant: 21), 151 | getStartedButton.centerXAnchor.constraint(equalTo: view.centerXAnchor) 152 | ]) 153 | } 154 | 155 | private func setupFeaturedLabel() { 156 | featuredLabel = UILabel() 157 | featuredLabel.textColor = .white 158 | featuredLabel.text = "Featured Games" 159 | featuredLabel.font = .systemFont(ofSize: 17, weight: .semibold) 160 | featuredLabel.translatesAutoresizingMaskIntoConstraints = false 161 | 162 | view.addSubview(featuredLabel) 163 | 164 | NSLayoutConstraint.activate([ 165 | featuredLabel.topAnchor.constraint(equalTo: getStartedButton.bottomAnchor, constant: 54), 166 | featuredLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor) 167 | ]) 168 | } 169 | 170 | private func setupContainerView() { 171 | containerView = UIView() 172 | containerView.translatesAutoresizingMaskIntoConstraints = false 173 | 174 | view.addSubview(containerView) 175 | 176 | NSLayoutConstraint.activate([ 177 | containerView.topAnchor.constraint(equalTo: featuredLabel.bottomAnchor, constant: 16), 178 | containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), 179 | containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), 180 | containerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16) 181 | ]) 182 | 183 | for index in 0...4 { 184 | let button = UIButton() 185 | button.tag = index 186 | button.configuration = .filled() 187 | button.configuration?.baseBackgroundColor = .tertiarySystemFill 188 | button.configuration?.cornerStyle = .medium 189 | button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) 190 | 191 | containerView.addSubview(button) 192 | } 193 | } 194 | 195 | @objc private func buttonTapped(_ sender: UIButton) { 196 | let viewController = SUIDetailViewController() 197 | viewController.modalPresentationStyle = .custom 198 | viewController.currentPage = sender.tag 199 | 200 | present(viewController, animated: true) 201 | } 202 | 203 | private func layoutContainerViewSubviews() { 204 | for (index, subview) in containerView.subviews.enumerated() { 205 | let width = containerView.frame.width 206 | let height = (containerView.frame.height - 16) / 5 207 | let y = (height + 4) * CGFloat(index) 208 | subview.frame = CGRect(x: 0, y: y, width: width, height: height) 209 | } 210 | } 211 | 212 | // MARK: - viewDidLayoutSubviews() 213 | override func viewDidLayoutSubviews() { 214 | super.viewDidLayoutSubviews() 215 | layoutContainerViewSubviews() 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Views/SUICardView/SUICardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUICardView.swift 3 | // Sports-UI-Demo 4 | // 5 | // Created by Seb Vidal on 08/03/2024. 6 | // 7 | 8 | import UIKit 9 | 10 | class SUICardView: UIView, UIScrollViewDelegate { 11 | // MARK: - Private Properties 12 | private var scrollView: UIScrollView! 13 | private var backgroundView: UIImageView! 14 | private var navigationBar: SUINavigationBar! 15 | 16 | // MARK: - Public Properties 17 | var contentInset: UIEdgeInsets { 18 | get { 19 | return scrollView.contentInset 20 | } set { 21 | scrollView.contentInset = newValue 22 | scrollView.contentOffset.y = -newValue.top 23 | } 24 | } 25 | 26 | var contentOffset: CGPoint { 27 | get { 28 | return scrollView.contentOffset 29 | } set { 30 | scrollView.contentOffset = newValue 31 | } 32 | } 33 | 34 | weak var delegate: SUICardViewDelegate? = nil 35 | 36 | var image: UIImage? { 37 | get { 38 | return backgroundView.image 39 | } set { 40 | backgroundView.image = newValue 41 | } 42 | } 43 | 44 | // MARK: - init(frame:) 45 | override init(frame: CGRect) { 46 | super.init(frame: frame) 47 | setupScrollView() 48 | setupBackgroundView() 49 | setupNavigationBar() 50 | } 51 | 52 | // MARK: - init(coder:) 53 | required init?(coder: NSCoder) { 54 | fatalError("init(coder:) has not been implemented") 55 | } 56 | 57 | // MARK: - Private Methods 58 | private func setupScrollView() { 59 | scrollView = UIScrollView() 60 | scrollView.delegate = self 61 | scrollView.contentSize.height = 2000 62 | scrollView.showsVerticalScrollIndicator = false 63 | scrollView.translatesAutoresizingMaskIntoConstraints = false 64 | scrollView.decelerationRate = .fast 65 | 66 | addSubview(scrollView) 67 | 68 | NSLayoutConstraint.activate([ 69 | scrollView.topAnchor.constraint(equalTo: topAnchor), 70 | scrollView.leadingAnchor.constraint(equalTo: leadingAnchor), 71 | scrollView.trailingAnchor.constraint(equalTo: trailingAnchor), 72 | scrollView.bottomAnchor.constraint(equalTo: bottomAnchor) 73 | ]) 74 | } 75 | 76 | private func setupBackgroundView() { 77 | backgroundView = UIImageView() 78 | backgroundView.clipsToBounds = true 79 | backgroundView.layer.cornerRadius = 12 80 | backgroundView.layer.cornerCurve = .continuous 81 | backgroundView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] 82 | backgroundView.backgroundColor = .black 83 | 84 | scrollView.addSubview(backgroundView) 85 | } 86 | 87 | private func setupNavigationBar() { 88 | navigationBar = SUINavigationBar() 89 | 90 | addSubview(navigationBar) 91 | } 92 | 93 | private func layoutBackgroundView() { 94 | backgroundView.frame.size.width = frame.width 95 | backgroundView.frame.size.height = scrollView.contentSize.height + scrollView.contentInset.top 96 | } 97 | 98 | private func layoutNavigationBar() { 99 | let width = frame.width 100 | let height = safeAreaInsets.top + 49 101 | navigationBar.frame.size = CGSize(width: width, height: height) 102 | 103 | let offset = scrollView.contentOffset.y.clamped(to: 0...height) 104 | let fraction = offset / height 105 | let y = min(0, -height + scrollView.contentOffset.y) 106 | navigationBar.frame.origin.y = y 107 | navigationBar.alpha = 1 * fraction 108 | } 109 | 110 | // MARK: - layoutSubviews() 111 | override func layoutSubviews() { 112 | super.layoutSubviews() 113 | layoutBackgroundView() 114 | layoutNavigationBar() 115 | } 116 | 117 | // MARK: - UIScrollViewDelegate 118 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 119 | delegate?.cardView(self, scrollViewDidScroll: scrollView) 120 | layoutNavigationBar() 121 | } 122 | 123 | func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 124 | let inset = scrollView.contentInset.top + safeAreaInsets.top 125 | let offset = scrollView.contentOffset.y + inset 126 | 127 | if offset < -64 { delegate?.cardView(self, didRequestDismissal: scrollView) } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Sports-UI-Demo/Views/SUINavigationBar/SUINavigationBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUINavigationBar.swift 3 | // Sports-UI-Demo 4 | // 5 | // Created by Seb Vidal on 08/03/2024. 6 | // 7 | 8 | import UIKit 9 | 10 | class SUINavigationBar: UIView { 11 | private var visualEffectView: UIVisualEffectView! 12 | private var titleLabel: UILabel! 13 | private var detailLabel: UILabel! 14 | private var separatorView: UIView! 15 | 16 | override init(frame: CGRect) { 17 | super.init(frame: frame) 18 | setupVisualEffectView() 19 | setupTitleLabel() 20 | setupDetailLabel() 21 | setupSeparatorView() 22 | } 23 | 24 | required init?(coder: NSCoder) { 25 | fatalError("init(coder:) has not been implemented") 26 | } 27 | 28 | private func setupVisualEffectView() { 29 | visualEffectView = UIVisualEffectView() 30 | visualEffectView.effect = UIBlurEffect(style: .systemMaterial) 31 | visualEffectView.translatesAutoresizingMaskIntoConstraints = false 32 | 33 | addSubview(visualEffectView) 34 | 35 | NSLayoutConstraint.activate([ 36 | visualEffectView.topAnchor.constraint(equalTo: topAnchor), 37 | visualEffectView.leadingAnchor.constraint(equalTo: leadingAnchor), 38 | visualEffectView.trailingAnchor.constraint(equalTo: trailingAnchor), 39 | visualEffectView.bottomAnchor.constraint(equalTo: bottomAnchor) 40 | ]) 41 | } 42 | 43 | private func setupTitleLabel() { 44 | titleLabel = UILabel() 45 | titleLabel.text = "Title" 46 | titleLabel.font = .systemFont(ofSize: 15, weight: .semibold) 47 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 48 | 49 | addSubview(titleLabel) 50 | 51 | NSLayoutConstraint.activate([ 52 | titleLabel.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 3), 53 | titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor) 54 | ]) 55 | } 56 | 57 | private func setupDetailLabel() { 58 | detailLabel = UILabel() 59 | detailLabel.text = "Subtitle" 60 | detailLabel.textColor = .secondaryLabel 61 | detailLabel.font = .systemFont(ofSize: 13) 62 | detailLabel.translatesAutoresizingMaskIntoConstraints = false 63 | 64 | addSubview(detailLabel) 65 | 66 | NSLayoutConstraint.activate([ 67 | detailLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 3), 68 | detailLabel.centerXAnchor.constraint(equalTo: centerXAnchor) 69 | ]) 70 | } 71 | 72 | private func setupSeparatorView() { 73 | separatorView = UIView() 74 | separatorView.translatesAutoresizingMaskIntoConstraints = false 75 | separatorView.backgroundColor = UIColor.value(forKey: "_systemChromeShadowColor") as? UIColor 76 | 77 | addSubview(separatorView) 78 | 79 | NSLayoutConstraint.activate([ 80 | separatorView.topAnchor.constraint(equalTo: bottomAnchor), 81 | separatorView.leadingAnchor.constraint(equalTo: leadingAnchor), 82 | separatorView.trailingAnchor.constraint(equalTo: trailingAnchor), 83 | separatorView.heightAnchor.constraint(equalToConstant: 1.0 / 3.0) 84 | ]) 85 | } 86 | } 87 | --------------------------------------------------------------------------------