├── .gitignore ├── CardPCDemo.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── CardModal.xcscheme ├── CardPresentationController.mp4 ├── CardPresentationController.podspec ├── CardPresentationController.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── CardPresentationController.xcscheme ├── CardPresentationController ├── CardAnimator.swift ├── CardConfiguration.swift ├── CardPresentationController.swift ├── CardTransitionManager.swift └── UIKit-CardPresentationExtensions.swift ├── EmbeddedNCExample ├── EmbeddedNCExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── EmbeddedNCExample │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ ├── Icon-App-83.5x83.5@2x.png │ │ └── ItunesArtwork@2x.png │ ├── Contents.json │ └── WonderWomanPoster.imageset │ │ ├── Contents.json │ │ └── WonderWomanPoster.jpg │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── ContentController.storyboard │ ├── ContentController.swift │ ├── Info.plist │ └── ViewController.swift ├── Example ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ ├── Icon-App-83.5x83.5@2x.png │ │ └── ItunesArtwork@2x.png │ ├── Contents.json │ └── WonderWomanPoster.imageset │ │ ├── Contents.json │ │ └── WonderWomanPoster.jpg ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── ContentController.storyboard ├── ContentController.swift ├── CustomContainerController.storyboard ├── CustomContainerController.swift ├── GridCell.swift ├── GridCell.xib ├── GridController.swift ├── GridLayout.swift ├── Info.plist ├── PopupNavigationController.swift ├── SecondController.storyboard ├── SecondController.swift └── ViewController.swift ├── Framework ├── CardPresentationController.h └── Info.plist ├── LICENSE ├── README.md ├── Vendor ├── BaseGridLayout.swift ├── Controllers │ ├── Embeddable.swift │ └── StoryboardLoadable.swift ├── GradientView.swift └── Views │ ├── DequeableView.swift │ ├── NibLoadable.swift │ └── ReusableView.swift └── resources ├── apple-music-iphone-xs.acorn ├── apple-music-iphone-xs.png ├── cardpc.png ├── presentedNC-top.png ├── presentedNC.png ├── presentedVC-top.png └── presentedVC.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | 28 | ## Playgrounds 29 | timeline.xctimeline 30 | playground.xcworkspace 31 | 32 | # Swift Package Manager 33 | # 34 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 35 | # Packages/ 36 | .build/ 37 | 38 | # CocoaPods 39 | # 40 | # We recommend against adding the Pods directory to your .gitignore. However 41 | # you should judge for yourself, the pros and cons are mentioned at: 42 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 43 | # 44 | # Pods/ 45 | 46 | # Carthage 47 | # 48 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 49 | # Carthage/Checkouts 50 | 51 | Carthage/Build 52 | 53 | # fastlane 54 | # 55 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 56 | # screenshots whenever they are needed. 57 | # For more information about the recommended setup visit: 58 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 59 | 60 | fastlane/report.xml 61 | fastlane/screenshots 62 | 63 | # big files 64 | *.mov 65 | *.mp4 -------------------------------------------------------------------------------- /CardPCDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CardPCDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CardPCDemo.xcodeproj/xcshareddata/xcschemes/CardModal.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /CardPresentationController.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/CardPresentationController.mp4 -------------------------------------------------------------------------------- /CardPresentationController.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'CardPresentationController' 3 | s.version = '1.6' 4 | s.summary = 'Custom UIPresentationController which mimics the behavior of Apple Music UI' 5 | s.description = 'Configurable, supports interactive dismissal and really easy to integrate. Fallbacks to system look & behavior on iOS 13.' 6 | s.homepage = 'https://github.com/radianttap/CardPresentationController' 7 | s.license = { :type => "MIT", :file => "LICENSE" } 8 | s.author = { 'Aleksandar Vacić' => 'radianttap.com' } 9 | s.social_media_url = "https://twitter.com/radiantav" 10 | s.ios.deployment_target = "10.0" 11 | s.source = { :git => "https://github.com/radianttap/CardPresentationController.git" } 12 | s.source_files = 'CardPresentationController/*.swift' 13 | s.frameworks = 'UIKit' 14 | 15 | s.swift_version = '5.0' 16 | end 17 | 18 | -------------------------------------------------------------------------------- /CardPresentationController.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | D0ECC50721FF7B72006D5ED1 /* CardPresentationController.h in Headers */ = {isa = PBXBuildFile; fileRef = D0ECC50521FF7B72006D5ED1 /* CardPresentationController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | D0ECC51021FF7B9D006D5ED1 /* CardPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05FF1E8214D54B50017A263 /* CardPresentationController.swift */; }; 12 | D0ECC51121FF7B9D006D5ED1 /* CardTransitionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D051930B214D6C690047AB2D /* CardTransitionManager.swift */; }; 13 | D0ECC51221FF7B9D006D5ED1 /* CardAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D051930D214D6DD10047AB2D /* CardAnimator.swift */; }; 14 | D0ECC51321FF7B9D006D5ED1 /* CardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AB35EF21C56F040038C6C6 /* CardConfiguration.swift */; }; 15 | D0ECC51421FF7B9D006D5ED1 /* UIKit-CardPresentationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AC738021BC61B800054FC3 /* UIKit-CardPresentationExtensions.swift */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | D051930B214D6C690047AB2D /* CardTransitionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardTransitionManager.swift; sourceTree = ""; }; 20 | D051930D214D6DD10047AB2D /* CardAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardAnimator.swift; sourceTree = ""; }; 21 | D05FF1E8214D54B50017A263 /* CardPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentationController.swift; sourceTree = ""; }; 22 | D0AB35EF21C56F040038C6C6 /* CardConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardConfiguration.swift; sourceTree = ""; }; 23 | D0AC738021BC61B800054FC3 /* UIKit-CardPresentationExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIKit-CardPresentationExtensions.swift"; sourceTree = ""; }; 24 | D0CE0567220442E300012062 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 25 | D0CE05682204458A00012062 /* CardPresentationController.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = CardPresentationController.podspec; sourceTree = ""; }; 26 | D0ECC50321FF7B71006D5ED1 /* CardPresentationController.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CardPresentationController.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | D0ECC50521FF7B72006D5ED1 /* CardPresentationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CardPresentationController.h; sourceTree = ""; }; 28 | D0ECC50621FF7B72006D5ED1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | D0ECC50021FF7B71006D5ED1 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | D01ADC72222BE2EF0007ACD8 /* Framework */ = { 43 | isa = PBXGroup; 44 | children = ( 45 | D0ECC50521FF7B72006D5ED1 /* CardPresentationController.h */, 46 | D0ECC50621FF7B72006D5ED1 /* Info.plist */, 47 | ); 48 | path = Framework; 49 | sourceTree = ""; 50 | }; 51 | D05FF1C9214D53520017A263 = { 52 | isa = PBXGroup; 53 | children = ( 54 | D0CE05682204458A00012062 /* CardPresentationController.podspec */, 55 | D0CE0567220442E300012062 /* README.md */, 56 | D05FF1E7214D54970017A263 /* CardPresentationController */, 57 | D01ADC72222BE2EF0007ACD8 /* Framework */, 58 | D05FF1D3214D53520017A263 /* Products */, 59 | ); 60 | sourceTree = ""; 61 | }; 62 | D05FF1D3214D53520017A263 /* Products */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | D0ECC50321FF7B71006D5ED1 /* CardPresentationController.framework */, 66 | ); 67 | name = Products; 68 | sourceTree = ""; 69 | }; 70 | D05FF1E7214D54970017A263 /* CardPresentationController */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | D05FF1E8214D54B50017A263 /* CardPresentationController.swift */, 74 | D051930B214D6C690047AB2D /* CardTransitionManager.swift */, 75 | D051930D214D6DD10047AB2D /* CardAnimator.swift */, 76 | D0AB35EF21C56F040038C6C6 /* CardConfiguration.swift */, 77 | D0AC738021BC61B800054FC3 /* UIKit-CardPresentationExtensions.swift */, 78 | ); 79 | path = CardPresentationController; 80 | sourceTree = ""; 81 | }; 82 | /* End PBXGroup section */ 83 | 84 | /* Begin PBXHeadersBuildPhase section */ 85 | D0ECC4FE21FF7B71006D5ED1 /* Headers */ = { 86 | isa = PBXHeadersBuildPhase; 87 | buildActionMask = 2147483647; 88 | files = ( 89 | D0ECC50721FF7B72006D5ED1 /* CardPresentationController.h in Headers */, 90 | ); 91 | runOnlyForDeploymentPostprocessing = 0; 92 | }; 93 | /* End PBXHeadersBuildPhase section */ 94 | 95 | /* Begin PBXNativeTarget section */ 96 | D0ECC50221FF7B71006D5ED1 /* CardPresentationController */ = { 97 | isa = PBXNativeTarget; 98 | buildConfigurationList = D0ECC50C21FF7B72006D5ED1 /* Build configuration list for PBXNativeTarget "CardPresentationController" */; 99 | buildPhases = ( 100 | D0ECC4FE21FF7B71006D5ED1 /* Headers */, 101 | D0ECC4FF21FF7B71006D5ED1 /* Sources */, 102 | D0ECC50021FF7B71006D5ED1 /* Frameworks */, 103 | D0ECC50121FF7B71006D5ED1 /* Resources */, 104 | ); 105 | buildRules = ( 106 | ); 107 | dependencies = ( 108 | ); 109 | name = CardPresentationController; 110 | productName = CardPresentationController; 111 | productReference = D0ECC50321FF7B71006D5ED1 /* CardPresentationController.framework */; 112 | productType = "com.apple.product-type.framework"; 113 | }; 114 | /* End PBXNativeTarget section */ 115 | 116 | /* Begin PBXProject section */ 117 | D05FF1CA214D53520017A263 /* Project object */ = { 118 | isa = PBXProject; 119 | attributes = { 120 | LastSwiftUpdateCheck = 1000; 121 | LastUpgradeCheck = 1000; 122 | ORGANIZATIONNAME = "Aleksandar Vacić"; 123 | TargetAttributes = { 124 | D0ECC50221FF7B71006D5ED1 = { 125 | CreatedOnToolsVersion = 10.1; 126 | LastSwiftMigration = 1020; 127 | }; 128 | }; 129 | }; 130 | buildConfigurationList = D05FF1CD214D53520017A263 /* Build configuration list for PBXProject "CardPresentationController" */; 131 | compatibilityVersion = "Xcode 9.3"; 132 | developmentRegion = en; 133 | hasScannedForEncodings = 0; 134 | knownRegions = ( 135 | en, 136 | Base, 137 | ); 138 | mainGroup = D05FF1C9214D53520017A263; 139 | productRefGroup = D05FF1D3214D53520017A263 /* Products */; 140 | projectDirPath = ""; 141 | projectRoot = ""; 142 | targets = ( 143 | D0ECC50221FF7B71006D5ED1 /* CardPresentationController */, 144 | ); 145 | }; 146 | /* End PBXProject section */ 147 | 148 | /* Begin PBXResourcesBuildPhase section */ 149 | D0ECC50121FF7B71006D5ED1 /* Resources */ = { 150 | isa = PBXResourcesBuildPhase; 151 | buildActionMask = 2147483647; 152 | files = ( 153 | ); 154 | runOnlyForDeploymentPostprocessing = 0; 155 | }; 156 | /* End PBXResourcesBuildPhase section */ 157 | 158 | /* Begin PBXSourcesBuildPhase section */ 159 | D0ECC4FF21FF7B71006D5ED1 /* Sources */ = { 160 | isa = PBXSourcesBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | D0ECC51321FF7B9D006D5ED1 /* CardConfiguration.swift in Sources */, 164 | D0ECC51121FF7B9D006D5ED1 /* CardTransitionManager.swift in Sources */, 165 | D0ECC51021FF7B9D006D5ED1 /* CardPresentationController.swift in Sources */, 166 | D0ECC51221FF7B9D006D5ED1 /* CardAnimator.swift in Sources */, 167 | D0ECC51421FF7B9D006D5ED1 /* UIKit-CardPresentationExtensions.swift in Sources */, 168 | ); 169 | runOnlyForDeploymentPostprocessing = 0; 170 | }; 171 | /* End PBXSourcesBuildPhase section */ 172 | 173 | /* Begin XCBuildConfiguration section */ 174 | D05FF1E2214D53540017A263 /* Debug */ = { 175 | isa = XCBuildConfiguration; 176 | buildSettings = { 177 | ALWAYS_SEARCH_USER_PATHS = NO; 178 | CLANG_ANALYZER_NONNULL = YES; 179 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 180 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 181 | CLANG_CXX_LIBRARY = "libc++"; 182 | CLANG_ENABLE_MODULES = YES; 183 | CLANG_ENABLE_OBJC_ARC = YES; 184 | CLANG_ENABLE_OBJC_WEAK = YES; 185 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 186 | CLANG_WARN_BOOL_CONVERSION = YES; 187 | CLANG_WARN_COMMA = YES; 188 | CLANG_WARN_CONSTANT_CONVERSION = YES; 189 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 190 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 191 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 192 | CLANG_WARN_EMPTY_BODY = YES; 193 | CLANG_WARN_ENUM_CONVERSION = YES; 194 | CLANG_WARN_INFINITE_RECURSION = YES; 195 | CLANG_WARN_INT_CONVERSION = YES; 196 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 197 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 198 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 199 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 200 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 201 | CLANG_WARN_STRICT_PROTOTYPES = YES; 202 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 203 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 204 | CLANG_WARN_UNREACHABLE_CODE = YES; 205 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 206 | CODE_SIGN_IDENTITY = "iPhone Developer"; 207 | COPY_PHASE_STRIP = NO; 208 | CURRENT_PROJECT_VERSION = 24; 209 | DEBUG_INFORMATION_FORMAT = dwarf; 210 | DYLIB_CURRENT_VERSION = "$(CURRENT_PROJECT_VERSION)"; 211 | ENABLE_STRICT_OBJC_MSGSEND = YES; 212 | ENABLE_TESTABILITY = YES; 213 | GCC_C_LANGUAGE_STANDARD = gnu11; 214 | GCC_DYNAMIC_NO_PIC = NO; 215 | GCC_NO_COMMON_BLOCKS = YES; 216 | GCC_OPTIMIZATION_LEVEL = 0; 217 | GCC_PREPROCESSOR_DEFINITIONS = ( 218 | "DEBUG=1", 219 | "$(inherited)", 220 | ); 221 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 222 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 223 | GCC_WARN_UNDECLARED_SELECTOR = YES; 224 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 225 | GCC_WARN_UNUSED_FUNCTION = YES; 226 | GCC_WARN_UNUSED_VARIABLE = YES; 227 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 228 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 229 | MTL_FAST_MATH = YES; 230 | ONLY_ACTIVE_ARCH = YES; 231 | SDKROOT = iphoneos; 232 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 233 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 234 | SWIFT_VERSION = 5.0; 235 | VERSIONING_SYSTEM = "apple-generic"; 236 | }; 237 | name = Debug; 238 | }; 239 | D05FF1E3214D53540017A263 /* Release */ = { 240 | isa = XCBuildConfiguration; 241 | buildSettings = { 242 | ALWAYS_SEARCH_USER_PATHS = NO; 243 | CLANG_ANALYZER_NONNULL = YES; 244 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 245 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 246 | CLANG_CXX_LIBRARY = "libc++"; 247 | CLANG_ENABLE_MODULES = YES; 248 | CLANG_ENABLE_OBJC_ARC = YES; 249 | CLANG_ENABLE_OBJC_WEAK = YES; 250 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 251 | CLANG_WARN_BOOL_CONVERSION = YES; 252 | CLANG_WARN_COMMA = YES; 253 | CLANG_WARN_CONSTANT_CONVERSION = YES; 254 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 255 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 256 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 257 | CLANG_WARN_EMPTY_BODY = YES; 258 | CLANG_WARN_ENUM_CONVERSION = YES; 259 | CLANG_WARN_INFINITE_RECURSION = YES; 260 | CLANG_WARN_INT_CONVERSION = YES; 261 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 262 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 263 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 264 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 265 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 266 | CLANG_WARN_STRICT_PROTOTYPES = YES; 267 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 268 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 269 | CLANG_WARN_UNREACHABLE_CODE = YES; 270 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 271 | CODE_SIGN_IDENTITY = "iPhone Developer"; 272 | COPY_PHASE_STRIP = NO; 273 | CURRENT_PROJECT_VERSION = 24; 274 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 275 | DYLIB_CURRENT_VERSION = "$(CURRENT_PROJECT_VERSION)"; 276 | ENABLE_NS_ASSERTIONS = NO; 277 | ENABLE_STRICT_OBJC_MSGSEND = YES; 278 | GCC_C_LANGUAGE_STANDARD = gnu11; 279 | GCC_NO_COMMON_BLOCKS = YES; 280 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 281 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 282 | GCC_WARN_UNDECLARED_SELECTOR = YES; 283 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 284 | GCC_WARN_UNUSED_FUNCTION = YES; 285 | GCC_WARN_UNUSED_VARIABLE = YES; 286 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 287 | MTL_ENABLE_DEBUG_INFO = NO; 288 | MTL_FAST_MATH = YES; 289 | SDKROOT = iphoneos; 290 | SWIFT_COMPILATION_MODE = wholemodule; 291 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 292 | SWIFT_VERSION = 5.0; 293 | VALIDATE_PRODUCT = YES; 294 | VERSIONING_SYSTEM = "apple-generic"; 295 | }; 296 | name = Release; 297 | }; 298 | D0ECC50D21FF7B72006D5ED1 /* Debug */ = { 299 | isa = XCBuildConfiguration; 300 | buildSettings = { 301 | CODE_SIGN_IDENTITY = ""; 302 | CODE_SIGN_STYLE = Automatic; 303 | CURRENT_PROJECT_VERSION = 30; 304 | DEFINES_MODULE = YES; 305 | DEVELOPMENT_TEAM = ""; 306 | DYLIB_COMPATIBILITY_VERSION = 1; 307 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 308 | INFOPLIST_FILE = "$(SRCROOT)/Framework/Info.plist"; 309 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 310 | LD_RUNPATH_SEARCH_PATHS = ( 311 | "$(inherited)", 312 | "@executable_path/Frameworks", 313 | "@loader_path/Frameworks", 314 | ); 315 | MARKETING_VERSION = 1.6; 316 | PRODUCT_BUNDLE_IDENTIFIER = com.radianttap.CardPresentationController; 317 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 318 | SKIP_INSTALL = YES; 319 | TARGETED_DEVICE_FAMILY = "1,2"; 320 | VERSIONING_SYSTEM = "apple-generic"; 321 | VERSION_INFO_PREFIX = ""; 322 | }; 323 | name = Debug; 324 | }; 325 | D0ECC50E21FF7B72006D5ED1 /* Release */ = { 326 | isa = XCBuildConfiguration; 327 | buildSettings = { 328 | CODE_SIGN_IDENTITY = ""; 329 | CODE_SIGN_STYLE = Automatic; 330 | CURRENT_PROJECT_VERSION = 30; 331 | DEFINES_MODULE = YES; 332 | DEVELOPMENT_TEAM = ""; 333 | DYLIB_COMPATIBILITY_VERSION = 1; 334 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 335 | INFOPLIST_FILE = "$(SRCROOT)/Framework/Info.plist"; 336 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 337 | LD_RUNPATH_SEARCH_PATHS = ( 338 | "$(inherited)", 339 | "@executable_path/Frameworks", 340 | "@loader_path/Frameworks", 341 | ); 342 | MARKETING_VERSION = 1.6; 343 | PRODUCT_BUNDLE_IDENTIFIER = com.radianttap.CardPresentationController; 344 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 345 | SKIP_INSTALL = YES; 346 | TARGETED_DEVICE_FAMILY = "1,2"; 347 | VERSIONING_SYSTEM = "apple-generic"; 348 | VERSION_INFO_PREFIX = ""; 349 | }; 350 | name = Release; 351 | }; 352 | /* End XCBuildConfiguration section */ 353 | 354 | /* Begin XCConfigurationList section */ 355 | D05FF1CD214D53520017A263 /* Build configuration list for PBXProject "CardPresentationController" */ = { 356 | isa = XCConfigurationList; 357 | buildConfigurations = ( 358 | D05FF1E2214D53540017A263 /* Debug */, 359 | D05FF1E3214D53540017A263 /* Release */, 360 | ); 361 | defaultConfigurationIsVisible = 0; 362 | defaultConfigurationName = Release; 363 | }; 364 | D0ECC50C21FF7B72006D5ED1 /* Build configuration list for PBXNativeTarget "CardPresentationController" */ = { 365 | isa = XCConfigurationList; 366 | buildConfigurations = ( 367 | D0ECC50D21FF7B72006D5ED1 /* Debug */, 368 | D0ECC50E21FF7B72006D5ED1 /* Release */, 369 | ); 370 | defaultConfigurationIsVisible = 0; 371 | defaultConfigurationName = Release; 372 | }; 373 | /* End XCConfigurationList section */ 374 | }; 375 | rootObject = D05FF1CA214D53520017A263 /* Project object */; 376 | } 377 | -------------------------------------------------------------------------------- /CardPresentationController.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CardPresentationController.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CardPresentationController.xcodeproj/xcshareddata/xcschemes/CardPresentationController.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /CardPresentationController/CardAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardAnimator.swift 3 | // CardPresentationController 4 | // 5 | // Copyright © 2018 Aleksandar Vacić, Radiant Tap 6 | // MIT License · http://choosealicense.com/licenses/mit/ 7 | // 8 | 9 | import UIKit 10 | 11 | @available(iOS 10.0, *) 12 | final class CardAnimator: NSObject, UIViewControllerAnimatedTransitioning { 13 | enum Direction { 14 | case presentation 15 | case dismissal 16 | } 17 | 18 | // Init 19 | 20 | var direction: Direction 21 | var configuration: CardConfiguration 22 | var isInteractive = false 23 | 24 | init(direction: Direction = .presentation, configuration: CardConfiguration) { 25 | self.direction = direction 26 | self.configuration = configuration 27 | super.init() 28 | } 29 | 30 | // Animators 31 | 32 | private(set) lazy var presentationAnimator: UIViewPropertyAnimator = setupAnimator(.presentation) 33 | private(set) lazy var dismissAnimator: UIViewPropertyAnimator = setupAnimator(.dismissal) 34 | 35 | private weak var transitionContext: UIViewControllerContextTransitioning? 36 | 37 | private var interactiveAnimator: UIViewPropertyAnimator { 38 | switch direction { 39 | case .presentation: 40 | return presentationAnimator 41 | case .dismissal: 42 | return dismissAnimator 43 | } 44 | } 45 | 46 | // Local configuration 47 | 48 | private var verticalSpacing: CGFloat { return configuration.verticalSpacing } 49 | private var verticalInset: CGFloat { return configuration.verticalInset } 50 | private var horizontalInset: CGFloat { return configuration.horizontalInset } 51 | private var cornerRadius: CGFloat { return configuration.cornerRadius } 52 | private var backFadeAlpha: CGFloat { return configuration.backFadeAlpha } 53 | private var initialTransitionFrame: CGRect? { return configuration.initialTransitionFrame } 54 | 55 | // MARK:- UIViewControllerAnimatedTransitioning 56 | 57 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 58 | let interval: TimeInterval 59 | switch direction { 60 | case .presentation: 61 | interval = 0.65 62 | case .dismissal: 63 | interval = 0.55 64 | } 65 | return interval 66 | } 67 | 68 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 69 | guard let pa = buildAnimator(for: transitionContext) else { 70 | return 71 | } 72 | pa.startAnimation() 73 | } 74 | 75 | func animationEnded(_ transitionCompleted: Bool) { 76 | isInteractive = false 77 | } 78 | } 79 | 80 | extension CardAnimator: UIViewControllerInteractiveTransitioning { 81 | func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) { 82 | guard let _ = buildAnimator(for: transitionContext) else { 83 | return 84 | } 85 | self.transitionContext = transitionContext 86 | } 87 | 88 | var wantsInteractiveStart: Bool { 89 | return isInteractive 90 | } 91 | 92 | func updateInteractiveTransition(_ percentComplete: CGFloat) { 93 | let pa = interactiveAnimator 94 | 95 | pa.fractionComplete = percentComplete 96 | transitionContext?.updateInteractiveTransition(percentComplete) 97 | } 98 | 99 | func cancelInteractiveTransition(with velocity: CGVector = .zero) { 100 | guard isInteractive else { 101 | return 102 | } 103 | 104 | transitionContext?.cancelInteractiveTransition() 105 | 106 | interactiveAnimator.isReversed = true // animate back to starting position 107 | 108 | let pct = interactiveAnimator.fractionComplete 109 | endInteraction(from: pct, 110 | withVelocity: velocity, 111 | durationFactor: 1 - pct) 112 | } 113 | 114 | func finishInteractiveTransition(with velocity: CGVector = .zero) { 115 | guard isInteractive else { 116 | return 117 | } 118 | 119 | transitionContext?.finishInteractiveTransition() 120 | 121 | let pct = interactiveAnimator.fractionComplete 122 | endInteraction(from: pct, 123 | withVelocity: velocity, 124 | durationFactor: pct) 125 | } 126 | } 127 | 128 | 129 | // UIViewPropertyAnimator 130 | 131 | private extension CardAnimator { 132 | func endInteraction(from percentComplete: CGFloat, withVelocity velocity: CGVector, durationFactor: CGFloat) { 133 | switch interactiveAnimator.state { 134 | case .inactive: 135 | interactiveAnimator.startAnimation() 136 | default: // case .active, .stopped, @unknown-futures 137 | interactiveAnimator.continueAnimation(withTimingParameters: nil, durationFactor: durationFactor) 138 | } 139 | } 140 | 141 | func setupAnimator(_ direction: Direction) -> UIViewPropertyAnimator { 142 | let params: SpringParameters 143 | 144 | switch direction { 145 | case .presentation: 146 | params = .tap 147 | case .dismissal: 148 | params = .momentum 149 | } 150 | 151 | // entire spring animation should not last more than transitionDuration 152 | let damping = params.damping 153 | let response = params.response 154 | let timingParameters = UISpringTimingParameters(damping: damping, response: response) 155 | 156 | let pa = UIViewPropertyAnimator(duration: 0, timingParameters: timingParameters) 157 | 158 | return pa 159 | } 160 | 161 | func buildAnimator(for transitionContext: UIViewControllerContextTransitioning) -> UIViewPropertyAnimator? { 162 | guard 163 | let fromVC = transitionContext.viewController(forKey: .from), 164 | let toVC = transitionContext.viewController(forKey: .to), 165 | let fromView = fromVC.view, 166 | let toView = toVC.view 167 | else { 168 | return nil 169 | } 170 | let containerView = transitionContext.containerView 171 | 172 | switch direction { 173 | case .presentation: 174 | let sourceCardPresentationController = fromVC.presentationController as? CardPresentationController 175 | 176 | let fromEndFrame: CGRect 177 | let toEndFrame: CGRect 178 | 179 | if let sourceCardPresentationController = sourceCardPresentationController { 180 | sourceCardPresentationController.fadeoutHandle() 181 | 182 | let fromBeginFrame: CGRect 183 | if #available(iOS 13, *) { 184 | fromBeginFrame = fromView.frame 185 | } else { 186 | // on iOS 13, origin.y for this seem to always be 0 187 | fromBeginFrame = transitionContext.initialFrame(for: fromVC) 188 | } 189 | fromEndFrame = fromBeginFrame.inset(by: UIEdgeInsets(top: 0, left: horizontalInset, bottom: 0, right: horizontalInset)) 190 | 191 | toEndFrame = fromBeginFrame.inset(by: UIEdgeInsets(top: verticalSpacing, left: 0, bottom: 0, right: 0)) 192 | 193 | } else { 194 | let fromBeginFrame: CGRect 195 | if #available(iOS 13, *) { 196 | fromBeginFrame = fromView.frame 197 | } else { 198 | // on iOS 13, origin.y for this seem to always be 0 199 | fromBeginFrame = transitionContext.initialFrame(for: fromVC) 200 | } 201 | fromEndFrame = fromBeginFrame.inset(by: UIEdgeInsets(top: verticalInset, left: horizontalInset, bottom: 0, right: horizontalInset)) 202 | 203 | let toBaseFinalFrame = transitionContext.finalFrame(for: toVC) 204 | toEndFrame = toBaseFinalFrame.inset(by: UIEdgeInsets(top: verticalInset + verticalSpacing, left: 0, bottom: 0, right: 0)) 205 | } 206 | let toStartFrame = offscreenFrame(inside: containerView) 207 | 208 | toView.clipsToBounds = true 209 | toView.frame = toStartFrame 210 | toView.layoutIfNeeded() 211 | containerView.addSubview(toView) 212 | 213 | let pa = presentationAnimator 214 | pa.addAnimations() { 215 | [weak self] in 216 | guard let self = self else { return } 217 | 218 | self.insetBackCards(of: sourceCardPresentationController) 219 | fromView.frame = fromEndFrame 220 | toView.frame = toEndFrame 221 | fromView.cardMaskTopCorners(using: self.cornerRadius) 222 | toView.cardMaskTopCorners(using: self.cornerRadius) 223 | 224 | fromView.alpha = self.backFadeAlpha 225 | } 226 | 227 | pa.addCompletion() { 228 | [weak self] animatingPosition in 229 | 230 | switch animatingPosition { 231 | case .start: 232 | self?.direction = .presentation 233 | fromView.isUserInteractionEnabled = true 234 | transitionContext.completeTransition(false) 235 | default: // case .end, .current (which should not be possible), @unknown-futures 236 | self?.direction = .dismissal 237 | fromView.isUserInteractionEnabled = false 238 | transitionContext.completeTransition(true) 239 | } 240 | } 241 | 242 | return pa 243 | 244 | 245 | case .dismissal: 246 | let targetCardPresentationController = toVC.presentationController as? CardPresentationController 247 | let isTargetAlreadyCard = (targetCardPresentationController != nil) 248 | 249 | let toBeginFrame = toView.frame 250 | let toEndFrame: CGRect 251 | 252 | if let _ = targetCardPresentationController { 253 | toEndFrame = toBeginFrame.inset(by: UIEdgeInsets(top: 0, left: -horizontalInset, bottom: 0, right: -horizontalInset)) 254 | } else { 255 | toEndFrame = transitionContext.finalFrame(for: toVC) 256 | } 257 | 258 | let fromEndFrame = offscreenFrame(inside: containerView) 259 | 260 | let pa = dismissAnimator 261 | pa.addAnimations() { 262 | [weak self] in 263 | guard let self = self else { return } 264 | 265 | fromView.cardUnmask() 266 | if !isTargetAlreadyCard { 267 | toView.cardUnmask() 268 | } 269 | self.outsetBackCards(of: targetCardPresentationController) 270 | 271 | fromView.frame = fromEndFrame 272 | toView.frame = toEndFrame 273 | toView.alpha = 1 274 | fromView.alpha = 1 275 | } 276 | 277 | pa.addCompletion() { 278 | [weak self] animatingPosition in 279 | guard let self = self else { return } 280 | 281 | switch animatingPosition { 282 | case .start: 283 | self.direction = .dismissal 284 | self.dismissAnimator = self.setupAnimator(.dismissal) 285 | 286 | toView.isUserInteractionEnabled = false 287 | 288 | transitionContext.completeTransition(false) 289 | 290 | default: // case .end, .current (which should not be possible), @unknown-futures 291 | self.direction = .presentation 292 | self.presentationAnimator = self.setupAnimator(.presentation) 293 | 294 | toView.isUserInteractionEnabled = true 295 | fromView.removeFromSuperview() 296 | 297 | if let targetCardPresentationController = targetCardPresentationController { 298 | targetCardPresentationController.fadeinHandle() 299 | } 300 | 301 | transitionContext.completeTransition(true) 302 | } 303 | } 304 | 305 | return pa 306 | } 307 | } 308 | } 309 | 310 | 311 | 312 | // Helper methods 313 | 314 | private extension CardAnimator { 315 | private func insetBackCards(of pc: CardPresentationController?) { 316 | guard 317 | let pc = pc, 318 | let pcView = pc.presentingViewController.view 319 | else { return } 320 | 321 | let frame = pcView.frame 322 | pcView.frame = frame.inset(by: UIEdgeInsets(top: 0, left: horizontalInset, bottom: 0, right: horizontalInset)) 323 | pcView.alpha *= backFadeAlpha 324 | 325 | insetBackCards(of: pc.presentingViewController.presentationController as? CardPresentationController) 326 | } 327 | 328 | private func outsetBackCards(of pc: CardPresentationController?) { 329 | guard 330 | let pc = pc, 331 | let pcView = pc.presentingViewController.view 332 | else { return } 333 | 334 | let frame = pcView.frame 335 | pcView.frame = frame.inset(by: UIEdgeInsets(top: 0, left: -horizontalInset, bottom: 0, right: -horizontalInset)) 336 | pcView.alpha /= backFadeAlpha 337 | 338 | outsetBackCards(of: pc.presentingViewController.presentationController as? CardPresentationController) 339 | } 340 | 341 | func offscreenFrame(inside containerView: UIView) -> CGRect { 342 | if let initialTransitionFrame = initialTransitionFrame { 343 | return initialTransitionFrame 344 | } else { 345 | var f = containerView.frame 346 | f.origin.y = f.height 347 | return f 348 | } 349 | } 350 | 351 | struct SpringParameters { 352 | let damping: CGFloat 353 | let response: CGFloat 354 | 355 | // From amazing session 803 at WWDC 2018: https://developer.apple.com/videos/play/wwdc2018-803/?time=2238 356 | // > Because the tap doesn't have any momentum in the direction of the presentation of Now Playing, 357 | // > we use 100% damping to make sure it doesn't overshoot. 358 | // 359 | static let tap = SpringParameters(damping: 1, response: 0.44) 360 | 361 | // > But, if you swipe to dismiss Now Playing, 362 | // > there is momentum in the direction of the dismissal, 363 | // > and so we use 80% damping to have a little bit of bounce and squish, 364 | // > making the gesture a lot more satisfying. 365 | // 366 | // (note that they use momentum even when tapping to dismiss) 367 | static let momentum = SpringParameters(damping: 0.8, response: 0.44) 368 | } 369 | } 370 | 371 | 372 | private extension UISpringTimingParameters { 373 | /// A design-friendly way to create a spring timing curve. 374 | /// See: https://medium.com/@nathangitter/building-fluid-interfaces-ios-swift-9732bb934bf5 375 | /// 376 | /// - Parameters: 377 | /// - damping: The 'bounciness' of the animation. Value must be between 0 and 1. 378 | /// - response: The 'speed' of the animation. 379 | /// - initialVelocity: The vector describing the starting motion of the property. Optional, default is `.zero`. 380 | convenience init(damping: CGFloat, response: CGFloat, initialVelocity: CGVector = .zero) { 381 | let stiffness = pow(2 * .pi / response, 2) 382 | let damp = 4 * .pi * damping / response 383 | self.init(mass: 1, stiffness: stiffness, damping: damp, initialVelocity: initialVelocity) 384 | } 385 | } 386 | 387 | -------------------------------------------------------------------------------- /CardPresentationController/CardConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardConfiguration.swift 3 | // CardPresentationController 4 | // 5 | // Copyright © 2018 Aleksandar Vacić, Radiant Tap 6 | // MIT License · http://choosealicense.com/licenses/mit/ 7 | // 8 | 9 | import UIKit 10 | 11 | /// Simple package for various values defining transition's look & feel. 12 | /// 13 | /// Supply it as optional parameter to `presentCard(...)`. 14 | public struct CardConfiguration { 15 | /// Vertical inset from the top or already shown card. 16 | public var verticalSpacing: CGFloat = 16 17 | 18 | /// Top vertical inset for the existing (presenting) view when it's being pushed further back. 19 | public var verticalInset: CGFloat = UIApplication.shared.statusBarFrame.size.height 20 | 21 | /// Leading and trailing inset for the existing (presenting) view when it's being pushed further back. 22 | public var horizontalInset: CGFloat = 16 23 | 24 | /// Height of the "empty" area at the top of the card where dismiss handle glyph will be centered. 25 | public var dismissAreaHeight: CGFloat = 16 26 | 27 | /// Cards have rounded corners, right? 28 | public var cornerRadius: CGFloat = 12 29 | 30 | /// The starting frame for the presented card. 31 | public var initialTransitionFrame: CGRect? 32 | 33 | /// How much to fade the back card. 34 | public var backFadeAlpha: CGFloat = 0.8 35 | 36 | /// Set to false to disable interactive dismissal 37 | public var allowInteractiveDismissal = true 38 | 39 | /// Default initializer, with most suitable values 40 | init() {} 41 | 42 | /// Common instance of the Configuration, applied to all cards 43 | /// unless a specific instance is supplied in `presentCard()` call. 44 | /// 45 | /// Set this value early in your app lifecycle to adjust the look of all cards in your app. 46 | public static var shared = CardConfiguration() 47 | } 48 | 49 | extension CardConfiguration { 50 | /// Very convenient initializer; supply only those params which are different from default ones. 51 | public init(verticalSpacing: CGFloat? = nil, 52 | verticalInset: CGFloat? = nil, 53 | horizontalInset: CGFloat? = nil, 54 | dismissAreaHeight: CGFloat? = nil, 55 | cornerRadius: CGFloat? = nil, 56 | backFadeAlpha: CGFloat? = nil, 57 | initialTransitionFrame: CGRect? = nil, 58 | allowInteractiveDismissal: Bool? = nil) 59 | { 60 | if let verticalSpacing = verticalSpacing { 61 | self.verticalSpacing = verticalSpacing 62 | } 63 | 64 | if let verticalInset = verticalInset { 65 | self.verticalInset = verticalInset 66 | } 67 | 68 | if let horizontalInset = horizontalInset { 69 | self.horizontalInset = horizontalInset 70 | } 71 | 72 | if let dismissAreaHeight = dismissAreaHeight { 73 | self.dismissAreaHeight = dismissAreaHeight 74 | } 75 | 76 | if let cornerRadius = cornerRadius { 77 | self.cornerRadius = cornerRadius 78 | } 79 | 80 | if let backFadeAlpha = backFadeAlpha { 81 | self.backFadeAlpha = backFadeAlpha 82 | } 83 | 84 | if let initialTransitionFrame = initialTransitionFrame { 85 | self.initialTransitionFrame = initialTransitionFrame 86 | } 87 | 88 | if let allowInteractiveDismissal = allowInteractiveDismissal { 89 | self.allowInteractiveDismissal = allowInteractiveDismissal 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /CardPresentationController/CardPresentationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardPresentationController.swift 3 | // CardPresentationController 4 | // 5 | // Copyright © 2018 Aleksandar Vacić, Radiant Tap 6 | // MIT License · http://choosealicense.com/licenses/mit/ 7 | // 8 | 9 | import UIKit 10 | 11 | @available(iOS 10.0, *) 12 | public class CardPresentationController: UIPresentationController { 13 | public static var useSystemPresentationOniOS13 = true 14 | 15 | /// This is a link to the original UIVC on which presentCard() was called. 16 | /// (this is populated by CardTransitionManager) 17 | /// It's used in this file to clean-up CTM instance once dismissal happens. 18 | weak var sourceController: UIViewController? 19 | 20 | /// Required link to the actual animator, 21 | /// so that pan gesture handler can drive the animation 22 | weak var cardAnimator: CardAnimator! 23 | 24 | /// How much space is available at the top of the presentedView (card) to draw the dismiss glyph (handle). 25 | /// 26 | /// By default, it's assumed it's 16pt and handle will be 5pt tall, centered in the middle of that area 27 | /// (thus effectivelly its center is 8pt from the top of the card). 28 | var dismissAreaHeight: CGFloat = 16 29 | 30 | 31 | 32 | // Init 33 | 34 | public private(set) var configuration: CardConfiguration 35 | 36 | public override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) { 37 | fatalError("Use init(configuration:presentedViewController:presenting:)") 38 | } 39 | 40 | public init(configuration: CardConfiguration, presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) { 41 | self.configuration = configuration 42 | super.init(presentedViewController: presentedViewController, presenting: presentingViewController) 43 | } 44 | 45 | // Private stuff 46 | 47 | private lazy var handleView: UIView = { 48 | let view = UIView() 49 | view.translatesAutoresizingMaskIntoConstraints = false 50 | view.backgroundColor = UIColor(white: 1, alpha: 0.5) 51 | view.layer.cornerRadius = 3 52 | view.alpha = 0 53 | return view 54 | }() 55 | 56 | private lazy var handleButton: UIButton = { 57 | let view = UIButton(frame: .zero) 58 | view.translatesAutoresizingMaskIntoConstraints = false 59 | view.backgroundColor = .clear 60 | return view 61 | }() 62 | 63 | private var handleTopConstraint: NSLayoutConstraint! 64 | 65 | private var usesDismissHandle: Bool { 66 | return !(presentedViewController is UINavigationController) 67 | } 68 | 69 | // MARK:- PresentationController 70 | 71 | public override func presentationTransitionWillBegin() { 72 | setupDismissHandle() 73 | 74 | super.presentationTransitionWillBegin() 75 | } 76 | 77 | public override func presentationTransitionDidEnd(_ completed: Bool) { 78 | super.presentationTransitionDidEnd(completed) 79 | 80 | if !completed { 81 | return 82 | } 83 | showDismissHandle() 84 | setupPanToDismiss() 85 | } 86 | 87 | public override func dismissalTransitionWillBegin() { 88 | fadeoutHandle() 89 | super.dismissalTransitionWillBegin() 90 | } 91 | 92 | public override func dismissalTransitionDidEnd(_ completed: Bool) { 93 | super.dismissalTransitionDidEnd(completed) 94 | if !completed { 95 | return 96 | } 97 | sourceController?.removeCardTransitionManager() 98 | } 99 | 100 | // MARK:- Public 101 | 102 | func fadeinHandle() { 103 | UIView.animate(withDuration: 0.15) { 104 | [weak self] in 105 | guard let self = self else { return } 106 | self.handleView.alpha = 1 107 | } 108 | } 109 | 110 | func fadeoutHandle() { 111 | UIView.animate(withDuration: 0.15) { 112 | [weak self] in 113 | guard let self = self else { return } 114 | self.handleView.alpha = 0 115 | } 116 | } 117 | 118 | // MARK:- Internal 119 | 120 | @objc private func handleTapped(_ sender: UIButton) { 121 | handleView.alpha = 0 122 | presentedViewController.dismiss(animated: true) 123 | } 124 | 125 | private func setupDismissHandle() { 126 | guard 127 | usesDismissHandle, 128 | let containerView = containerView 129 | else { return } 130 | 131 | containerView.addSubview(handleView) 132 | handleView.widthAnchor.constraint(equalToConstant: 50).isActive = true 133 | handleView.heightAnchor.constraint(equalToConstant: 5).isActive = true 134 | handleView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).isActive = true 135 | 136 | handleTopConstraint = handleView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10) 137 | handleTopConstraint.isActive = true 138 | 139 | containerView.addSubview(handleButton) 140 | handleButton.widthAnchor.constraint(equalTo: handleView.widthAnchor).isActive = true 141 | handleButton.heightAnchor.constraint(equalTo: handleView.widthAnchor).isActive = true 142 | handleButton.centerYAnchor.constraint(equalTo: handleView.centerYAnchor).isActive = true 143 | handleButton.centerXAnchor.constraint(equalTo: handleView.centerXAnchor).isActive = true 144 | 145 | handleButton.addTarget(self, action: #selector(handleTapped), for: .touchUpInside) 146 | } 147 | 148 | private func showDismissHandle() { 149 | guard 150 | usesDismissHandle, 151 | let containerView = containerView 152 | else { return } 153 | 154 | containerView.bringSubviewToFront(handleView) 155 | containerView.bringSubviewToFront(handleButton) 156 | 157 | // Center dismiss handle in the (hopefully empty) area at the top of the presented card. 158 | // Place in the middle of that space. 159 | if let v = presentedViewController.view { 160 | let handleCenterY = v.frame.minY + dismissAreaHeight / 2 161 | handleTopConstraint.constant = handleCenterY - handleView.frame.height / 2 162 | } 163 | 164 | self.handleView.superview?.layoutIfNeeded() 165 | self.fadeinHandle() 166 | } 167 | 168 | // MARK:- Pan to dismiss 169 | 170 | private var panGR: UIPanGestureRecognizer? 171 | private var hasStartedPan = false 172 | 173 | private func setupPanToDismiss() { 174 | if !configuration.allowInteractiveDismissal { return } 175 | 176 | let gr = UIPanGestureRecognizer(target: self, action: #selector(panned)) 177 | gr.delegate = self 178 | 179 | containerView?.addGestureRecognizer(gr) 180 | panGR = gr 181 | } 182 | 183 | @objc private func panned(_ gr: UIPanGestureRecognizer) { 184 | guard let containerView = containerView else { return } 185 | 186 | let verticalMove = gr.translation(in: containerView).y 187 | let pct = verticalMove / containerView.bounds.height 188 | let verticalVelocity = gr.velocity(in: containerView) 189 | 190 | switch gr.state { 191 | case .began: 192 | // do not start dismiss until pan goes down 193 | if verticalMove <= 0 { return } 194 | // setup flag that pan has finally started in the correct direction 195 | hasStartedPan = true 196 | // and reset the movement so far 197 | gr.setTranslation(.zero, in: containerView) 198 | 199 | // tell Animator that this will be interactive 200 | cardAnimator.isInteractive = true 201 | 202 | // and then initiate dismissal 203 | presentedViewController.dismiss(animated: true) 204 | 205 | case .changed: 206 | if !hasStartedPan { return } 207 | cardAnimator.updateInteractiveTransition(pct) 208 | // handleView.alpha = max(0, 1 - pct * 4) // handle disappears 4x faster 209 | 210 | case .ended, .cancelled: 211 | if !hasStartedPan { return } 212 | let vector = verticalVelocity.vector 213 | 214 | if verticalVelocity.y < 0 { 215 | cardAnimator.cancelInteractiveTransition(with: vector) 216 | handleView.alpha = 1 217 | 218 | } else if verticalVelocity.y > 0 { 219 | cardAnimator.finishInteractiveTransition(with: vector) 220 | handleView.alpha = 0 221 | 222 | } else { 223 | if pct < 0.5 { 224 | cardAnimator.cancelInteractiveTransition(with: vector) 225 | handleView.alpha = 1 226 | } else { 227 | cardAnimator.finishInteractiveTransition(with: vector) 228 | handleView.alpha = 0 229 | } 230 | } 231 | hasStartedPan = false 232 | 233 | default: 234 | break 235 | } 236 | } 237 | } 238 | 239 | extension CardPresentationController: UIGestureRecognizerDelegate { 240 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, 241 | shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool 242 | { 243 | if gestureRecognizer != panGR { return true } 244 | 245 | let otherView = otherGestureRecognizer.view 246 | 247 | // allow unconditional panning if that other view is not `UIScrollView` 248 | guard let scrollView = otherView as? UIScrollView else { 249 | return true 250 | } 251 | 252 | // if it is `UIScrollView`, 253 | // allow panning only if its content is at the very top 254 | if (scrollView.contentOffset.y + scrollView.contentInset.top) == 0 { 255 | return true 256 | } 257 | 258 | // otherwise, disallow pan to dismiss 259 | return false 260 | } 261 | } 262 | 263 | private extension CGPoint { 264 | var vector: CGVector { 265 | return CGVector(dx: x, dy: y) 266 | } 267 | } 268 | 269 | -------------------------------------------------------------------------------- /CardPresentationController/CardTransitionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardTransitionManager.swift 3 | // CardPresentationController 4 | // 5 | // Copyright © 2018 Aleksandar Vacić, Radiant Tap 6 | // MIT License · http://choosealicense.com/licenses/mit/ 7 | // 8 | 9 | import UIKit 10 | 11 | @available(iOS 10.0, *) 12 | final class CardTransitionManager: NSObject, UIViewControllerTransitioningDelegate { 13 | private(set) var configuration: CardConfiguration 14 | 15 | init(configuration: CardConfiguration) { 16 | self.configuration = configuration 17 | super.init() 18 | } 19 | 20 | private lazy var cardAnimator = CardAnimator(configuration: configuration) 21 | 22 | func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { 23 | let pc = CardPresentationController(configuration: configuration, presentedViewController: presented, presenting: presenting) 24 | pc.sourceController = source 25 | pc.cardAnimator = cardAnimator 26 | pc.dismissAreaHeight = configuration.dismissAreaHeight 27 | return pc 28 | } 29 | 30 | func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 31 | cardAnimator.direction = .presentation 32 | return cardAnimator 33 | } 34 | 35 | func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 36 | cardAnimator.direction = .dismissal 37 | return cardAnimator 38 | } 39 | 40 | func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 41 | if cardAnimator.isInteractive { return cardAnimator } 42 | return nil 43 | } 44 | 45 | func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 46 | if cardAnimator.isInteractive { return cardAnimator } 47 | return nil 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /CardPresentationController/UIKit-CardPresentationExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKit-CardPresentationExtensions.swift 3 | // CardPresentationController 4 | // 5 | // Created by Aleksandar Vacić on 12/8/18. 6 | // Copyright © 2018 Aleksandar Vacić. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIViewController { 12 | private struct AssociatedKeys { 13 | static var cardTransitionManager = "CardTransitionManager" 14 | } 15 | private(set) var cardTransitionManager: CardTransitionManager? { 16 | get { 17 | return objc_getAssociatedObject(self, &AssociatedKeys.cardTransitionManager) as? CardTransitionManager 18 | } 19 | set { 20 | objc_setAssociatedObject(self, &AssociatedKeys.cardTransitionManager, newValue, .OBJC_ASSOCIATION_RETAIN) 21 | } 22 | } 23 | 24 | 25 | @available(iOS 10.0, *) 26 | /// Presents given View Controller using custom Card-like modal transition. Think like Apple‘s Music or Wallet apps. 27 | /// 28 | /// Existing view will slide down and fade a bit and top corners would be rounded. 29 | /// Presented controller will slide up, over it, slightly below and also with rounder corners. 30 | /// 31 | /// - Parameters: 32 | /// - viewControllerToPresent: `UIViewController` instance to present. 33 | /// - configuration: an instance of `CardConfiguration`. By default it's `nil` which means that defaults will be used. 34 | /// - flag: Pass `true` to animate the presentation; otherwise, `pass` false. 35 | /// - completion: The closure to execute after the presentation finishes. This closure has no return value and takes no parameters. You may specify `nil` for this parameter or omit it entirely. 36 | public func presentCard(_ viewControllerToPresent: UIViewController, 37 | configuration: CardConfiguration? = nil, 38 | animated flag: Bool, 39 | completion: (() -> Void)? = nil) 40 | { 41 | if #available(iOS 13, *), CardPresentationController.useSystemPresentationOniOS13 { 42 | // if we are on iOS 13, fallback to default system look & behavior 43 | present(viewControllerToPresent, animated: flag, completion: completion) 44 | } else { 45 | // make it custom 46 | viewControllerToPresent.modalPresentationStyle = .custom 47 | 48 | // enforce statusBarStyle preferred by presented UIVC 49 | viewControllerToPresent.modalPresentationCapturesStatusBarAppearance = true 50 | 51 | // card config, using supplied or default 52 | let config = configuration ?? CardConfiguration.shared 53 | 54 | // then build transition manager 55 | let tm = CardTransitionManager(configuration: config) 56 | self.cardTransitionManager = tm 57 | viewControllerToPresent.transitioningDelegate = tm 58 | 59 | present(viewControllerToPresent, 60 | animated: flag, 61 | completion: completion) 62 | } 63 | } 64 | 65 | func removeCardTransitionManager() { 66 | cardTransitionManager = nil 67 | } 68 | } 69 | 70 | extension UIView { 71 | func cardMaskTopCorners(using cornerRadius: CGFloat = 24) { 72 | clipsToBounds = true 73 | 74 | // from iOS 11, it's possible to choose which corners are rounded 75 | if #available(iOS 11.0, *) { 76 | layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] 77 | } 78 | 79 | layer.cornerRadius = cornerRadius 80 | } 81 | 82 | func cardUnmask() { 83 | layer.cornerRadius = 0 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | D021C72721C3FDB0004CFA5A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021C72621C3FDB0004CFA5A /* AppDelegate.swift */; }; 11 | D021C72921C3FDB0004CFA5A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021C72821C3FDB0004CFA5A /* ViewController.swift */; }; 12 | D021C72C21C3FDB0004CFA5A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D021C72A21C3FDB0004CFA5A /* Main.storyboard */; }; 13 | D021C72E21C3FDB2004CFA5A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D021C72D21C3FDB2004CFA5A /* Assets.xcassets */; }; 14 | D021C73121C3FDB2004CFA5A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D021C72F21C3FDB2004CFA5A /* LaunchScreen.storyboard */; }; 15 | D021C73D21C3FDEB004CFA5A /* UIKit-CardPresentationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021C73921C3FDEB004CFA5A /* UIKit-CardPresentationExtensions.swift */; }; 16 | D021C73E21C3FDEB004CFA5A /* CardPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021C73A21C3FDEB004CFA5A /* CardPresentationController.swift */; }; 17 | D021C73F21C3FDEB004CFA5A /* CardAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021C73B21C3FDEB004CFA5A /* CardAnimator.swift */; }; 18 | D021C74021C3FDEB004CFA5A /* CardTransitionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021C73C21C3FDEB004CFA5A /* CardTransitionManager.swift */; }; 19 | D021C74921C3FE0B004CFA5A /* StoryboardLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021C74321C3FE0B004CFA5A /* StoryboardLoadable.swift */; }; 20 | D021C74A21C3FE0B004CFA5A /* Embeddable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021C74421C3FE0B004CFA5A /* Embeddable.swift */; }; 21 | D021C74B21C3FE0B004CFA5A /* DequeableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021C74621C3FE0B004CFA5A /* DequeableView.swift */; }; 22 | D021C74C21C3FE0B004CFA5A /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021C74721C3FE0B004CFA5A /* ReusableView.swift */; }; 23 | D021C74D21C3FE0B004CFA5A /* NibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021C74821C3FE0B004CFA5A /* NibLoadable.swift */; }; 24 | D021C75021C3FE68004CFA5A /* ContentController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D021C74E21C3FE68004CFA5A /* ContentController.storyboard */; }; 25 | D021C75121C3FE68004CFA5A /* ContentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021C74F21C3FE68004CFA5A /* ContentController.swift */; }; 26 | D0AB35F221C5AB920038C6C6 /* CardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AB35F121C5AB920038C6C6 /* CardConfiguration.swift */; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXFileReference section */ 30 | D021C72321C3FDB0004CFA5A /* EmbeddedNCExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EmbeddedNCExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 31 | D021C72621C3FDB0004CFA5A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 32 | D021C72821C3FDB0004CFA5A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 33 | D021C72B21C3FDB0004CFA5A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 34 | D021C72D21C3FDB2004CFA5A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 35 | D021C73021C3FDB2004CFA5A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 36 | D021C73221C3FDB2004CFA5A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 37 | D021C73921C3FDEB004CFA5A /* UIKit-CardPresentationExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIKit-CardPresentationExtensions.swift"; sourceTree = ""; }; 38 | D021C73A21C3FDEB004CFA5A /* CardPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardPresentationController.swift; sourceTree = ""; }; 39 | D021C73B21C3FDEB004CFA5A /* CardAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardAnimator.swift; sourceTree = ""; }; 40 | D021C73C21C3FDEB004CFA5A /* CardTransitionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardTransitionManager.swift; sourceTree = ""; }; 41 | D021C74321C3FE0B004CFA5A /* StoryboardLoadable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoryboardLoadable.swift; sourceTree = ""; }; 42 | D021C74421C3FE0B004CFA5A /* Embeddable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Embeddable.swift; sourceTree = ""; }; 43 | D021C74621C3FE0B004CFA5A /* DequeableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DequeableView.swift; sourceTree = ""; }; 44 | D021C74721C3FE0B004CFA5A /* ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; 45 | D021C74821C3FE0B004CFA5A /* NibLoadable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NibLoadable.swift; sourceTree = ""; }; 46 | D021C74E21C3FE68004CFA5A /* ContentController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ContentController.storyboard; sourceTree = ""; }; 47 | D021C74F21C3FE68004CFA5A /* ContentController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentController.swift; sourceTree = ""; }; 48 | D0AB35F121C5AB920038C6C6 /* CardConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardConfiguration.swift; sourceTree = ""; }; 49 | /* End PBXFileReference section */ 50 | 51 | /* Begin PBXFrameworksBuildPhase section */ 52 | D021C72021C3FDB0004CFA5A /* Frameworks */ = { 53 | isa = PBXFrameworksBuildPhase; 54 | buildActionMask = 2147483647; 55 | files = ( 56 | ); 57 | runOnlyForDeploymentPostprocessing = 0; 58 | }; 59 | /* End PBXFrameworksBuildPhase section */ 60 | 61 | /* Begin PBXGroup section */ 62 | D021C71A21C3FDB0004CFA5A = { 63 | isa = PBXGroup; 64 | children = ( 65 | D021C73821C3FDEB004CFA5A /* CardPresentationController */, 66 | D021C72521C3FDB0004CFA5A /* EmbeddedNCExample */, 67 | D021C72421C3FDB0004CFA5A /* Products */, 68 | D021C74121C3FE0B004CFA5A /* Vendor */, 69 | ); 70 | sourceTree = ""; 71 | }; 72 | D021C72421C3FDB0004CFA5A /* Products */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | D021C72321C3FDB0004CFA5A /* EmbeddedNCExample.app */, 76 | ); 77 | name = Products; 78 | sourceTree = ""; 79 | }; 80 | D021C72521C3FDB0004CFA5A /* EmbeddedNCExample */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | D021C72621C3FDB0004CFA5A /* AppDelegate.swift */, 84 | D021C72821C3FDB0004CFA5A /* ViewController.swift */, 85 | D021C72A21C3FDB0004CFA5A /* Main.storyboard */, 86 | D021C72D21C3FDB2004CFA5A /* Assets.xcassets */, 87 | D021C72F21C3FDB2004CFA5A /* LaunchScreen.storyboard */, 88 | D021C73221C3FDB2004CFA5A /* Info.plist */, 89 | D021C74F21C3FE68004CFA5A /* ContentController.swift */, 90 | D021C74E21C3FE68004CFA5A /* ContentController.storyboard */, 91 | ); 92 | path = EmbeddedNCExample; 93 | sourceTree = ""; 94 | }; 95 | D021C73821C3FDEB004CFA5A /* CardPresentationController */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | D0AB35F121C5AB920038C6C6 /* CardConfiguration.swift */, 99 | D021C73921C3FDEB004CFA5A /* UIKit-CardPresentationExtensions.swift */, 100 | D021C73A21C3FDEB004CFA5A /* CardPresentationController.swift */, 101 | D021C73B21C3FDEB004CFA5A /* CardAnimator.swift */, 102 | D021C73C21C3FDEB004CFA5A /* CardTransitionManager.swift */, 103 | ); 104 | name = CardPresentationController; 105 | path = ../CardPresentationController; 106 | sourceTree = ""; 107 | }; 108 | D021C74121C3FE0B004CFA5A /* Vendor */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | D021C74221C3FE0B004CFA5A /* Controllers */, 112 | D021C74521C3FE0B004CFA5A /* Views */, 113 | ); 114 | name = Vendor; 115 | path = ../Vendor; 116 | sourceTree = ""; 117 | }; 118 | D021C74221C3FE0B004CFA5A /* Controllers */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | D021C74321C3FE0B004CFA5A /* StoryboardLoadable.swift */, 122 | D021C74421C3FE0B004CFA5A /* Embeddable.swift */, 123 | ); 124 | path = Controllers; 125 | sourceTree = ""; 126 | }; 127 | D021C74521C3FE0B004CFA5A /* Views */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | D021C74621C3FE0B004CFA5A /* DequeableView.swift */, 131 | D021C74721C3FE0B004CFA5A /* ReusableView.swift */, 132 | D021C74821C3FE0B004CFA5A /* NibLoadable.swift */, 133 | ); 134 | path = Views; 135 | sourceTree = ""; 136 | }; 137 | /* End PBXGroup section */ 138 | 139 | /* Begin PBXNativeTarget section */ 140 | D021C72221C3FDB0004CFA5A /* EmbeddedNCExample */ = { 141 | isa = PBXNativeTarget; 142 | buildConfigurationList = D021C73521C3FDB2004CFA5A /* Build configuration list for PBXNativeTarget "EmbeddedNCExample" */; 143 | buildPhases = ( 144 | D021C71F21C3FDB0004CFA5A /* Sources */, 145 | D021C72021C3FDB0004CFA5A /* Frameworks */, 146 | D021C72121C3FDB0004CFA5A /* Resources */, 147 | ); 148 | buildRules = ( 149 | ); 150 | dependencies = ( 151 | ); 152 | name = EmbeddedNCExample; 153 | productName = EmbeddedNCExample; 154 | productReference = D021C72321C3FDB0004CFA5A /* EmbeddedNCExample.app */; 155 | productType = "com.apple.product-type.application"; 156 | }; 157 | /* End PBXNativeTarget section */ 158 | 159 | /* Begin PBXProject section */ 160 | D021C71B21C3FDB0004CFA5A /* Project object */ = { 161 | isa = PBXProject; 162 | attributes = { 163 | LastSwiftUpdateCheck = 1010; 164 | LastUpgradeCheck = 1010; 165 | ORGANIZATIONNAME = "Radiant Tap"; 166 | TargetAttributes = { 167 | D021C72221C3FDB0004CFA5A = { 168 | CreatedOnToolsVersion = 10.1; 169 | LastSwiftMigration = 1020; 170 | }; 171 | }; 172 | }; 173 | buildConfigurationList = D021C71E21C3FDB0004CFA5A /* Build configuration list for PBXProject "EmbeddedNCExample" */; 174 | compatibilityVersion = "Xcode 9.3"; 175 | developmentRegion = en; 176 | hasScannedForEncodings = 0; 177 | knownRegions = ( 178 | en, 179 | Base, 180 | ); 181 | mainGroup = D021C71A21C3FDB0004CFA5A; 182 | productRefGroup = D021C72421C3FDB0004CFA5A /* Products */; 183 | projectDirPath = ""; 184 | projectRoot = ""; 185 | targets = ( 186 | D021C72221C3FDB0004CFA5A /* EmbeddedNCExample */, 187 | ); 188 | }; 189 | /* End PBXProject section */ 190 | 191 | /* Begin PBXResourcesBuildPhase section */ 192 | D021C72121C3FDB0004CFA5A /* Resources */ = { 193 | isa = PBXResourcesBuildPhase; 194 | buildActionMask = 2147483647; 195 | files = ( 196 | D021C73121C3FDB2004CFA5A /* LaunchScreen.storyboard in Resources */, 197 | D021C72E21C3FDB2004CFA5A /* Assets.xcassets in Resources */, 198 | D021C75021C3FE68004CFA5A /* ContentController.storyboard in Resources */, 199 | D021C72C21C3FDB0004CFA5A /* Main.storyboard in Resources */, 200 | ); 201 | runOnlyForDeploymentPostprocessing = 0; 202 | }; 203 | /* End PBXResourcesBuildPhase section */ 204 | 205 | /* Begin PBXSourcesBuildPhase section */ 206 | D021C71F21C3FDB0004CFA5A /* Sources */ = { 207 | isa = PBXSourcesBuildPhase; 208 | buildActionMask = 2147483647; 209 | files = ( 210 | D021C74A21C3FE0B004CFA5A /* Embeddable.swift in Sources */, 211 | D021C75121C3FE68004CFA5A /* ContentController.swift in Sources */, 212 | D021C74C21C3FE0B004CFA5A /* ReusableView.swift in Sources */, 213 | D021C74021C3FDEB004CFA5A /* CardTransitionManager.swift in Sources */, 214 | D021C72921C3FDB0004CFA5A /* ViewController.swift in Sources */, 215 | D021C74921C3FE0B004CFA5A /* StoryboardLoadable.swift in Sources */, 216 | D021C74D21C3FE0B004CFA5A /* NibLoadable.swift in Sources */, 217 | D021C74B21C3FE0B004CFA5A /* DequeableView.swift in Sources */, 218 | D021C73D21C3FDEB004CFA5A /* UIKit-CardPresentationExtensions.swift in Sources */, 219 | D021C73F21C3FDEB004CFA5A /* CardAnimator.swift in Sources */, 220 | D0AB35F221C5AB920038C6C6 /* CardConfiguration.swift in Sources */, 221 | D021C73E21C3FDEB004CFA5A /* CardPresentationController.swift in Sources */, 222 | D021C72721C3FDB0004CFA5A /* AppDelegate.swift in Sources */, 223 | ); 224 | runOnlyForDeploymentPostprocessing = 0; 225 | }; 226 | /* End PBXSourcesBuildPhase section */ 227 | 228 | /* Begin PBXVariantGroup section */ 229 | D021C72A21C3FDB0004CFA5A /* Main.storyboard */ = { 230 | isa = PBXVariantGroup; 231 | children = ( 232 | D021C72B21C3FDB0004CFA5A /* Base */, 233 | ); 234 | name = Main.storyboard; 235 | sourceTree = ""; 236 | }; 237 | D021C72F21C3FDB2004CFA5A /* LaunchScreen.storyboard */ = { 238 | isa = PBXVariantGroup; 239 | children = ( 240 | D021C73021C3FDB2004CFA5A /* Base */, 241 | ); 242 | name = LaunchScreen.storyboard; 243 | sourceTree = ""; 244 | }; 245 | /* End PBXVariantGroup section */ 246 | 247 | /* Begin XCBuildConfiguration section */ 248 | D021C73321C3FDB2004CFA5A /* Debug */ = { 249 | isa = XCBuildConfiguration; 250 | buildSettings = { 251 | ALWAYS_SEARCH_USER_PATHS = NO; 252 | CLANG_ANALYZER_NONNULL = YES; 253 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 254 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 255 | CLANG_CXX_LIBRARY = "libc++"; 256 | CLANG_ENABLE_MODULES = YES; 257 | CLANG_ENABLE_OBJC_ARC = YES; 258 | CLANG_ENABLE_OBJC_WEAK = YES; 259 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 260 | CLANG_WARN_BOOL_CONVERSION = YES; 261 | CLANG_WARN_COMMA = YES; 262 | CLANG_WARN_CONSTANT_CONVERSION = YES; 263 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 264 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 265 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 266 | CLANG_WARN_EMPTY_BODY = YES; 267 | CLANG_WARN_ENUM_CONVERSION = YES; 268 | CLANG_WARN_INFINITE_RECURSION = YES; 269 | CLANG_WARN_INT_CONVERSION = YES; 270 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 271 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 272 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 273 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 274 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 275 | CLANG_WARN_STRICT_PROTOTYPES = YES; 276 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 277 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 278 | CLANG_WARN_UNREACHABLE_CODE = YES; 279 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 280 | CODE_SIGN_IDENTITY = "iPhone Developer"; 281 | COPY_PHASE_STRIP = NO; 282 | DEBUG_INFORMATION_FORMAT = dwarf; 283 | ENABLE_STRICT_OBJC_MSGSEND = YES; 284 | ENABLE_TESTABILITY = YES; 285 | GCC_C_LANGUAGE_STANDARD = gnu11; 286 | GCC_DYNAMIC_NO_PIC = NO; 287 | GCC_NO_COMMON_BLOCKS = YES; 288 | GCC_OPTIMIZATION_LEVEL = 0; 289 | GCC_PREPROCESSOR_DEFINITIONS = ( 290 | "DEBUG=1", 291 | "$(inherited)", 292 | ); 293 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 294 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 295 | GCC_WARN_UNDECLARED_SELECTOR = YES; 296 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 297 | GCC_WARN_UNUSED_FUNCTION = YES; 298 | GCC_WARN_UNUSED_VARIABLE = YES; 299 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 300 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 301 | MTL_FAST_MATH = YES; 302 | ONLY_ACTIVE_ARCH = YES; 303 | SDKROOT = iphoneos; 304 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 305 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 306 | SWIFT_VERSION = 5.0; 307 | }; 308 | name = Debug; 309 | }; 310 | D021C73421C3FDB2004CFA5A /* 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 | CODE_SIGN_IDENTITY = "iPhone Developer"; 343 | COPY_PHASE_STRIP = NO; 344 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 345 | ENABLE_NS_ASSERTIONS = NO; 346 | ENABLE_STRICT_OBJC_MSGSEND = YES; 347 | GCC_C_LANGUAGE_STANDARD = gnu11; 348 | GCC_NO_COMMON_BLOCKS = YES; 349 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 350 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 351 | GCC_WARN_UNDECLARED_SELECTOR = YES; 352 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 353 | GCC_WARN_UNUSED_FUNCTION = YES; 354 | GCC_WARN_UNUSED_VARIABLE = YES; 355 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 356 | MTL_ENABLE_DEBUG_INFO = NO; 357 | MTL_FAST_MATH = YES; 358 | SDKROOT = iphoneos; 359 | SWIFT_COMPILATION_MODE = wholemodule; 360 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 361 | SWIFT_VERSION = 5.0; 362 | VALIDATE_PRODUCT = YES; 363 | }; 364 | name = Release; 365 | }; 366 | D021C73621C3FDB2004CFA5A /* Debug */ = { 367 | isa = XCBuildConfiguration; 368 | buildSettings = { 369 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 370 | CODE_SIGN_STYLE = Automatic; 371 | INFOPLIST_FILE = EmbeddedNCExample/Info.plist; 372 | LD_RUNPATH_SEARCH_PATHS = ( 373 | "$(inherited)", 374 | "@executable_path/Frameworks", 375 | ); 376 | PRODUCT_BUNDLE_IDENTIFIER = com.radianttap.EmbeddedNCExample; 377 | PRODUCT_NAME = "$(TARGET_NAME)"; 378 | TARGETED_DEVICE_FAMILY = "1,2"; 379 | }; 380 | name = Debug; 381 | }; 382 | D021C73721C3FDB2004CFA5A /* Release */ = { 383 | isa = XCBuildConfiguration; 384 | buildSettings = { 385 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 386 | CODE_SIGN_STYLE = Automatic; 387 | INFOPLIST_FILE = EmbeddedNCExample/Info.plist; 388 | LD_RUNPATH_SEARCH_PATHS = ( 389 | "$(inherited)", 390 | "@executable_path/Frameworks", 391 | ); 392 | PRODUCT_BUNDLE_IDENTIFIER = com.radianttap.EmbeddedNCExample; 393 | PRODUCT_NAME = "$(TARGET_NAME)"; 394 | TARGETED_DEVICE_FAMILY = "1,2"; 395 | }; 396 | name = Release; 397 | }; 398 | /* End XCBuildConfiguration section */ 399 | 400 | /* Begin XCConfigurationList section */ 401 | D021C71E21C3FDB0004CFA5A /* Build configuration list for PBXProject "EmbeddedNCExample" */ = { 402 | isa = XCConfigurationList; 403 | buildConfigurations = ( 404 | D021C73321C3FDB2004CFA5A /* Debug */, 405 | D021C73421C3FDB2004CFA5A /* Release */, 406 | ); 407 | defaultConfigurationIsVisible = 0; 408 | defaultConfigurationName = Release; 409 | }; 410 | D021C73521C3FDB2004CFA5A /* Build configuration list for PBXNativeTarget "EmbeddedNCExample" */ = { 411 | isa = XCConfigurationList; 412 | buildConfigurations = ( 413 | D021C73621C3FDB2004CFA5A /* Debug */, 414 | D021C73721C3FDB2004CFA5A /* Release */, 415 | ); 416 | defaultConfigurationIsVisible = 0; 417 | defaultConfigurationName = Release; 418 | }; 419 | /* End XCConfigurationList section */ 420 | }; 421 | rootObject = D021C71B21C3FDB0004CFA5A /* Project object */; 422 | } 423 | -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // EmbeddedNCExample 4 | // 5 | // Created by Aleksandar Vacić on 12/14/18. 6 | // Copyright © 2018 Radiant Tap. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | return true 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "ItunesArtwork@2x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/WonderWomanPoster.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "WonderWomanPoster.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/WonderWomanPoster.imageset/WonderWomanPoster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/EmbeddedNCExample/EmbeddedNCExample/Assets.xcassets/WonderWomanPoster.imageset/WonderWomanPoster.jpg -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/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 | -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/Base.lproj/Main.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 | -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/ContentController.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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 55 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/ContentController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlainPopupController.swift 3 | // EmbeddedNCExample 4 | // 5 | // Created by Aleksandar Vacić on 10/28/18. 6 | // Copyright © 2018 Aleksandar Vacić. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class ContentController: UIViewController, StoryboardLoadable { 12 | // Configuration 13 | 14 | enum Context { 15 | case popup 16 | case embed 17 | } 18 | var context: Context = .popup { 19 | didSet { 20 | if !isViewLoaded { return } 21 | processContext() 22 | } 23 | } 24 | 25 | 26 | // UI 27 | 28 | @IBOutlet private weak var messageLabel: UILabel! 29 | @IBOutlet private weak var popupButton: UIButton! 30 | 31 | override var preferredStatusBarStyle: UIStatusBarStyle { 32 | return .lightContent 33 | } 34 | 35 | 36 | // Data source 37 | 38 | private var message: String? 39 | 40 | 41 | // View lifecycle 42 | 43 | override func viewDidLoad() { 44 | super.viewDidLoad() 45 | 46 | messageLabel.text = nil 47 | processContext() 48 | populateMessage() 49 | } 50 | 51 | func message(_ s: String) { 52 | self.message = s 53 | if !isViewLoaded { return } 54 | populateMessage() 55 | } 56 | } 57 | 58 | private extension ContentController { 59 | func processContext() { 60 | switch context { 61 | case .embed: 62 | popupButton.isHidden = true 63 | case .popup: 64 | popupButton.isHidden = false 65 | } 66 | } 67 | 68 | func populateMessage() { 69 | messageLabel.text = message 70 | } 71 | 72 | @IBAction func popup(_ sender: UIButton) { 73 | let vc = ContentController.instantiate() 74 | 75 | presentCard(vc, 76 | animated: true) 77 | } 78 | } 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | extension ContentController { 139 | override func viewWillAppear(_ animated: Bool) { 140 | super.viewWillAppear(animated) 141 | 142 | if countOfPresentingParents == 4 { 143 | popupButton.setTitle("Ehm, it's enough, don't you think?", for: .normal) 144 | popupButton.isUserInteractionEnabled = false 145 | popupButton.alpha = 0.6 146 | } 147 | } 148 | } 149 | 150 | fileprivate extension UIViewController { 151 | var countOfPresentingParents: Int { 152 | var c = 0 153 | if let vc = self.presentingViewController { 154 | c = 1 + vc.countOfPresentingParents 155 | } 156 | return c 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /EmbeddedNCExample/EmbeddedNCExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // EmbeddedNCExample 4 | // 5 | // Created by Aleksandar Vacić on 12/14/18. 6 | // Copyright © 2018 Radiant Tap. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class ViewController: UIViewController { 12 | 13 | private var controller: UIViewController? 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | let vc = ContentController.instantiate() 19 | 20 | let nc = UINavigationController(rootViewController: vc) 21 | nc.navigationBar.barTintColor = .red 22 | // nc.setNavigationBarHidden(true, animated: false) 23 | embed(controller: nc, into: view) 24 | controller = nc 25 | 26 | vc.message("The issue with this kind of setup is the magic that UIKit applies to safeAreaInsets and extending the navigationBar under status bar. So when you 'extract' views to make them part of transition, all that magic is gone and (sub)views reflow.\n\nNo idea is this fixable on general terms.") 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CardPresentationExample 4 | // 5 | // Created by Aleksandar Vacić on 9/15/18. 6 | // Copyright © 2018 Aleksandar Vacić. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CardPresentationController 11 | 12 | @UIApplicationMain 13 | final class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | applyTheme() 19 | return true 20 | } 21 | 22 | } 23 | 24 | private extension AppDelegate { 25 | func applyTheme() { 26 | // Example of globally changing every card in the app 27 | // 28 | // (uncomment to see it in action) 29 | // CardConfiguration.shared = CardConfiguration(verticalSpacing: 8, 30 | // horizontalInset: 8, 31 | // cornerRadius: 0, 32 | // backFadeAlpha: 0.5) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "ItunesArtwork@2x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/Example/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/WonderWomanPoster.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "WonderWomanPoster.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/WonderWomanPoster.imageset/WonderWomanPoster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/Example/Assets.xcassets/WonderWomanPoster.imageset/WonderWomanPoster.jpg -------------------------------------------------------------------------------- /Example/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 | -------------------------------------------------------------------------------- /Example/Base.lproj/Main.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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 81 | 98 | 115 | 132 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /Example/ContentController.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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 55 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Example/ContentController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlainPopupController.swift 3 | // CardPresentationExample 4 | // 5 | // Created by Aleksandar Vacić on 10/28/18. 6 | // Copyright © 2018 Aleksandar Vacić. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CardPresentationController 11 | 12 | final class ContentController: UIViewController, StoryboardLoadable { 13 | // Configuration 14 | 15 | enum Context { 16 | case popup 17 | case embed 18 | } 19 | var context: Context = .popup { 20 | didSet { 21 | if !isViewLoaded { return } 22 | processContext() 23 | } 24 | } 25 | 26 | 27 | // UI 28 | 29 | @IBOutlet private weak var messageLabel: UILabel! 30 | @IBOutlet private weak var popupButton: UIButton! 31 | 32 | override var preferredStatusBarStyle: UIStatusBarStyle { 33 | return .lightContent 34 | } 35 | 36 | 37 | // Data source 38 | 39 | private var message: String? 40 | 41 | 42 | // View lifecycle 43 | 44 | override func viewDidLoad() { 45 | super.viewDidLoad() 46 | 47 | messageLabel.text = nil 48 | processContext() 49 | populateMessage() 50 | } 51 | 52 | func message(_ s: String) { 53 | self.message = s 54 | if !isViewLoaded { return } 55 | populateMessage() 56 | } 57 | } 58 | 59 | private extension ContentController { 60 | func processContext() { 61 | switch context { 62 | case .embed: 63 | popupButton.isHidden = true 64 | case .popup: 65 | popupButton.isHidden = false 66 | } 67 | } 68 | 69 | func populateMessage() { 70 | messageLabel.text = message 71 | } 72 | 73 | @IBAction func popup(_ sender: UIButton) { 74 | let vc = ContentController.instantiate() 75 | 76 | presentCard(vc, 77 | animated: true) 78 | } 79 | } 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | extension ContentController { 140 | override func viewWillAppear(_ animated: Bool) { 141 | super.viewWillAppear(animated) 142 | 143 | if countOfPresentingParents == 4 { 144 | popupButton.setTitle("Ehm, it's enough, don't you think?", for: .normal) 145 | popupButton.isUserInteractionEnabled = false 146 | popupButton.alpha = 0.6 147 | } 148 | } 149 | } 150 | 151 | fileprivate extension UIViewController { 152 | var countOfPresentingParents: Int { 153 | var c = 0 154 | if let vc = self.presentingViewController { 155 | c = 1 + vc.countOfPresentingParents 156 | } 157 | return c 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Example/CustomContainerController.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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Example/CustomContainerController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomContainerController.swift 3 | // CardModal 4 | // 5 | // Created by Aleksandar Vacić on 3/3/19. 6 | // Copyright © 2019 Aleksandar Vacić. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class CustomContainerController: UIViewController, StoryboardLoadable { 12 | // UI 13 | 14 | @IBOutlet private var containingView: UIView! 15 | 16 | 17 | // Public 18 | 19 | private(set) var embeddedController: UIViewController? 20 | 21 | private(set) var edgeInsets: UIEdgeInsets = UIEdgeInsets(top: 44, left: 16, bottom: 16, right: 16) 22 | 23 | func display(vc: UIViewController) { 24 | embeddedController = vc 25 | if !isViewLoaded { return } 26 | embedIfNeeded() 27 | } 28 | 29 | func clear(vc: UIViewController?) { 30 | unembed(controller: vc) 31 | embeddedController = nil 32 | } 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | // UIKit 41 | 42 | override func viewDidLoad() { 43 | super.viewDidLoad() 44 | 45 | embedIfNeeded() 46 | } 47 | 48 | private func embedIfNeeded() { 49 | guard let vc = embeddedController else { return } 50 | embed(controller: vc, into: containingView) 51 | } 52 | 53 | override var preferredStatusBarStyle: UIStatusBarStyle { 54 | return .lightContent 55 | } 56 | 57 | override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { 58 | super.dismiss(animated: flag) { 59 | [weak self] in 60 | 61 | self?.clear(vc: self?.embeddedController) 62 | 63 | completion?() 64 | } 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /Example/GridCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridCell.swift 3 | // CardPresentationExample 4 | // 5 | // Created by Aleksandar Vacić on 12/16/18. 6 | // Copyright © 2018 Aleksandar Vacić. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class GridCell: UICollectionViewCell, NibReusableView { 12 | @IBOutlet private weak var gradient: GradientView! 13 | 14 | private var startColor: UIColor = .darkGray 15 | private var endColor: UIColor = .orange 16 | } 17 | 18 | extension GridCell { 19 | override func awakeFromNib() { 20 | super.awakeFromNib() 21 | cleanup() 22 | 23 | applyTheme() 24 | } 25 | 26 | override func prepareForReuse() { 27 | super.prepareForReuse() 28 | cleanup() 29 | } 30 | 31 | func populate(with color: ColorPair) { 32 | startColor = color.start 33 | endColor = color.end 34 | 35 | applyTheme() 36 | } 37 | } 38 | 39 | private extension GridCell { 40 | func cleanup() { 41 | } 42 | 43 | func applyTheme() { 44 | gradient.direction = .vertical 45 | gradient.clipsToBounds = true 46 | gradient.layer.cornerRadius = 6 47 | 48 | gradient.colors = [ 49 | startColor, 50 | endColor 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/GridCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Example/GridController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridController.swift 3 | // CardPresentationExample 4 | // 5 | // Created by Aleksandar Vacić on 12/16/18. 6 | // Copyright © 2018 Aleksandar Vacić. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | typealias ColorPair = (start: UIColor, end: UIColor) 12 | 13 | final class GridController: UICollectionViewController { 14 | init() { 15 | let layout = GridLayout() 16 | layout.minimumLineSpacing = 16 17 | layout.minimumInteritemSpacing = 16 18 | layout.sectionInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) 19 | 20 | super.init(collectionViewLayout: layout) 21 | } 22 | 23 | required init?(coder aDecoder: NSCoder) { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | 27 | private var gradients: [ColorPair] = [] 28 | } 29 | 30 | extension GridController { 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | view.backgroundColor = .darkGray 34 | collectionView.backgroundColor = view.backgroundColor 35 | 36 | collectionView.register(GridCell.self) 37 | generateGradients() 38 | } 39 | } 40 | 41 | private extension GridController { 42 | func generateGradients() { 43 | for _ in 0 ..< 50 { 44 | let cp = ColorPair(.random, .random) 45 | gradients.append(cp) 46 | } 47 | } 48 | } 49 | 50 | extension GridController { 51 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 52 | return gradients.count 53 | } 54 | 55 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 56 | let cell: GridCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) 57 | let colorPair = gradients[indexPath.item] 58 | cell.populate(with: colorPair) 59 | return cell 60 | } 61 | } 62 | 63 | private extension UIColor { 64 | static var random: UIColor { 65 | let random = { CGFloat(arc4random_uniform(255)) / 255.0 } 66 | return UIColor(red: random(), green: random(), blue: random(), alpha: 1) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Example/GridLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridLayout.swift 3 | // CardPresentationExample 4 | // 5 | // Created by Aleksandar Vacić on 12/16/18. 6 | // Copyright © 2018 Aleksandar Vacić. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class GridLayout: BaseGridLayout { 12 | var numberOfColumns: Int? 13 | 14 | override func prepare() { 15 | defer { 16 | super.prepare() 17 | } 18 | guard var availableWidth = collectionView?.bounds.width else { return } 19 | 20 | let aspectRatio = itemSize.width / itemSize.height 21 | let columns: CGFloat 22 | if let numberOfColumns = numberOfColumns { 23 | columns = CGFloat(numberOfColumns) 24 | } else { 25 | // customize for CV bounds.width 26 | columns = floor(availableWidth / itemSize.width) 27 | } 28 | 29 | availableWidth -= (columns - 1) * minimumInteritemSpacing 30 | availableWidth -= (sectionInset.left + sectionInset.right) 31 | 32 | itemSize.width = availableWidth / columns 33 | itemSize.height = itemSize.width * 1/aspectRatio 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Card Modal 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.8 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Example/PopupNavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PopupNavigationController.swift 3 | // CardPresentationExample 4 | // 5 | // Created by Aleksandar Vacić on 12/9/18. 6 | // Copyright © 2018 Aleksandar Vacić. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class PopupNavigationController: UINavigationController { 12 | override var preferredStatusBarStyle: UIStatusBarStyle { 13 | return .lightContent 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /Example/SecondController.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 29 | 37 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /Example/SecondController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecondController.swift 3 | // CardPresentationExample 4 | // 5 | // Created by Aleksandar Vacić on 12/14/18. 6 | // Copyright © 2018 Aleksandar Vacić. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CardPresentationController 11 | 12 | final class SecondController: UIViewController, StoryboardLoadable { 13 | 14 | override func viewWillAppear(_ animated: Bool) { 15 | super.viewWillAppear(animated) 16 | 17 | navigationController?.setNavigationBarHidden(true, animated: animated) 18 | } 19 | } 20 | 21 | private extension SecondController { 22 | /// Return to previous VC in the NC stack 23 | /// 24 | /// - Parameter sender: button which initiated this action 25 | @IBAction func goBack(_ sender: UIButton) { 26 | navigationController?.setNavigationBarHidden(false, animated: true) 27 | navigationController?.popViewController(animated: true) 28 | } 29 | 30 | /// Display popup as inset card 31 | /// 32 | /// - Parameter sender: button which initiated this action 33 | @IBAction func popupCard(_ sender: UIButton) { 34 | let vc = ContentController.instantiate() 35 | 36 | presentCard(vc, 37 | animated: true) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // CardPresentationExample 4 | // 5 | // Created by Aleksandar Vacić on 9/15/18. 6 | // Copyright © 2018 Aleksandar Vacić. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CardPresentationController 11 | 12 | final class ViewController: UIViewController { 13 | @IBOutlet private var container: UIView! 14 | 15 | @IBOutlet private var toggle: UISwitch! 16 | 17 | // Embedded 18 | 19 | private var controller: ContentController? 20 | 21 | // View lifecycle 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | if #available(iOS 13, *) { 27 | toggle.isOn = CardPresentationController.useSystemPresentationOniOS13 28 | } else { 29 | toggle.superview?.isHidden = true 30 | } 31 | 32 | if let gradient = view as? GradientView { 33 | gradient.direction = .vertical 34 | gradient.colors = [.gray, .darkGray] 35 | } 36 | 37 | let vc = ContentController.instantiate() 38 | vc.context = .embed 39 | embed(controller: vc, into: container) 40 | controller = vc 41 | } 42 | } 43 | 44 | private extension ViewController { 45 | /// Display regular, familiar full screen popup 46 | /// 47 | /// - Parameter sender: button which initiated this action 48 | @IBAction func popupDefault(_ sender: UIButton) { 49 | let vc = ContentController.instantiate() 50 | addDismissBarButton(to: vc) 51 | 52 | // wrap inside NC 53 | let nc = UINavigationController(rootViewController: vc) 54 | 55 | present(nc, 56 | animated: true) 57 | } 58 | 59 | /// Display popup as inset card 60 | /// 61 | /// - Parameter sender: button which initiated this action 62 | @IBAction func popupNavBarCard(_ sender: UIButton) { 63 | let vc = ContentController.instantiate() 64 | addDismissBarButton(to: vc) 65 | 66 | // wrap inside custom NC, so we can enforce statusBarStyle 67 | let nc = PopupNavigationController(rootViewController: vc) 68 | 69 | presentCard(nc, 70 | animated: true) 71 | } 72 | 73 | /// Display custom container with embedded content, as popup card. 74 | /// 75 | /// - Parameter sender: button which initiated this action 76 | @IBAction func popupCustomContainerCard(_ sender: UIButton) { 77 | let vc = ContentController.instantiate() 78 | // vc.context = .embed 79 | 80 | let cc = CustomContainerController.initial() 81 | cc.display(vc: vc) 82 | 83 | // center the dismiss handle inside the reserved area at the top of the container 84 | let config = CardConfiguration(dismissAreaHeight: cc.edgeInsets.top) 85 | 86 | presentCard(cc, 87 | configuration: config, 88 | animated: true) 89 | } 90 | 91 | /// Expand popup from the arbitrary rect on the screen 92 | /// (and also collapse down to the same area) 93 | /// 94 | /// - Parameter sender: button which initiated this action 95 | @IBAction func expandCard(_ sender: UIButton) { 96 | let vc = ContentController.instantiate() 97 | vc.context = .embed 98 | 99 | let f = container.convert(sender.bounds, to: view.window!) 100 | let config = CardConfiguration(initialTransitionFrame: f) 101 | 102 | presentCard(vc, 103 | configuration: config, 104 | animated: true) 105 | } 106 | 107 | @IBAction func pushNext(_ sender: UIBarButtonItem) { 108 | let vc = SecondController.instantiate() 109 | show(vc, sender: self) 110 | } 111 | 112 | /// Display popup as inset card 113 | /// 114 | /// - Parameter sender: button which initiated this action 115 | @IBAction func popupGrid(_ sender: UIButton) { 116 | let vc = GridController() 117 | 118 | presentCard(vc, 119 | animated: true) 120 | } 121 | 122 | /// Display popup as inset card, with initial vertical inset of 132pt 123 | /// 124 | /// - Parameter sender: button which initiated this action 125 | @IBAction func popupLargeInsetCard(_ sender: UIButton) { 126 | let vc = ContentController.instantiate() 127 | 128 | let config = CardConfiguration(verticalInset: 132) 129 | 130 | presentCard(vc, 131 | configuration: config, 132 | animated: true) 133 | } 134 | } 135 | 136 | 137 | fileprivate extension ViewController { 138 | /// Dismisses whatever popup is currently shown 139 | /// 140 | /// - Parameter sender: An UI object that initiated dismissal 141 | @IBAction func dismissPopup(_ sender: Any) { 142 | dismiss(animated: true) 143 | } 144 | 145 | func addDismissBarButton(to vc: UIViewController) { 146 | // and add Done button to dismiss the popup 147 | let bbi = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(ViewController.dismissPopup)) 148 | var buttonItems = vc.navigationItem.leftBarButtonItems ?? [] 149 | buttonItems.append(bbi) 150 | vc.navigationItem.leftBarButtonItems = buttonItems 151 | } 152 | 153 | @IBAction func toggleSystemCardUsage(_ sender: UISwitch) { 154 | CardPresentationController.useSystemPresentationOniOS13 = sender.isOn 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Framework/CardPresentationController.h: -------------------------------------------------------------------------------- 1 | // 2 | // CardPresentationController.h 3 | // CardPresentationController 4 | // 5 | // Created by Aleksandar Vacić on 1/28/19. 6 | // Copyright © 2019 Aleksandar Vacić. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for CardPresentationController. 12 | FOUNDATION_EXPORT double CardPresentationControllerVersionNumber; 13 | 14 | //! Project version string for CardPresentationController. 15 | FOUNDATION_EXPORT const unsigned char CardPresentationControllerVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Framework/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 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2018 Aleksandar Vacić, Radiant Tap 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This component is deprecated. 2 | 3 | - We have native card support since iOS 13. 4 | - In iOS 15 Apple is adding custom presentation sheets for half-modals. 5 | 6 | This component has served its purpose, **please move on to native API**. 7 | 8 | --- 9 | 10 | 11 | [![](https://img.shields.io/github/tag/radianttap/CardPresentationController.svg?label=current)](https://github.com/radianttap/CardPresentationController/releases) 12 | ![platforms: iOS](https://img.shields.io/badge/platform-iOS-blue.svg) 13 | [![](https://img.shields.io/github/license/radianttap/CardPresentationController.svg)](https://github.com/radianttap/CardPresentationController/blob/master/LICENSE) 14 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-AD4709.svg?style=flat)](https://github.com/Carthage/Carthage) 15 | [![CocoaPods compatible](https://img.shields.io/badge/CocoaPods-compatible-fb0006.svg)](https://cocoapods.org) 16 | ![](https://img.shields.io/badge/swift-5-223344.svg?logo=swift&labelColor=FA7343&logoColor=white) 17 | 18 | # CardPresentationController 19 | 20 | Custom [UIPresentationController](https://developer.apple.com/documentation/uikit/uipresentationcontroller) which mimics the behavior of Apple Music UI. Should work just fine from iOS 10 and beyond. 21 | 22 | [DEMO video on iPhone Xs simulator](CardPresentationController.mp4) 23 | 24 | ### Modal presentation in iOS 13 25 | 26 | iOS 13 changed the behavior of the ordinary `present(vc, ...) calls` - all modals now look like cards. *Thus you don’t need this library on iOS 13.* I always recommend to use system stuff as much as possible thus this library will, by default, fallback to system look & behavior if you are on iOS 13. 27 | 28 | To toggle this off and still use this library to present modal cards, set this at some point before presenting your first `UIViewController`: 29 | 30 | ``` 31 | CardPresentationController.useSystemPresentationOniOS13 = false 32 | ``` 33 | 34 | Keep in mind that visual display of multiple cards in this library is different from what iOS 13 does. (I don’t intend to change this, it’s not really worth it.) 35 | 36 | ## Installation 37 | 38 | ### Manually 39 | 40 | Add the folder `CardPresentationController` into your project. It's only five files. 41 | 42 | If you prefer to use dependency managers, see below. 43 | Releases are tagged with [Semantic Versioning](https://semver.org) in mind. 44 | 45 | ### CocoaPods 46 | 47 | [CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate Coordinator into your Xcode project using CocoaPods, specify it in your `Podfile`: 48 | 49 | ```ruby 50 | pod 'CardPresentationController', :git => 'https://github.com/radianttap/CardPresentationController.git' 51 | ``` 52 | 53 | ### Setting up with Carthage 54 | 55 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that automates the process of adding frameworks to your Cocoa application. 56 | 57 | You can install Carthage with [Homebrew](http://brew.sh/) using the following command: 58 | 59 | ```bash 60 | $ brew update 61 | $ brew install carthage 62 | ``` 63 | 64 | To integrate CardPresentationController into your Xcode project using Carthage, specify it in your `Cartfile`: 65 | 66 | ```ogdl 67 | github "radianttap/CardPresentationController" 68 | ``` 69 | 70 | 71 | 72 | ## Usage 73 | 74 | From anywhere you want to present some `UIViewController`, call 75 | 76 | ```swift 77 | let vc = ... 78 | presentCard(vc, animated: true) 79 | ``` 80 | 81 | You dismiss it as any other modal: 82 | 83 | ```swift 84 | dismiss(animated: true) 85 | ``` 86 | 87 | This will present `vc` modally, flying-in from the bottom edge. Existing view will be kept shown as dimmed background card, on black background. 88 | 89 | You can *present card from another card*; library will stack the cards nicely. Do use common sense as popups over popups don’t make pleasant user experience. 90 | 91 | ### Advanced behavior 92 | 93 | View of the `presenting` Controller will be (by default) 20% transparent to blend into the background a bit, thus looking dimmed. 94 | 95 | That back "card" is also inset a bit from the edges. 96 | 97 | ![](resources/presentedNC-top.png) 98 | 99 | If the _presented_ VC is `UINavigationController` instance, nothing special happens. It’s assumed that you will add `UIBarButtonItem` which will facilitate dismissal. 100 | 101 | If it is not, then `CardPresentationController` will automatically add a button at the middle of the shown card. Tapping on that will dismiss the card. 102 | 103 | ![](resources/presentedVC-top.png) 104 | 105 | As you present card over card, back cards will be ever more transparent and horizontally inset. In most cases, this should look rather nice. 106 | 107 | Library also supports interactive dismissal — simply pan from top to bottom and UI will obey you. You can pan up or down and the direction and position where you let go will determine will the card finish dismissing or return to presented state. 108 | 109 | ### Status bar style 110 | 111 | CardPresentationController tries its best to enforce `.lightContent` status bar style. You can help it, by adding this into your UIVC subclass: 112 | 113 | ```swift 114 | override var preferredStatusBarStyle: UIStatusBarStyle { 115 | return .lightContent 116 | } 117 | ``` 118 | 119 | If you are presenting UINC, then my advice is to subclass it and override `preferredStatusBarStyle` property in the same way. 120 | 121 | ## Requirements 122 | 123 | *Requires iOS 10*, since it uses [UIViewPropertyAnimator](https://developer.apple.com/documentation/uikit/uiviewpropertyanimator), [UISpringTimingParameters](https://developer.apple.com/documentation/uikit/uispringtimingparameters) and a bunch of other modern UIKit animation APIs. 124 | 125 | On iOS 11 it uses [maskedCorners](https://developer.apple.com/documentation/quartzcore/calayer/2877488-maskedcorners) property to round just the top corners. On iOS 10.x it will fallback to rounding all corners. 126 | 127 | ## How it works 128 | 129 | The main object here is `CardTransitionManager`, which acts as `UIViewControllerTransitioningDelegate`. It is internally instantiated and assigned as property on UIVC which called `presentCard()` – that's _sourceController_ in the UIPresentationController parlance. 130 | 131 | This instance of CTM is automatically removed on dismissal. 132 | 133 | CTM creates and manages the other two required objects: 134 | 135 | * `CardPresentationController`: manages additional views (like dismiss handle at the top of the card) and other aspects of the custom presentation 136 | * `CardAnimator`: which performs the animated transition 137 | 138 | In case you missed it — *you don’t deal with any of that*. It’s all implementation detail, hidden inside these 3 classes. You never instantiate them directly. 139 | 140 | The only object you can put to use, if you want to, is… 141 | 142 | ### CardConfiguration 143 | 144 | When calling `presentCard`, you can supply optional `CardConfiguration` instance. This is simple struct containing the following parameters: 145 | 146 | ```swift 147 | /// Vertical inset from the top or already shown card 148 | var verticalSpacing: CGFloat = 16 149 | 150 | /// Leading and trailing inset for the existing (presenting) view 151 | /// when it's being pushed further back 152 | var horizontalInset: CGFloat = 16 153 | 154 | /// Height of the "empty" area at the top of the card 155 | /// where dismiss handle glyph will be centered. 156 | public var dismissAreaHeight: CGFloat = 16 157 | 158 | /// Cards have rounded corners, right? 159 | var cornerRadius: CGFloat = 12 160 | 161 | /// The starting frame for the presented card. 162 | var initialTransitionFrame: CGRect? 163 | 164 | /// How much to fade the back card. 165 | var backFadeAlpha: CGFloat = 0.8 166 | 167 | /// Set to false to disable interactive dismissal 168 | var allowInteractiveDismissal = true 169 | ``` 170 | 171 | There’s a very handy `init` for it where you can supply any combination of these parameters. 172 | 173 | If you don't supply config, then `CardConfiguration.shared` will be used, consisting of the default values shown above. 174 | You can override this property early in app's lifecycle so adjust default look of the cards for the entire app (see AppDelegate.swift for an example). 175 | 176 | ### Advanced example 177 | 178 | Thus if you want to control where the card originates — say if you want to mimic Apple Music's now-playing card — you can: 179 | 180 | ```swift 181 | let vc = ContentController.instantiate() 182 | 183 | let f = container.convert(sender.bounds, to: view.window!) 184 | let config = CardConfiguration(initialTransitionFrame: f) 185 | 186 | presentCard(vc, configuration: config, animated: true) 187 | ``` 188 | 189 | The important bit here is setting `initialTransitionFrame` property to the frame *in the UIWindow coordinating space*, since transition happens in it. 190 | 191 | ### Caveats 192 | 193 | `CardAnimator` animates layout of its own subviews – `from` and `to` views included in `transitionContext`. Behavior and layout of the internal subviews of both _presented_ and _presenting_/_source_ views is up to you *but* CardAnimator will try its best to animate them along. 194 | 195 | Depending on the complexity of your UI, in may be impossible to make the transition perfect. Usually in cases where UIKit applies its own private API magic related to status / navigation bars. 196 | See `EmbeddedNCExample` where I have `UINavigationController` embedded inside ordinary `UIViewController`. This is very unusual UIVC stack which I would love to solve since I have project using just that. 197 | 198 | ## LICENSE 199 | 200 | [MIT](LICENSE), as usual for all my stuff. 201 | 202 | 203 | ## Give back 204 | 205 | If you found this code useful, please consider [buying me a coffee](https://www.buymeacoffee.com/radianttap) or two. ☕️😋 206 | -------------------------------------------------------------------------------- /Vendor/BaseGridLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridLayout.swift 3 | // Radiant Tap Essentials 4 | // 5 | // Copyright © 2015 Radiant Tap 6 | // MIT License · http://choosealicense.com/licenses/mit/ 7 | // 8 | 9 | import UIKit 10 | 11 | class BaseGridLayout: UICollectionViewFlowLayout { 12 | 13 | override func awakeFromNib() { 14 | super.awakeFromNib() 15 | commonInit() 16 | } 17 | 18 | override init() { 19 | super.init() 20 | commonInit() 21 | } 22 | 23 | required init?(coder aDecoder: NSCoder) { 24 | super.init(coder: aDecoder) 25 | commonInit() 26 | } 27 | 28 | func commonInit() { 29 | scrollDirection = .vertical 30 | itemSize = CGSize(width: 120, height: 120) 31 | headerReferenceSize = .zero 32 | footerReferenceSize = .zero 33 | minimumLineSpacing = 0 34 | minimumInteritemSpacing = 0 35 | sectionInset = .zero 36 | } 37 | 38 | override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 39 | switch scrollDirection { 40 | case .horizontal: 41 | return newBounds.height != collectionView?.bounds.height 42 | case .vertical: 43 | return newBounds.width != collectionView?.bounds.width 44 | @unknown default: 45 | return false 46 | } 47 | } 48 | 49 | override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext { 50 | let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext 51 | switch scrollDirection { 52 | case .horizontal: 53 | context.invalidateFlowLayoutDelegateMetrics = newBounds.height != collectionView?.bounds.height 54 | case .vertical: 55 | context.invalidateFlowLayoutDelegateMetrics = newBounds.width != collectionView?.bounds.width 56 | @unknown default: 57 | break 58 | } 59 | return context 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Vendor/Controllers/Embeddable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Embeddable.swift 3 | // Radiant Tap Essentials 4 | // https://github.com/radianttap/swift-essentials 5 | // 6 | // Copyright © 2016 Radiant Tap 7 | // MIT License · http://choosealicense.com/licenses/mit/ 8 | 9 | import UIKit 10 | 11 | extension UIViewController { 12 | /// (view, parentView) -> Void 13 | public typealias LayoutBlock = (UIView, UIView) -> Void 14 | 15 | /// Embeds the `view` of the given UIViewController into supplied `parentView` (or into `self.view`, if `nil`). 16 | /// 17 | /// Default value of `LayoutBlock` aligns the embedded `view` with the edges of the `parentView`, using priority=999 for the bottom and trailing constraints). 18 | /// This helps to avoid auto-layout issues when embedding into zero-width or height container views. 19 | public func embed(controller vc: T, into parentView: UIView?, layout: LayoutBlock = { 20 | v, pv in 21 | 22 | let constraints: [NSLayoutConstraint] = [ 23 | v.topAnchor.constraint(equalTo: pv.topAnchor), 24 | v.leadingAnchor.constraint(equalTo: pv.leadingAnchor), 25 | { 26 | let lc = v.bottomAnchor.constraint(equalTo: pv.bottomAnchor) 27 | lc.priority = UILayoutPriority(rawValue: 999) 28 | return lc 29 | }(), 30 | { 31 | let lc = v.trailingAnchor.constraint(equalTo: pv.trailingAnchor) 32 | lc.priority = UILayoutPriority(rawValue: 999) 33 | return lc 34 | }() 35 | ] 36 | constraints.forEach { $0.isActive = true } 37 | }) 38 | where T: UIViewController 39 | { 40 | let container = parentView ?? self.view! 41 | 42 | addChild(vc) 43 | container.addSubview(vc.view) 44 | vc.view.translatesAutoresizingMaskIntoConstraints = false 45 | layout(vc.view, container) 46 | vc.didMove(toParent: self) 47 | 48 | // Note: after this, save the controller reference 49 | // somewhere in calling scope 50 | } 51 | 52 | public func unembed(controller: UIViewController?) { 53 | guard let controller = controller else { return } 54 | 55 | controller.willMove(toParent: nil) 56 | if controller.isViewLoaded { 57 | controller.view.removeFromSuperview() 58 | } 59 | controller.removeFromParent() 60 | 61 | // Note: don't forget to nullify your own controller instance 62 | // in order to clear it out from memory 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /Vendor/Controllers/StoryboardLoadable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoryboardLoadable.swift 3 | // Radiant Tap Essentials 4 | // https://github.com/radianttap/swift-essentials 5 | // 6 | // Copyright © 2016 Radiant Tap 7 | // MIT License · http://choosealicense.com/licenses/mit/ 8 | // 9 | 10 | import UIKit 11 | 12 | 13 | public protocol StoryboardLoadable { 14 | static var storyboardName: String { get } 15 | static var storyboardIdentifier: String { get } 16 | } 17 | 18 | 19 | extension StoryboardLoadable where Self: UIViewController { 20 | 21 | public static var storyboardName: String { 22 | return String(describing: self) 23 | } 24 | 25 | public static var storyboardIdentifier: String { 26 | return String(describing: self) 27 | } 28 | 29 | public static func instantiate(fromStoryboardNamed name: String? = nil) -> Self { 30 | let sb = name ?? self.storyboardName 31 | let storyboard = UIStoryboard(name: sb, bundle: nil) 32 | return instantiate(fromStoryboard: storyboard) 33 | } 34 | 35 | public static func instantiate(fromStoryboard storyboard: UIStoryboard) -> Self { 36 | let identifier = self.storyboardIdentifier 37 | guard let vc = storyboard.instantiateViewController(withIdentifier: identifier) as? Self else { 38 | fatalError("Failed to instantiate view controller with identifier=\(identifier) from storyboard \( storyboard )") 39 | } 40 | return vc 41 | 42 | } 43 | 44 | public static func initial(fromStoryboardNamed name: String? = nil) -> Self { 45 | let sb = name ?? self.storyboardName 46 | let storyboard = UIStoryboard(name: sb, bundle: nil) 47 | return initial(fromStoryboard: storyboard) 48 | } 49 | 50 | public static func initial(fromStoryboard storyboard: UIStoryboard) -> Self { 51 | guard let vc = storyboard.instantiateInitialViewController() as? Self else { 52 | fatalError("Failed to instantiate initial view controller from storyboard named \( storyboard )") 53 | } 54 | return vc 55 | } 56 | } 57 | 58 | 59 | extension UINavigationController: StoryboardLoadable {} 60 | extension UITabBarController: StoryboardLoadable {} 61 | extension UISplitViewController: StoryboardLoadable {} 62 | extension UIPageViewController: StoryboardLoadable {} 63 | -------------------------------------------------------------------------------- /Vendor/GradientView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientView.swift 3 | // Gradient View 4 | // 5 | // Created by Sam Soffes on 10/27/09. 6 | // Copyright (c) 2009-2014 Sam Soffes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// Simple view for drawing gradients and borders. 12 | @IBDesignable open class GradientView: UIView { 13 | 14 | // MARK: - Types 15 | 16 | /// The mode of the gradient. 17 | @objc public enum Mode: Int { 18 | /// A linear gradient. 19 | case linear 20 | 21 | /// A radial gradient. 22 | case radial 23 | } 24 | 25 | 26 | /// The direction of the gradient. 27 | @objc public enum Direction: Int { 28 | /// The gradient is vertical. 29 | case vertical 30 | 31 | /// The gradient is horizontal 32 | case horizontal 33 | } 34 | 35 | 36 | // MARK: - Properties 37 | 38 | /// An optional array of `UIColor` objects used to draw the gradient. If the value is `nil`, the `backgroundColor` 39 | /// will be drawn instead of a gradient. The default is `nil`. 40 | open var colors: [UIColor]? { 41 | didSet { 42 | updateGradient() 43 | } 44 | } 45 | 46 | /// An array of `UIColor` objects used to draw the dimmed gradient. If the value is `nil`, `colors` will be 47 | /// converted to grayscale. This will use the same `locations` as `colors`. If length of arrays don't match, bad 48 | /// things will happen. You must make sure the number of dimmed colors equals the number of regular colors. 49 | /// 50 | /// The default is `nil`. 51 | open var dimmedColors: [UIColor]? { 52 | didSet { 53 | updateGradient() 54 | } 55 | } 56 | 57 | /// Automatically dim gradient colors when prompted by the system (i.e. when an alert is shown). 58 | /// 59 | /// The default is `true`. 60 | open var automaticallyDims: Bool = true 61 | 62 | /// An optional array of `CGFloat`s defining the location of each gradient stop. 63 | /// 64 | /// The gradient stops are specified as values between `0` and `1`. The values must be monotonically increasing. If 65 | /// `nil`, the stops are spread uniformly across the range. 66 | /// 67 | /// Defaults to `nil`. 68 | open var locations: [CGFloat]? { 69 | didSet { 70 | updateGradient() 71 | } 72 | } 73 | 74 | /// The mode of the gradient. The default is `.Linear`. 75 | @IBInspectable open var mode: Mode = .linear { 76 | didSet { 77 | setNeedsDisplay() 78 | } 79 | } 80 | 81 | /// The direction of the gradient. Only valid for the `Mode.Linear` mode. The default is `.Vertical`. 82 | @IBInspectable open var direction: Direction = .horizontal { 83 | didSet { 84 | setNeedsDisplay() 85 | } 86 | } 87 | 88 | /// 1px borders will be drawn instead of 1pt borders. The default is `true`. 89 | @IBInspectable open var drawsThinBorders: Bool = true { 90 | didSet { 91 | setNeedsDisplay() 92 | } 93 | } 94 | 95 | /// The top border color. The default is `nil`. 96 | @IBInspectable open var topBorderColor: UIColor? { 97 | didSet { 98 | setNeedsDisplay() 99 | } 100 | } 101 | 102 | /// The right border color. The default is `nil`. 103 | @IBInspectable open var rightBorderColor: UIColor? { 104 | didSet { 105 | setNeedsDisplay() 106 | } 107 | } 108 | 109 | /// The bottom border color. The default is `nil`. 110 | @IBInspectable open var bottomBorderColor: UIColor? { 111 | didSet { 112 | setNeedsDisplay() 113 | } 114 | } 115 | 116 | /// The left border color. The default is `nil`. 117 | @IBInspectable open var leftBorderColor: UIColor? { 118 | didSet { 119 | setNeedsDisplay() 120 | } 121 | } 122 | 123 | 124 | // MARK: - UIView 125 | 126 | override open func draw(_ rect: CGRect) { 127 | let context = UIGraphicsGetCurrentContext() 128 | let size = bounds.size 129 | 130 | // Gradient 131 | if let gradient = gradient { 132 | let options: CGGradientDrawingOptions = [.drawsAfterEndLocation] 133 | 134 | if mode == .linear { 135 | let startPoint = CGPoint.zero 136 | let endPoint = direction == .vertical ? CGPoint(x: 0, y: size.height) : CGPoint(x: size.width, y: 0) 137 | context?.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: options) 138 | } else { 139 | let center = CGPoint(x: bounds.midX, y: bounds.midY) 140 | context?.drawRadialGradient(gradient, startCenter: center, startRadius: 0, endCenter: center, endRadius: min(size.width, size.height) / 2, options: options) 141 | } 142 | } 143 | 144 | let screen: UIScreen = window?.screen ?? UIScreen.main 145 | let borderWidth: CGFloat = drawsThinBorders ? 1.0 / screen.scale : 1.0 146 | 147 | // Top border 148 | if let color = topBorderColor { 149 | context?.setFillColor(color.cgColor) 150 | context?.fill(CGRect(x: 0, y: 0, width: size.width, height: borderWidth)) 151 | } 152 | 153 | let sideY: CGFloat = topBorderColor != nil ? borderWidth : 0 154 | let sideHeight: CGFloat = size.height - sideY - (bottomBorderColor != nil ? borderWidth : 0) 155 | 156 | // Right border 157 | if let color = rightBorderColor { 158 | context?.setFillColor(color.cgColor) 159 | context?.fill(CGRect(x: size.width - borderWidth, y: sideY, width: borderWidth, height: sideHeight)) 160 | } 161 | 162 | // Bottom border 163 | if let color = bottomBorderColor { 164 | context?.setFillColor(color.cgColor) 165 | context?.fill(CGRect(x: 0, y: size.height - borderWidth, width: size.width, height: borderWidth)) 166 | } 167 | 168 | // Left border 169 | if let color = leftBorderColor { 170 | context?.setFillColor(color.cgColor) 171 | context?.fill(CGRect(x: 0, y: sideY, width: borderWidth, height: sideHeight)) 172 | } 173 | } 174 | 175 | override open func tintColorDidChange() { 176 | super.tintColorDidChange() 177 | 178 | if automaticallyDims { 179 | updateGradient() 180 | } 181 | } 182 | 183 | override open func didMoveToWindow() { 184 | super.didMoveToWindow() 185 | contentMode = .redraw 186 | } 187 | 188 | 189 | // MARK: - Private 190 | 191 | fileprivate var gradient: CGGradient? 192 | 193 | fileprivate func updateGradient() { 194 | gradient = nil 195 | setNeedsDisplay() 196 | 197 | let colors = gradientColors() 198 | if let colors = colors { 199 | let colorSpace = CGColorSpaceCreateDeviceRGB() 200 | let colorSpaceModel = colorSpace.model 201 | 202 | let gradientColors: [CGColor] = colors.compactMap { (color: UIColor) -> CGColor? in 203 | let cgColor = color.cgColor 204 | let cgColorSpace = cgColor.colorSpace ?? colorSpace 205 | 206 | // The color's color space is RGB, simply add it. 207 | if cgColorSpace.model == colorSpaceModel { 208 | return cgColor 209 | } 210 | 211 | // Convert to RGB. There may be a more efficient way to do this. 212 | var red: CGFloat = 0 213 | var blue: CGFloat = 0 214 | var green: CGFloat = 0 215 | var alpha: CGFloat = 0 216 | color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) 217 | return UIColor(red: red, green: green, blue: blue, alpha: alpha).cgColor 218 | } 219 | 220 | gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors as CFArray, locations: locations) 221 | } 222 | } 223 | 224 | fileprivate func gradientColors() -> [UIColor]? { 225 | if tintAdjustmentMode == .dimmed { 226 | if let dimmedColors = dimmedColors { 227 | return dimmedColors 228 | } 229 | 230 | if automaticallyDims { 231 | if let colors = colors { 232 | return colors.map { 233 | var hue: CGFloat = 0 234 | var brightness: CGFloat = 0 235 | var alpha: CGFloat = 0 236 | 237 | $0.getHue(&hue, saturation: nil, brightness: &brightness, alpha: &alpha) 238 | 239 | return UIColor(hue: hue, saturation: 0, brightness: brightness, alpha: alpha) 240 | } 241 | } 242 | } 243 | } 244 | 245 | return colors 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /Vendor/Views/DequeableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DequeableView.swift 3 | // Radiant Tap Essentials 4 | // https://github.com/radianttap/swift-essentials 5 | // 6 | // Copyright © 2016 Radiant Tap 7 | // MIT License · http://choosealicense.com/licenses/mit/ 8 | // 9 | 10 | import UIKit 11 | 12 | 13 | extension UICollectionView { 14 | 15 | // register for the Class-based cell 16 | public func register(_: T.Type) 17 | where T: ReusableView 18 | { 19 | register(T.self, forCellWithReuseIdentifier: T.reuseIdentifier) 20 | } 21 | 22 | // register for the Nib-based cell 23 | public func register(_: T.Type) 24 | where T:NibReusableView 25 | { 26 | register(T.nib, forCellWithReuseIdentifier: T.reuseIdentifier) 27 | } 28 | 29 | public func dequeueReusableCell(forIndexPath indexPath: IndexPath) -> T 30 | where T:ReusableView 31 | { 32 | // this deque and cast can fail if you forget to register the proper cell 33 | guard let cell = dequeueReusableCell(withReuseIdentifier: T.reuseIdentifier, for: indexPath) as? T else { 34 | // thus crash instantly and nudge the developer 35 | fatalError("Dequeing a cell with identifier: \(T.reuseIdentifier) failed.\nDid you maybe forget to register it in viewDidLoad?") 36 | } 37 | return cell 38 | } 39 | 40 | // register for the Class-based supplementary view 41 | public func register(_: T.Type, kind: String) 42 | where T:ReusableView 43 | { 44 | register(T.self, forSupplementaryViewOfKind: kind, withReuseIdentifier: T.reuseIdentifier) 45 | } 46 | 47 | // register for the Nib-based supplementary view 48 | public func register(_: T.Type, kind: String) 49 | where T:NibReusableView 50 | { 51 | register(T.nib, forSupplementaryViewOfKind: kind, withReuseIdentifier: T.reuseIdentifier) 52 | } 53 | 54 | public func dequeueReusableView(kind: String, atIndexPath indexPath: IndexPath) -> T 55 | where T:ReusableView 56 | { 57 | // this deque and cast can fail if you forget to register the proper cell 58 | guard let view = dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: T.reuseIdentifier, for: indexPath) as? T else { 59 | // thus crash instantly and nudge the developer 60 | fatalError("Dequeing supplementary view of kind: \( kind ) with identifier: \( T.reuseIdentifier ) failed.\nDid you maybe forget to register it in viewDidLoad?") 61 | } 62 | return view 63 | } 64 | } 65 | 66 | 67 | extension UITableView { 68 | 69 | // register for the Class-based cell 70 | public func register(_: T.Type) 71 | where T: ReusableView 72 | { 73 | register(T.self, forCellReuseIdentifier: T.reuseIdentifier) 74 | } 75 | 76 | // register for the Nib-based cell 77 | public func register(_: T.Type) 78 | where T:NibReusableView 79 | { 80 | register(T.nib, forCellReuseIdentifier: T.reuseIdentifier) 81 | } 82 | 83 | public func dequeueReusableCell(forIndexPath indexPath: IndexPath) -> T 84 | where T:ReusableView 85 | { 86 | guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else { 87 | fatalError("Dequeing a cell with identifier: \(T.reuseIdentifier) failed.\nDid you maybe forget to register it in viewDidLoad?") 88 | } 89 | return cell 90 | } 91 | 92 | // register for the Class-based header/footer view 93 | public func register(_: T.Type) 94 | where T:ReusableView 95 | { 96 | register(T.self, forHeaderFooterViewReuseIdentifier: T.reuseIdentifier) 97 | } 98 | 99 | // register for the Nib-based header/footer view 100 | public func register(_: T.Type) 101 | where T:NibReusableView 102 | { 103 | register(T.nib, forHeaderFooterViewReuseIdentifier: T.reuseIdentifier) 104 | } 105 | 106 | public func dequeueReusableView() -> T? 107 | where T:ReusableView 108 | { 109 | let v = dequeueReusableHeaderFooterView(withIdentifier: T.reuseIdentifier) as? T 110 | return v 111 | } 112 | } 113 | 114 | -------------------------------------------------------------------------------- /Vendor/Views/NibLoadable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NibLoadable.swift 3 | // Radiant Tap Essentials 4 | // https://github.com/radianttap/swift-essentials 5 | // 6 | // Copyright © 2016 Radiant Tap 7 | // MIT License · http://choosealicense.com/licenses/mit/ 8 | // 9 | 10 | import UIKit 11 | 12 | /// Adopt this protocol on all subclasses of UITableViewCell and UICollectionViewCell 13 | /// that use their own .xib file 14 | public protocol NibLoadableView { 15 | /// By default, it returns the subclass name 16 | static var nibName: String { get } 17 | 18 | /// Instantiates UINib using `nibName` as the name, from the main bundle 19 | static var nib: UINib { get } 20 | } 21 | 22 | extension NibLoadableView where Self: UIView { 23 | public static var nibName: String { 24 | return String(describing: self) 25 | } 26 | 27 | public static var nib: UINib { 28 | return UINib(nibName: self.nibName, bundle: nil) 29 | } 30 | } 31 | 32 | public protocol NibReusableView : ReusableView, NibLoadableView {} 33 | 34 | 35 | 36 | /// Adopt this in cases where you need to create an ad-hoc instance of the given view 37 | /// Can be adopted only by classes marked as `final`, due to `Self` constraint 38 | public protocol NibLoadableFinalView: NibLoadableView { 39 | /// Creates an instance of the cell from the `nibName`.xib file 40 | static var nibInstance : Self { get } 41 | } 42 | 43 | extension NibLoadableFinalView { 44 | public static var nibInstance : Self { 45 | guard let nibObject = self.nib.instantiate(withOwner: nil, options: nil).last as? Self else { 46 | fatalError("Failed to create an instance of \(self) from \(self.nibName) nib.") 47 | } 48 | return nibObject 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /Vendor/Views/ReusableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReusableView.swift 3 | // Radiant Tap Essentials 4 | // https://github.com/radianttap/swift-essentials 5 | // 6 | // Copyright © 2016 Radiant Tap 7 | // MIT License · http://choosealicense.com/licenses/mit/ 8 | // 9 | 10 | import UIKit 11 | 12 | /// Protocol to allow any UIView to become reusable view 13 | public protocol ReusableView { 14 | /// By default, it returns the subclass name 15 | static var reuseIdentifier: String { get } 16 | } 17 | 18 | extension ReusableView where Self: UIView { 19 | public static var reuseIdentifier: String { 20 | return String(describing: self) 21 | } 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /resources/apple-music-iphone-xs.acorn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/resources/apple-music-iphone-xs.acorn -------------------------------------------------------------------------------- /resources/apple-music-iphone-xs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/resources/apple-music-iphone-xs.png -------------------------------------------------------------------------------- /resources/cardpc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/resources/cardpc.png -------------------------------------------------------------------------------- /resources/presentedNC-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/resources/presentedNC-top.png -------------------------------------------------------------------------------- /resources/presentedNC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/resources/presentedNC.png -------------------------------------------------------------------------------- /resources/presentedVC-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/resources/presentedVC-top.png -------------------------------------------------------------------------------- /resources/presentedVC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radianttap/CardPresentationController/91aae49731cfa0b7107a03420f27eb0307f8a0ae/resources/presentedVC.png --------------------------------------------------------------------------------