├── .gitignore ├── CollectionViewSemiModalTransitioning.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── CollectionViewSemiModalTransitioning ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── CollectionViewSemiModal │ ├── CollectionSemiModalViewCell.swift │ ├── CollectionSemiModalViewCell.xib │ ├── CollectionSemiModalViewController.storyboard │ ├── CollectionSemiModalViewController.swift │ ├── DetailViewController.swift │ ├── TableViewTitleCell.swift │ └── TableViewTitleCell.xib ├── CustomTransition.swift ├── Extensions.swift ├── Info.plist ├── SemiModalTransitioning │ ├── CollectionViewPresentAnimator.swift │ ├── DismissalTransitionable.swift │ ├── DismissalTransitioningInteractor.swift │ ├── ModalPresentationController.swift │ └── SemiModalTransitioningDelegate.swift └── ViewController.swift └── README.md /.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 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | 70 | # Mac 71 | .DS_Store -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 982355DE22DB7171009C5FD5 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 982355DD22DB7171009C5FD5 /* DetailViewController.swift */; }; 11 | 9826DD1122D1CE76005ADAEF /* TableViewTitleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9826DD0F22D1CE76005ADAEF /* TableViewTitleCell.swift */; }; 12 | 9826DD1222D1CE76005ADAEF /* TableViewTitleCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9826DD1022D1CE76005ADAEF /* TableViewTitleCell.xib */; }; 13 | 985319572211CC5800378DA5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 985319562211CC5800378DA5 /* AppDelegate.swift */; }; 14 | 985319592211CC5800378DA5 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 985319582211CC5800378DA5 /* ViewController.swift */; }; 15 | 9853195C2211CC5800378DA5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9853195A2211CC5800378DA5 /* Main.storyboard */; }; 16 | 9853195E2211CC5B00378DA5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9853195D2211CC5B00378DA5 /* Assets.xcassets */; }; 17 | 985319612211CC5B00378DA5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9853195F2211CC5B00378DA5 /* LaunchScreen.storyboard */; }; 18 | 9878E9BE22BE028B00E7C37A /* ModalPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9878E9BA22BE028B00E7C37A /* ModalPresentationController.swift */; }; 19 | 9878E9BF22BE028B00E7C37A /* DismissalTransitionable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9878E9BB22BE028B00E7C37A /* DismissalTransitionable.swift */; }; 20 | 9878E9C122BE028B00E7C37A /* DismissalTransitioningInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9878E9BD22BE028B00E7C37A /* DismissalTransitioningInteractor.swift */; }; 21 | 9878E9C322BE06B600E7C37A /* SemiModalTransitioningDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9878E9C222BE06B600E7C37A /* SemiModalTransitioningDelegate.swift */; }; 22 | 98E3C5C12217CE3C00DDDD72 /* CollectionSemiModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E3C5C02217CE3C00DDDD72 /* CollectionSemiModalViewController.swift */; }; 23 | 98E3C5C32217CE7C00DDDD72 /* CollectionSemiModalViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 98E3C5C22217CE7C00DDDD72 /* CollectionSemiModalViewController.storyboard */; }; 24 | 98E3C5E7223382A700DDDD72 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E3C5E6223382A700DDDD72 /* Extensions.swift */; }; 25 | 98E3C5EC2233890C00DDDD72 /* CollectionSemiModalViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E3C5EA2233890C00DDDD72 /* CollectionSemiModalViewCell.swift */; }; 26 | 98E3C5ED2233890C00DDDD72 /* CollectionSemiModalViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98E3C5EB2233890C00DDDD72 /* CollectionSemiModalViewCell.xib */; }; 27 | 98E3C5EF22392DBA00DDDD72 /* CollectionViewPresentAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E3C5EE22392DBA00DDDD72 /* CollectionViewPresentAnimator.swift */; }; 28 | 98E3C5F9224376ED00DDDD72 /* CustomTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E3C5F8224376ED00DDDD72 /* CustomTransition.swift */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 982355DD22DB7171009C5FD5 /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; 33 | 9826DD0F22D1CE76005ADAEF /* TableViewTitleCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewTitleCell.swift; sourceTree = ""; }; 34 | 9826DD1022D1CE76005ADAEF /* TableViewTitleCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TableViewTitleCell.xib; sourceTree = ""; }; 35 | 985319532211CC5800378DA5 /* CollectionViewSemiModalTransitioning.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CollectionViewSemiModalTransitioning.app; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | 985319562211CC5800378DA5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 985319582211CC5800378DA5 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 38 | 9853195B2211CC5800378DA5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 39 | 9853195D2211CC5B00378DA5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 40 | 985319602211CC5B00378DA5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 41 | 985319622211CC5B00378DA5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 42 | 9878E9BA22BE028B00E7C37A /* ModalPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalPresentationController.swift; sourceTree = ""; }; 43 | 9878E9BB22BE028B00E7C37A /* DismissalTransitionable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DismissalTransitionable.swift; sourceTree = ""; }; 44 | 9878E9BD22BE028B00E7C37A /* DismissalTransitioningInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DismissalTransitioningInteractor.swift; sourceTree = ""; }; 45 | 9878E9C222BE06B600E7C37A /* SemiModalTransitioningDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemiModalTransitioningDelegate.swift; sourceTree = ""; }; 46 | 98E3C5C02217CE3C00DDDD72 /* CollectionSemiModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionSemiModalViewController.swift; sourceTree = ""; }; 47 | 98E3C5C22217CE7C00DDDD72 /* CollectionSemiModalViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = CollectionSemiModalViewController.storyboard; sourceTree = ""; }; 48 | 98E3C5E6223382A700DDDD72 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 49 | 98E3C5EA2233890C00DDDD72 /* CollectionSemiModalViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionSemiModalViewCell.swift; sourceTree = ""; }; 50 | 98E3C5EB2233890C00DDDD72 /* CollectionSemiModalViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CollectionSemiModalViewCell.xib; sourceTree = ""; }; 51 | 98E3C5EE22392DBA00DDDD72 /* CollectionViewPresentAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewPresentAnimator.swift; sourceTree = ""; }; 52 | 98E3C5F8224376ED00DDDD72 /* CustomTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTransition.swift; sourceTree = ""; }; 53 | /* End PBXFileReference section */ 54 | 55 | /* Begin PBXFrameworksBuildPhase section */ 56 | 985319502211CC5800378DA5 /* Frameworks */ = { 57 | isa = PBXFrameworksBuildPhase; 58 | buildActionMask = 2147483647; 59 | files = ( 60 | ); 61 | runOnlyForDeploymentPostprocessing = 0; 62 | }; 63 | /* End PBXFrameworksBuildPhase section */ 64 | 65 | /* Begin PBXGroup section */ 66 | 9853194A2211CC5700378DA5 = { 67 | isa = PBXGroup; 68 | children = ( 69 | 985319552211CC5800378DA5 /* CollectionViewSemiModalTransitioning */, 70 | 985319542211CC5800378DA5 /* Products */, 71 | ); 72 | sourceTree = ""; 73 | }; 74 | 985319542211CC5800378DA5 /* Products */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 985319532211CC5800378DA5 /* CollectionViewSemiModalTransitioning.app */, 78 | ); 79 | name = Products; 80 | sourceTree = ""; 81 | }; 82 | 985319552211CC5800378DA5 /* CollectionViewSemiModalTransitioning */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 9878E9B122BE025100E7C37A /* SemiModalTransitioning */, 86 | 9878E9B022BDE1AE00E7C37A /* CollectionViewSemiModal */, 87 | 985319562211CC5800378DA5 /* AppDelegate.swift */, 88 | 985319582211CC5800378DA5 /* ViewController.swift */, 89 | 9853195A2211CC5800378DA5 /* Main.storyboard */, 90 | 9853195D2211CC5B00378DA5 /* Assets.xcassets */, 91 | 9853195F2211CC5B00378DA5 /* LaunchScreen.storyboard */, 92 | 985319622211CC5B00378DA5 /* Info.plist */, 93 | 98E3C5E6223382A700DDDD72 /* Extensions.swift */, 94 | 98E3C5F8224376ED00DDDD72 /* CustomTransition.swift */, 95 | ); 96 | path = CollectionViewSemiModalTransitioning; 97 | sourceTree = ""; 98 | }; 99 | 9878E9B022BDE1AE00E7C37A /* CollectionViewSemiModal */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 98E3C5C02217CE3C00DDDD72 /* CollectionSemiModalViewController.swift */, 103 | 98E3C5C22217CE7C00DDDD72 /* CollectionSemiModalViewController.storyboard */, 104 | 98E3C5EA2233890C00DDDD72 /* CollectionSemiModalViewCell.swift */, 105 | 98E3C5EB2233890C00DDDD72 /* CollectionSemiModalViewCell.xib */, 106 | 9826DD0F22D1CE76005ADAEF /* TableViewTitleCell.swift */, 107 | 9826DD1022D1CE76005ADAEF /* TableViewTitleCell.xib */, 108 | 982355DD22DB7171009C5FD5 /* DetailViewController.swift */, 109 | ); 110 | path = CollectionViewSemiModal; 111 | sourceTree = ""; 112 | }; 113 | 9878E9B122BE025100E7C37A /* SemiModalTransitioning */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 98E3C5EE22392DBA00DDDD72 /* CollectionViewPresentAnimator.swift */, 117 | 9878E9BA22BE028B00E7C37A /* ModalPresentationController.swift */, 118 | 9878E9BB22BE028B00E7C37A /* DismissalTransitionable.swift */, 119 | 9878E9BD22BE028B00E7C37A /* DismissalTransitioningInteractor.swift */, 120 | 9878E9C222BE06B600E7C37A /* SemiModalTransitioningDelegate.swift */, 121 | ); 122 | path = SemiModalTransitioning; 123 | sourceTree = ""; 124 | }; 125 | /* End PBXGroup section */ 126 | 127 | /* Begin PBXNativeTarget section */ 128 | 985319522211CC5800378DA5 /* CollectionViewSemiModalTransitioning */ = { 129 | isa = PBXNativeTarget; 130 | buildConfigurationList = 985319652211CC5B00378DA5 /* Build configuration list for PBXNativeTarget "CollectionViewSemiModalTransitioning" */; 131 | buildPhases = ( 132 | 9853194F2211CC5800378DA5 /* Sources */, 133 | 985319502211CC5800378DA5 /* Frameworks */, 134 | 985319512211CC5800378DA5 /* Resources */, 135 | ); 136 | buildRules = ( 137 | ); 138 | dependencies = ( 139 | ); 140 | name = CollectionViewSemiModalTransitioning; 141 | productName = CollectionViewSemiModalTransitioning; 142 | productReference = 985319532211CC5800378DA5 /* CollectionViewSemiModalTransitioning.app */; 143 | productType = "com.apple.product-type.application"; 144 | }; 145 | /* End PBXNativeTarget section */ 146 | 147 | /* Begin PBXProject section */ 148 | 9853194B2211CC5700378DA5 /* Project object */ = { 149 | isa = PBXProject; 150 | attributes = { 151 | LastSwiftUpdateCheck = 1010; 152 | LastUpgradeCheck = 1010; 153 | ORGANIZATIONNAME = Yoichi; 154 | TargetAttributes = { 155 | 985319522211CC5800378DA5 = { 156 | CreatedOnToolsVersion = 10.1; 157 | }; 158 | }; 159 | }; 160 | buildConfigurationList = 9853194E2211CC5700378DA5 /* Build configuration list for PBXProject "CollectionViewSemiModalTransitioning" */; 161 | compatibilityVersion = "Xcode 9.3"; 162 | developmentRegion = en; 163 | hasScannedForEncodings = 0; 164 | knownRegions = ( 165 | en, 166 | Base, 167 | ); 168 | mainGroup = 9853194A2211CC5700378DA5; 169 | productRefGroup = 985319542211CC5800378DA5 /* Products */; 170 | projectDirPath = ""; 171 | projectRoot = ""; 172 | targets = ( 173 | 985319522211CC5800378DA5 /* CollectionViewSemiModalTransitioning */, 174 | ); 175 | }; 176 | /* End PBXProject section */ 177 | 178 | /* Begin PBXResourcesBuildPhase section */ 179 | 985319512211CC5800378DA5 /* Resources */ = { 180 | isa = PBXResourcesBuildPhase; 181 | buildActionMask = 2147483647; 182 | files = ( 183 | 985319612211CC5B00378DA5 /* LaunchScreen.storyboard in Resources */, 184 | 98E3C5C32217CE7C00DDDD72 /* CollectionSemiModalViewController.storyboard in Resources */, 185 | 9853195E2211CC5B00378DA5 /* Assets.xcassets in Resources */, 186 | 9853195C2211CC5800378DA5 /* Main.storyboard in Resources */, 187 | 9826DD1222D1CE76005ADAEF /* TableViewTitleCell.xib in Resources */, 188 | 98E3C5ED2233890C00DDDD72 /* CollectionSemiModalViewCell.xib in Resources */, 189 | ); 190 | runOnlyForDeploymentPostprocessing = 0; 191 | }; 192 | /* End PBXResourcesBuildPhase section */ 193 | 194 | /* Begin PBXSourcesBuildPhase section */ 195 | 9853194F2211CC5800378DA5 /* Sources */ = { 196 | isa = PBXSourcesBuildPhase; 197 | buildActionMask = 2147483647; 198 | files = ( 199 | 98E3C5EC2233890C00DDDD72 /* CollectionSemiModalViewCell.swift in Sources */, 200 | 98E3C5EF22392DBA00DDDD72 /* CollectionViewPresentAnimator.swift in Sources */, 201 | 9878E9C122BE028B00E7C37A /* DismissalTransitioningInteractor.swift in Sources */, 202 | 9878E9BF22BE028B00E7C37A /* DismissalTransitionable.swift in Sources */, 203 | 98E3C5C12217CE3C00DDDD72 /* CollectionSemiModalViewController.swift in Sources */, 204 | 985319592211CC5800378DA5 /* ViewController.swift in Sources */, 205 | 9878E9C322BE06B600E7C37A /* SemiModalTransitioningDelegate.swift in Sources */, 206 | 9878E9BE22BE028B00E7C37A /* ModalPresentationController.swift in Sources */, 207 | 9826DD1122D1CE76005ADAEF /* TableViewTitleCell.swift in Sources */, 208 | 985319572211CC5800378DA5 /* AppDelegate.swift in Sources */, 209 | 98E3C5E7223382A700DDDD72 /* Extensions.swift in Sources */, 210 | 98E3C5F9224376ED00DDDD72 /* CustomTransition.swift in Sources */, 211 | 982355DE22DB7171009C5FD5 /* DetailViewController.swift in Sources */, 212 | ); 213 | runOnlyForDeploymentPostprocessing = 0; 214 | }; 215 | /* End PBXSourcesBuildPhase section */ 216 | 217 | /* Begin PBXVariantGroup section */ 218 | 9853195A2211CC5800378DA5 /* Main.storyboard */ = { 219 | isa = PBXVariantGroup; 220 | children = ( 221 | 9853195B2211CC5800378DA5 /* Base */, 222 | ); 223 | name = Main.storyboard; 224 | sourceTree = ""; 225 | }; 226 | 9853195F2211CC5B00378DA5 /* LaunchScreen.storyboard */ = { 227 | isa = PBXVariantGroup; 228 | children = ( 229 | 985319602211CC5B00378DA5 /* Base */, 230 | ); 231 | name = LaunchScreen.storyboard; 232 | sourceTree = ""; 233 | }; 234 | /* End PBXVariantGroup section */ 235 | 236 | /* Begin XCBuildConfiguration section */ 237 | 985319632211CC5B00378DA5 /* Debug */ = { 238 | isa = XCBuildConfiguration; 239 | buildSettings = { 240 | ALWAYS_SEARCH_USER_PATHS = NO; 241 | CLANG_ANALYZER_NONNULL = YES; 242 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 243 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 244 | CLANG_CXX_LIBRARY = "libc++"; 245 | CLANG_ENABLE_MODULES = YES; 246 | CLANG_ENABLE_OBJC_ARC = YES; 247 | CLANG_ENABLE_OBJC_WEAK = YES; 248 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 249 | CLANG_WARN_BOOL_CONVERSION = YES; 250 | CLANG_WARN_COMMA = YES; 251 | CLANG_WARN_CONSTANT_CONVERSION = YES; 252 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 253 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 254 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 255 | CLANG_WARN_EMPTY_BODY = YES; 256 | CLANG_WARN_ENUM_CONVERSION = YES; 257 | CLANG_WARN_INFINITE_RECURSION = YES; 258 | CLANG_WARN_INT_CONVERSION = YES; 259 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 260 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 261 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 262 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 263 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 264 | CLANG_WARN_STRICT_PROTOTYPES = YES; 265 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 266 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 267 | CLANG_WARN_UNREACHABLE_CODE = YES; 268 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 269 | CODE_SIGN_IDENTITY = "iPhone Developer"; 270 | COPY_PHASE_STRIP = NO; 271 | DEBUG_INFORMATION_FORMAT = dwarf; 272 | ENABLE_STRICT_OBJC_MSGSEND = YES; 273 | ENABLE_TESTABILITY = YES; 274 | GCC_C_LANGUAGE_STANDARD = gnu11; 275 | GCC_DYNAMIC_NO_PIC = NO; 276 | GCC_NO_COMMON_BLOCKS = YES; 277 | GCC_OPTIMIZATION_LEVEL = 0; 278 | GCC_PREPROCESSOR_DEFINITIONS = ( 279 | "DEBUG=1", 280 | "$(inherited)", 281 | ); 282 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 283 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 284 | GCC_WARN_UNDECLARED_SELECTOR = YES; 285 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 286 | GCC_WARN_UNUSED_FUNCTION = YES; 287 | GCC_WARN_UNUSED_VARIABLE = YES; 288 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 289 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 290 | MTL_FAST_MATH = YES; 291 | ONLY_ACTIVE_ARCH = YES; 292 | SDKROOT = iphoneos; 293 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 294 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 295 | }; 296 | name = Debug; 297 | }; 298 | 985319642211CC5B00378DA5 /* Release */ = { 299 | isa = XCBuildConfiguration; 300 | buildSettings = { 301 | ALWAYS_SEARCH_USER_PATHS = NO; 302 | CLANG_ANALYZER_NONNULL = YES; 303 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 304 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 305 | CLANG_CXX_LIBRARY = "libc++"; 306 | CLANG_ENABLE_MODULES = YES; 307 | CLANG_ENABLE_OBJC_ARC = YES; 308 | CLANG_ENABLE_OBJC_WEAK = YES; 309 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 310 | CLANG_WARN_BOOL_CONVERSION = YES; 311 | CLANG_WARN_COMMA = YES; 312 | CLANG_WARN_CONSTANT_CONVERSION = YES; 313 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 314 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 315 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 316 | CLANG_WARN_EMPTY_BODY = YES; 317 | CLANG_WARN_ENUM_CONVERSION = YES; 318 | CLANG_WARN_INFINITE_RECURSION = YES; 319 | CLANG_WARN_INT_CONVERSION = YES; 320 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 321 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 322 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 323 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 324 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 325 | CLANG_WARN_STRICT_PROTOTYPES = YES; 326 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 327 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 328 | CLANG_WARN_UNREACHABLE_CODE = YES; 329 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 330 | CODE_SIGN_IDENTITY = "iPhone Developer"; 331 | COPY_PHASE_STRIP = NO; 332 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 333 | ENABLE_NS_ASSERTIONS = NO; 334 | ENABLE_STRICT_OBJC_MSGSEND = YES; 335 | GCC_C_LANGUAGE_STANDARD = gnu11; 336 | GCC_NO_COMMON_BLOCKS = YES; 337 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 338 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 339 | GCC_WARN_UNDECLARED_SELECTOR = YES; 340 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 341 | GCC_WARN_UNUSED_FUNCTION = YES; 342 | GCC_WARN_UNUSED_VARIABLE = YES; 343 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 344 | MTL_ENABLE_DEBUG_INFO = NO; 345 | MTL_FAST_MATH = YES; 346 | SDKROOT = iphoneos; 347 | SWIFT_COMPILATION_MODE = wholemodule; 348 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 349 | VALIDATE_PRODUCT = YES; 350 | }; 351 | name = Release; 352 | }; 353 | 985319662211CC5B00378DA5 /* Debug */ = { 354 | isa = XCBuildConfiguration; 355 | buildSettings = { 356 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 357 | CODE_SIGN_STYLE = Automatic; 358 | DEVELOPMENT_TEAM = MYNLSAF27S; 359 | INFOPLIST_FILE = CollectionViewSemiModalTransitioning/Info.plist; 360 | LD_RUNPATH_SEARCH_PATHS = ( 361 | "$(inherited)", 362 | "@executable_path/Frameworks", 363 | ); 364 | PRODUCT_BUNDLE_IDENTIFIER = com.CollectionViewSemiModalTransitioning; 365 | PRODUCT_NAME = "$(TARGET_NAME)"; 366 | SWIFT_VERSION = 4.2; 367 | TARGETED_DEVICE_FAMILY = "1,2"; 368 | }; 369 | name = Debug; 370 | }; 371 | 985319672211CC5B00378DA5 /* Release */ = { 372 | isa = XCBuildConfiguration; 373 | buildSettings = { 374 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 375 | CODE_SIGN_STYLE = Automatic; 376 | DEVELOPMENT_TEAM = MYNLSAF27S; 377 | INFOPLIST_FILE = CollectionViewSemiModalTransitioning/Info.plist; 378 | LD_RUNPATH_SEARCH_PATHS = ( 379 | "$(inherited)", 380 | "@executable_path/Frameworks", 381 | ); 382 | PRODUCT_BUNDLE_IDENTIFIER = com.CollectionViewSemiModalTransitioning; 383 | PRODUCT_NAME = "$(TARGET_NAME)"; 384 | SWIFT_VERSION = 4.2; 385 | TARGETED_DEVICE_FAMILY = "1,2"; 386 | }; 387 | name = Release; 388 | }; 389 | /* End XCBuildConfiguration section */ 390 | 391 | /* Begin XCConfigurationList section */ 392 | 9853194E2211CC5700378DA5 /* Build configuration list for PBXProject "CollectionViewSemiModalTransitioning" */ = { 393 | isa = XCConfigurationList; 394 | buildConfigurations = ( 395 | 985319632211CC5B00378DA5 /* Debug */, 396 | 985319642211CC5B00378DA5 /* Release */, 397 | ); 398 | defaultConfigurationIsVisible = 0; 399 | defaultConfigurationName = Release; 400 | }; 401 | 985319652211CC5B00378DA5 /* Build configuration list for PBXNativeTarget "CollectionViewSemiModalTransitioning" */ = { 402 | isa = XCConfigurationList; 403 | buildConfigurations = ( 404 | 985319662211CC5B00378DA5 /* Debug */, 405 | 985319672211CC5B00378DA5 /* Release */, 406 | ); 407 | defaultConfigurationIsVisible = 0; 408 | defaultConfigurationName = Release; 409 | }; 410 | /* End XCConfigurationList section */ 411 | }; 412 | rootObject = 9853194B2211CC5700378DA5 /* Project object */; 413 | } 414 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CollectionViewSemiModalTransitioning 4 | // 5 | // Created by Yoichi on 2019/02/12. 6 | // Copyright © 2019 Yoichi. 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 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/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 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/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 | 39 | 40 | 41 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/CollectionViewSemiModal/CollectionSemiModalViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewCell.swift 3 | // CollectionViewSemiModalTransitioning 4 | // 5 | // Created by Yoichi on 2019/03/09. 6 | // Copyright © 2019 Yoichi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CollectionSemiModalViewCell: UICollectionViewCell { 12 | var titleColorView: UIView? { 13 | guard let titleCell = tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? TableViewTitleCell else { return nil } 14 | return titleCell.colorView 15 | } 16 | 17 | var scrollViewDidScrollHandler: ((_ offsetY: CGFloat) -> Void)? 18 | 19 | var tableViewDidSelectHandler: ((_ row: Int) -> Void)? 20 | 21 | var closeTapHandler: (() -> Void)? 22 | 23 | @IBOutlet weak var tableView: UITableView! 24 | 25 | var data: ViewData! 26 | private var headerHeight: CGFloat! 27 | 28 | override func awakeFromNib() { 29 | super.awakeFromNib() 30 | 31 | contentView.backgroundColor = .clear 32 | 33 | tableView.backgroundColor = .clear 34 | tableView.delegate = self 35 | tableView.dataSource = self 36 | tableView.rowHeight = UITableView.automaticDimension 37 | tableView.estimatedRowHeight = 44 38 | tableView.tableHeaderView = UIView(frame: .zero) 39 | tableView.contentInsetAdjustmentBehavior = .never 40 | tableView.register(cellType: TableViewTitleCell.self) 41 | } 42 | 43 | func configure(headerHeight: CGFloat, data: ViewData) { 44 | self.data = data 45 | self.headerHeight = headerHeight 46 | tableView.tableHeaderView?.frame = CGRect(x: 0, y: 0, width: tableView.frame.width, height: headerHeight) 47 | tableView.reloadData() 48 | } 49 | 50 | func switchTitleColorView(isClear: Bool) { 51 | titleColorView?.backgroundColor = isClear ? .clear : data.color 52 | } 53 | 54 | func scrollToTop() { 55 | tableView.scrollRectToVisible(tableView.tableHeaderView!.frame, animated: true) 56 | } 57 | 58 | func updateBounces(_ isBounces: Bool) { 59 | tableView.bounces = isBounces 60 | } 61 | } 62 | 63 | // MARK: - UITableViewDelegate, UITableViewDataSource Methods 64 | extension CollectionSemiModalViewCell: UITableViewDelegate, UITableViewDataSource { 65 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 66 | return 30 67 | } 68 | 69 | func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 70 | return UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: headerHeight)) 71 | } 72 | 73 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 74 | if indexPath.row == 0 { 75 | let cell = tableView.dequeueReusableCell(with: TableViewTitleCell.self, for: indexPath) 76 | cell.configure(data: data) 77 | cell.closeTapHandler = closeTapHandler 78 | return cell 79 | } else { 80 | let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell") 81 | cell.selectionStyle = .none 82 | cell.textLabel?.text = String(indexPath.row) 83 | return cell 84 | } 85 | } 86 | 87 | func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { 88 | if indexPath.row == 0 { 89 | return 360 90 | } else { 91 | return 44 92 | } 93 | } 94 | 95 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 96 | tableViewDidSelectHandler?(indexPath.row) 97 | } 98 | 99 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 100 | /// TableViewが慣性でスクロール終了した際、最上部のCellが表示されていれば先頭までスクロール 101 | scrollToTopIfNeeded(offsetY: scrollView.contentOffset.y) 102 | } 103 | 104 | func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 105 | /// TableViewをドラッグしてスクロール終了した際、最上部のCellが表示されていれば先頭までスクロール 106 | scrollToTopIfNeeded(offsetY: scrollView.contentOffset.y) 107 | } 108 | 109 | private func scrollToTopIfNeeded(offsetY scrollViewContentOffsetY: CGFloat) { 110 | if scrollViewContentOffsetY < headerHeight { 111 | scrollToTop() 112 | } 113 | } 114 | 115 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 116 | /// TableView ScrollDown 117 | scrollViewDidScrollHandler?(scrollView.contentOffset.y) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/CollectionViewSemiModal/CollectionSemiModalViewCell.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 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/CollectionViewSemiModal/CollectionSemiModalViewController.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 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/CollectionViewSemiModal/CollectionSemiModalViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewController.swift 3 | // CollectionViewSemiModalTransitioning 4 | // 5 | // Created by Yoichi on 2019/02/16. 6 | // Copyright © 2019 Yoichi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class CollectionSemiModalViewController: UIViewController, DismissalTransitionable { 12 | // DismissalTransitionable Property 13 | let percentThreshold: CGFloat = 0.2 14 | let shouldFinishVerocityY: CGFloat = 1200 15 | let interactor = DismissalTransitioningInteractor() 16 | 17 | private let visibleNaviBarOffsetY: CGFloat = 100 18 | private let cellHeaderHeight: CGFloat = 72 19 | 20 | var selectedIndex = 0 21 | private var indexOfCellBeforeDragging = 0 22 | private var tableViewContentOffsetY: CGFloat = 0 23 | private var isScrollingCollectionView = false 24 | private var isFirst = true 25 | private var dataList: [ViewData] = [] 26 | 27 | @IBOutlet weak var collectionView: UICollectionView! 28 | @IBOutlet weak var layout: CustomCollectionViewFlowLayout! 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | setupNavigation() 33 | setupViews() 34 | setupInteractor() 35 | } 36 | 37 | override func viewDidLayoutSubviews() { 38 | super.viewDidLayoutSubviews() 39 | if isFirst { 40 | collectionView.scrollToItem(at: IndexPath(row: selectedIndex, section: 0), at: .centeredHorizontally, animated: false) 41 | isFirst = false 42 | } 43 | } 44 | 45 | private func setupNavigation() { 46 | navigationController?.isNavigationBarHidden = true 47 | navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white] 48 | navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(didTapDone)) 49 | navigationItem.leftBarButtonItem?.tintColor = .white 50 | } 51 | 52 | private func setupViews() { 53 | view.backgroundColor = .clear 54 | 55 | let collectionViewGesture = UIPanGestureRecognizer(target: self, action: #selector(collectionViewDidDragging(_:))) 56 | collectionViewGesture.delegate = self 57 | collectionView.addGestureRecognizer(collectionViewGesture) 58 | 59 | collectionView.register(cellType: CollectionSemiModalViewCell.self) 60 | collectionView.delegate = self 61 | collectionView.dataSource = self 62 | collectionView.backgroundColor = .clear 63 | layout.prepare() 64 | 65 | // ナビゲーションバーの表示制御を行う場合、表示切り替えごとにcontentInsetが変動し、それにより表示が崩れたりCollectionViewのサイズがおかしくなってスクロールができなくなる。 66 | // 対策として、contentInsetAdjustmentBehavior の設定をCollectionViewとCell内部のScrollViewで変動しないよう、.neverに設定する。 67 | // 合わせて、CollectionViewの上方向制約条件はSafeAreaに対してではなく、SuperViewに対して行う必要がある。 68 | collectionView.contentInsetAdjustmentBehavior = .never 69 | } 70 | 71 | /// OverCurrentTransitioningInteractorのセットアップ 各種ハンドラーのセット 72 | private func setupInteractor() { 73 | interactor.startHandler = { [weak self] in 74 | self?.collectionView.visibleCells 75 | .compactMap { $0 as? CollectionSemiModalViewCell } 76 | .forEach { $0.updateBounces(false) } 77 | } 78 | interactor.changedHandler = { [weak self] offsetY in 79 | self?.collectionView.frame.origin = CGPoint(x: 0, y: offsetY) 80 | } 81 | interactor.finishHandler = { [weak self] in 82 | self?.dismiss(isInteractive: true) 83 | } 84 | interactor.resetHandler = { [weak self] in 85 | UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut], animations: { 86 | self?.collectionView.frame.origin = CGPoint(x: 0, y: 0) 87 | self?.collectionView.visibleCells 88 | .compactMap { $0 as? CollectionSemiModalViewCell } 89 | .forEach { $0.updateBounces(true) } 90 | }, completion: nil) 91 | } 92 | } 93 | 94 | @objc private func didTapDone() { 95 | dismiss(isInteractive: false) 96 | } 97 | 98 | /// DismissのAnimation設定 99 | /// 100 | /// - Parameter isInteractive: InteractiveなDismissAnimationが必要な場合: true, 通常のDismissAnimationの場合: false 101 | private func dismiss(isInteractive: Bool) { 102 | if let delegate = navigationController?.transitioningDelegate as? SemiModalTransitioningDelegate { 103 | delegate.isInteractiveDismissal = isInteractive 104 | } 105 | self.navigationController?.dismiss(animated: true, completion: nil) 106 | } 107 | 108 | /// CollectionViewの縦方向スクロールをハンドリング 109 | /// 110 | /// - Parameter sender: UIPanGestureRecognizer 111 | @objc private func collectionViewDidDragging(_ sender: UIPanGestureRecognizer) { 112 | // CollectionViewが横方向にスクロールしている間はInteraction開始処理しない。 113 | if isScrollingCollectionView { return } 114 | handleTransitionGesture(sender, tableViewContentOffsetY: tableViewContentOffsetY) 115 | } 116 | 117 | /// CollectionViewの水平方向の位置を元に、中央付近にあるCollectionViewCellのindexを返却 118 | private func indexOfMajorCell() -> Int { 119 | let itemWidth = layout.pageWidth 120 | let proportionalOffset = layout.collectionView!.contentOffset.x / itemWidth 121 | let index = Int(round(proportionalOffset)) 122 | let numberOfItems = collectionView.numberOfItems(inSection: 0) 123 | let safeIndex = max(0, min(numberOfItems - 1, index)) 124 | return safeIndex 125 | } 126 | } 127 | 128 | // MARK: - UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout Methods 129 | extension CollectionSemiModalViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { 130 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 131 | return dataList.count 132 | } 133 | 134 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 135 | let cell = collectionView.dequeueReusableCell(with: CollectionSemiModalViewCell.self, for: indexPath) 136 | let baseRect = cell.frame 137 | let data = dataList[indexPath.row] 138 | cell.tag = indexPath.row 139 | cell.configure(headerHeight: cellHeaderHeight, data: data) 140 | cell.scrollViewDidScrollHandler = { [weak self] offsetY in 141 | self?.tableViewContentOffsetY = offsetY 142 | self?.transformCell(cell, baseRect: baseRect) 143 | } 144 | cell.tableViewDidSelectHandler = { [weak self] row in 145 | self?.transitionDetail(data: data, row: row) 146 | } 147 | cell.closeTapHandler = { [weak self] in 148 | self?.dismiss(isInteractive: true) 149 | } 150 | return cell 151 | } 152 | 153 | private func transitionDetail(data: ViewData, row: Int) { 154 | let vc = DetailViewController() 155 | vc.data = data 156 | vc.row = row 157 | vc.popActonHandler = { [weak self] in 158 | self?.switchDisplayNavigationBar(data: data) 159 | } 160 | navigationController?.pushViewController(vc, animated: true) 161 | } 162 | 163 | /// NavigationBarの表示制御 164 | /// 一定以上TableViewがスクロールされている場合にナビバーを表示する。 165 | private func switchDisplayNavigationBar(data: ViewData) { 166 | if let nv = navigationController { 167 | if cellHeaderHeight + visibleNaviBarOffsetY <= abs(tableViewContentOffsetY), nv.isNavigationBarHidden { 168 | title = data.title 169 | nv.navigationBar.barTintColor = data.color 170 | nv.setNavigationBarHidden(false, animated: true) 171 | } 172 | if abs(tableViewContentOffsetY) < cellHeaderHeight + visibleNaviBarOffsetY, !nv.isNavigationBarHidden { 173 | nv.setNavigationBarHidden(true, animated: true) 174 | } 175 | } 176 | } 177 | 178 | /// TableViewのスクロールに合わせて、画面内のCollectionViewCellのFrameを制御 179 | /// 180 | /// - Parameters: 181 | /// - cell: TableViewをスクロールしているCollectionViewCell 182 | /// - baseRect: CollectionViewCell初期位置のframe 183 | private func transformCell(_ cell: CollectionSemiModalViewCell, baseRect: CGRect) { 184 | switchDisplayNavigationBar(data: cell.data) 185 | // Cellの拡大中は横スクロールできないよう、TableViewのスクロール位置により制御 186 | collectionView.isScrollEnabled = tableViewContentOffsetY == 0 187 | 188 | let targetHeight = cellHeaderHeight + visibleNaviBarOffsetY // CellWidthが画面幅まで拡大するのが完了する高さ 189 | let verticalMovement = tableViewContentOffsetY / targetHeight 190 | let upwardMovement = fmaxf(Float(verticalMovement), 0.0) 191 | let upwardMovementPercent = fminf(upwardMovement, 1.0) 192 | let transformX = Float(view.frame.width - baseRect.size.width) * upwardMovementPercent 193 | let newPosX = Float(baseRect.origin.x) - transformX / 2 194 | let newWidth = baseRect.size.width + CGFloat(transformX) 195 | // 中央のCellを操作 196 | cell.frame = CGRect(x: CGFloat(newPosX), 197 | y: baseRect.origin.y, 198 | width: newWidth, 199 | height: baseRect.size.height) 200 | // 前後のCollectionViewCellを動かす 201 | collectionView.visibleCells.forEach { vCell in 202 | if vCell.tag < cell.tag { 203 | vCell.frame.origin.x = (baseRect.origin.x - layout.pageWidth) - CGFloat(transformX / 2) 204 | } else if cell.tag < vCell.tag { 205 | vCell.frame.origin.x = (baseRect.origin.x + layout.pageWidth) + CGFloat(transformX / 2) 206 | } 207 | } 208 | } 209 | 210 | func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 211 | let cell = collectionView.dequeueReusableCell(with: CollectionSemiModalViewCell.self, for: indexPath) 212 | cell.scrollToTop() 213 | } 214 | 215 | func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 216 | indexOfCellBeforeDragging = indexOfMajorCell() 217 | isScrollingCollectionView = true 218 | } 219 | 220 | 221 | /// CollectionViewの横スクロールを必ず中央で止まるように制御している 222 | /// ドラッグ完了位置(Cell半分以上スクロール)、もしくは、スワイプ時の速度のどちらかが該当条件を満たしていた場合に、前後のCollectionViewCellの中央までスクロールするよう制御している 223 | func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { 224 | isScrollingCollectionView = false 225 | // 横スクロールの速度閾値 226 | let swipeVelocityThreshold: CGFloat = 0.5 227 | 228 | // 横スクロールを現在の位置で止め、現在の横スクロール位置から中央に表示されるCollectionViewCellのindexを取得 229 | targetContentOffset.pointee = scrollView.contentOffset 230 | let indexOfMajorCell = self.indexOfMajorCell() 231 | 232 | let dataSourceCount = collectionView(collectionView!, numberOfItemsInSection: 0) 233 | // 横スクロールの速度が次のCellへスライドする閾値を超えているか(かつindexが範囲内) 234 | let hasEnoughVelocityToSlideToTheNextCell = indexOfCellBeforeDragging + 1 < dataSourceCount && velocity.x > swipeVelocityThreshold 235 | // 横スクロールの速度が前のCellへスライドする閾値を超えているか(かつindexが範囲内) 236 | let hasEnoughVelocityToSlideToThePrevCell = indexOfCellBeforeDragging - 1 >= 0 && velocity.x < -swipeVelocityThreshold 237 | // ドラッグ開始前のIndexと現在のIndexが一致しているか 238 | let majorCellIsTheCellBeforeDragging = indexOfMajorCell == indexOfCellBeforeDragging 239 | // スワイプ速度による前後Cellへのスクロールを行うか 240 | let didSwipeToSkipCell = majorCellIsTheCellBeforeDragging && (hasEnoughVelocityToSlideToTheNextCell || hasEnoughVelocityToSlideToThePrevCell) 241 | 242 | if didSwipeToSkipCell { 243 | // スワイプ速度による前後スクロール制御 244 | let snapToIndex = indexOfCellBeforeDragging + (hasEnoughVelocityToSlideToTheNextCell ? 1 : -1) 245 | let toValue = layout.pageWidth * CGFloat(snapToIndex) 246 | 247 | // usingSpringWithDamping: 1 振動なし、initialSpringVelocity: アニメーション初速をCollectionViewの横スクロール速度に設定 248 | UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity.x, options: .allowUserInteraction, animations: { 249 | scrollView.contentOffset = CGPoint(x: toValue, y: 0) 250 | scrollView.layoutIfNeeded() 251 | }, completion: { _ in 252 | self.selectedIndex = snapToIndex 253 | }) 254 | 255 | } else { 256 | // indexによるスクロール位置の更新 257 | let indexPath = IndexPath(row: indexOfMajorCell, section: 0) 258 | layout.collectionView!.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) 259 | selectedIndex = indexOfMajorCell 260 | } 261 | } 262 | } 263 | 264 | // MARK: - Make Self ViewController 265 | extension CollectionSemiModalViewController { 266 | static func make(dataList: [ViewData], selectedIndex: Int) -> CollectionSemiModalViewController { 267 | let sb = UIStoryboard(name: "CollectionSemiModalViewController", bundle: nil) 268 | let vc = sb.instantiateInitialViewController() as! CollectionSemiModalViewController 269 | vc.dataList = dataList 270 | vc.selectedIndex = selectedIndex 271 | return vc 272 | } 273 | } 274 | 275 | // MARK: - UIGestureRecognizerDelegate Methods 276 | extension CollectionSemiModalViewController: UIGestureRecognizerDelegate { 277 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 278 | return true 279 | } 280 | } 281 | 282 | /// CustomCollectionViewFlowLayout 283 | final class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout { 284 | let edgeSideMargin: CGFloat = 24 285 | 286 | private let kFlickVelocityThreshold: CGFloat = 0.2 287 | private let lineSpacing: CGFloat = 8 288 | 289 | var pageWidth: CGFloat { 290 | let width = collectionView!.frame.width - edgeSideMargin * 2 291 | return width + minimumLineSpacing 292 | } 293 | 294 | override func prepare() { 295 | super.prepare() 296 | guard let collectionView = collectionView else { return } 297 | let width = collectionView.frame.width - edgeSideMargin * 2 298 | let height = collectionView.frame.height 299 | itemSize = CGSize(width: width, height: height) 300 | minimumLineSpacing = lineSpacing 301 | sectionInset = UIEdgeInsets(top: 0, left: edgeSideMargin, bottom: 0, right: edgeSideMargin) 302 | scrollDirection = .horizontal 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/CollectionViewSemiModal/DetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailViewController.swift 3 | // CollectionViewSemiModalTransitioning 4 | // 5 | // Created by 本山洋一 on 2019/07/14. 6 | // Copyright © 2019 Yoichi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class DetailViewController: UIViewController { 12 | var data: ViewData! 13 | var row: Int! 14 | var popActonHandler: (() -> Void)? 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | title = String(format: "%@ No.%d", data.title, row) 19 | view.backgroundColor = .white 20 | 21 | navigationController?.navigationBar.barTintColor = data.color 22 | navigationController?.setNavigationBarHidden(false, animated: false) 23 | navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(didTapBack)) 24 | navigationItem.leftBarButtonItem?.tintColor = .white 25 | } 26 | 27 | @objc private func didTapBack() { 28 | popActonHandler?() 29 | navigationController?.popViewController(animated: true) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/CollectionViewSemiModal/TableViewTitleCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewTitleCell.swift 3 | // CollectionViewSemiModalTransitioning 4 | // 5 | // Created by 本山洋一 on 2019/07/07. 6 | // Copyright © 2019 Yoichi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TableViewTitleCell: UITableViewCell { 12 | 13 | var closeTapHandler: (() -> Void)? 14 | 15 | @IBOutlet weak var titleLabel: UILabel! 16 | @IBOutlet weak var baseView: UIView! 17 | @IBOutlet weak var colorView: UIView! 18 | 19 | override func awakeFromNib() { 20 | super.awakeFromNib() 21 | setupViews() 22 | } 23 | 24 | private func setupViews() { 25 | selectionStyle = .none 26 | backgroundColor = .clear 27 | baseView.layer.cornerRadius = 10 28 | baseView.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner] 29 | baseView.clipsToBounds = true 30 | } 31 | 32 | func configure(data: ViewData) { 33 | titleLabel.text = data.title 34 | colorView.backgroundColor = data.color 35 | } 36 | 37 | @IBAction func didTapClose(_ sender: Any) { 38 | closeTapHandler?() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/CollectionViewSemiModal/TableViewTitleCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 32 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/CustomTransition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomTransition.swift 3 | // CollectionViewSemiModalTransitioning 4 | // 5 | // Created by Yoichi on 2019/03/21. 6 | // Copyright © 2019 Yoichi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | class CustomTransition: NSObject, UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning{ 13 | // 14 | // class var sharedInstance : CustomTransition { 15 | // struct Static { 16 | // static let instance : CustomTransition = CustomTransition() 17 | // } 18 | // return Static.instance 19 | // } 20 | 21 | fileprivate var isPresent = false 22 | 23 | // MARK: - UIViewControllerTransitioningDelegate 24 | public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 25 | // 遷移時にTrasitionを担当する(UIViewControllerAnimatedTransitioningプロトコルを実装した)クラスを返す 26 | isPresent = true 27 | return self 28 | } 29 | 30 | // public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 31 | // // 復帰時にTrasitionを担当する(UIViewControllerAnimatedTransitioningプロトコルを実装した)クラスを返す 32 | // isPresent = false 33 | // return self 34 | // } 35 | 36 | // MARK: - UIViewControllerAnimatedTransitioning 37 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 38 | return 0.7 39 | } 40 | 41 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 42 | // if isPresent { 43 | presentTransition(transitionContext: transitionContext) 44 | // } else { 45 | // dissmissalTransition(transitionContext: transitionContext) 46 | // } 47 | } 48 | 49 | // 遷移時のTrastion処理 50 | func presentTransition(transitionContext: UIViewControllerContextTransitioning) { 51 | 52 | // 遷移元、遷移先及び、遷移コンテナの取得 53 | let firstViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as! CollectionSemiModalViewController 54 | let secondViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as! UINavigationController 55 | // let secondViewController = navigationController.viewControllers.first as! DetailViewController 56 | let containerView = transitionContext.containerView 57 | 58 | // 遷移元のセルの取得 59 | 60 | let cell:CollectionSemiModalViewCell = firstViewController.collectionView?.cellForItem(at: (firstViewController.collectionView?.indexPathsForSelectedItems?.first)!) as! CollectionSemiModalViewCell 61 | // 遷移元のセルのイメージビューからアニメーション用のビューを作成 62 | let animationView = UIView() 63 | animationView.addSubview(cell.tableView!) 64 | animationView.frame = containerView.convert(cell.contentView.frame, from: cell.contentView.superview) 65 | 66 | // 遷移元のセルのイメージビューを非表示にする 67 | cell.tableView.isHidden = true 68 | 69 | //遷移後のビューコントローラを、予め最後の位置まで移動完了させ非表示にする 70 | secondViewController.view.frame = transitionContext.finalFrame(for: secondViewController) 71 | secondViewController.view.alpha = 0 72 | // 遷移後のイメージは、アニメーションが完了するまで非表示にする 73 | // secondViewController.tableView.isHidden = true 74 | 75 | // 遷移コンテナに、遷移後のビューと、アニメーション用のビューを追加する 76 | containerView.addSubview(secondViewController.view) 77 | containerView.addSubview(animationView) 78 | 79 | UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: { 80 | // 遷移後のビューを徐々に表示する 81 | secondViewController.view.alpha = 1.0 82 | // アニメーション用のビューを、遷移後のイメージの位置までアニメーションする 83 | animationView.frame = UIApplication.shared.keyWindow!.frame 84 | }, completion: { 85 | finished in 86 | // 遷移後のイメージを表示する 87 | // secondViewController.tableView.isHidden = false 88 | // セルのイメージの非表示を元に戻す 89 | cell.tableView.isHidden = false 90 | 91 | // アニメーション用のビューを削除する 92 | cell.tableView.removeFromSuperview() 93 | animationView.removeFromSuperview() 94 | transitionContext.completeTransition(true) 95 | }) 96 | } 97 | 98 | // 復帰時のTrastion処理 99 | // func dissmissalTransition(transitionContext: UIViewControllerContextTransitioning) { 100 | // // 遷移元、遷移先及び、遷移コンテナの取得 101 | // let secondViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as! DetailViewController 102 | // let firstViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as! FirstViewController 103 | // let containerView = transitionContext.containerView 104 | // 105 | // // 遷移元のイメージビューからアニメーション用のビューを作成 106 | // let animationView = secondViewController.photoView.snapshotView(afterScreenUpdates: false) 107 | // animationView?.frame = containerView.convert(secondViewController.photoView.frame, from: secondViewController.photoView.superview) 108 | // // 遷移元のイメージを非表示にする 109 | // secondViewController.photoView.isHidden = true 110 | // 111 | // // 遷移先のセルを取得 112 | // let cell:CollectionViewCell = firstViewController.collectionView?.cellForItem(at: secondViewController.indexPath) as! CollectionViewCell 113 | // 114 | // // 遷移先のセルのイメージを非表示 115 | // cell.photoView.isHidden = true 116 | // 117 | // //遷移後のビューコントローラを、予め最後の位置まで移動完了させ非表示にする 118 | // firstViewController.view.frame = transitionContext.finalFrame(for: firstViewController) 119 | // 120 | // // 遷移コンテナに、遷移後のビューと、アニメーション用のビューを追加する 121 | // containerView.insertSubview(firstViewController.view, belowSubview: secondViewController.view) 122 | // containerView.addSubview(animationView!) 123 | // 124 | // UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: { 125 | // // 遷移元のビューを徐々に非表示にする 126 | // secondViewController.view.alpha = 0 127 | // // アニメーションビューは、遷移後のイメージの位置まで、アニメーションする 128 | // animationView?.frame = containerView.convert(cell.photoView.frame, from: cell.photoView.superview) 129 | // }, completion: { 130 | // finished in 131 | // // アニメーション用のビューを削除する 132 | // animationView?.removeFromSuperview() 133 | // // 遷移元のイメージの非表示を元に戻す 134 | // secondViewController.photoView.isHidden = false 135 | // // セルのイメージの非表示を元に戻す 136 | // cell.photoView.isHidden = false 137 | // transitionContext.completeTransition(true) 138 | // }) 139 | // } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // CollectionViewSemiModalTransitioning 4 | // 5 | // Created by Yoichi on 2019/03/09. 6 | // Copyright © 2019 Yoichi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // This Extensions https://qiita.com/tattn/items/dc7dfe2fceec00bb4ff7 12 | 13 | 14 | // MARK: - ClassName 15 | protocol ClassNameProtocol { 16 | static var className: String { get } 17 | var className: String { get } 18 | } 19 | 20 | extension ClassNameProtocol { 21 | static var className: String { 22 | return String(describing: self) 23 | } 24 | 25 | var className: String { 26 | return type(of: self).className 27 | } 28 | } 29 | 30 | extension NSObject: ClassNameProtocol {} 31 | 32 | // MARK: - UITableView 33 | extension UITableView { 34 | func register(cellType: UITableViewCell.Type, bundle: Bundle? = nil) { 35 | let className = cellType.className 36 | let nib = UINib(nibName: className, bundle: bundle) 37 | register(nib, forCellReuseIdentifier: className) 38 | } 39 | 40 | func register(cellTypes: [UITableViewCell.Type], bundle: Bundle? = nil) { 41 | cellTypes.forEach { register(cellType: $0, bundle: bundle) } 42 | } 43 | 44 | func dequeueReusableCell(with type: T.Type, for indexPath: IndexPath) -> T { 45 | return self.dequeueReusableCell(withIdentifier: type.className, for: indexPath) as! T 46 | } 47 | } 48 | 49 | // MARK: - UICollectionView 50 | extension UICollectionView { 51 | func register(cellType: UICollectionViewCell.Type, bundle: Bundle? = nil) { 52 | let className = cellType.className 53 | let nib = UINib(nibName: className, bundle: bundle) 54 | register(nib, forCellWithReuseIdentifier: className) 55 | } 56 | 57 | func register(cellTypes: [UICollectionViewCell.Type], bundle: Bundle? = nil) { 58 | cellTypes.forEach { register(cellType: $0, bundle: bundle) } 59 | } 60 | 61 | func register(reusableViewType: UICollectionReusableView.Type, 62 | ofKind kind: String = UICollectionView.elementKindSectionHeader, 63 | bundle: Bundle? = nil) { 64 | let className = reusableViewType.className 65 | let nib = UINib(nibName: className, bundle: bundle) 66 | register(nib, forSupplementaryViewOfKind: kind, withReuseIdentifier: className) 67 | } 68 | 69 | func register(reusableViewTypes: [UICollectionReusableView.Type], 70 | ofKind kind: String = UICollectionView.elementKindSectionHeader, 71 | bundle: Bundle? = nil) { 72 | reusableViewTypes.forEach { register(reusableViewType: $0, ofKind: kind, bundle: bundle) } 73 | } 74 | 75 | func dequeueReusableCell(with type: T.Type, 76 | for indexPath: IndexPath) -> T { 77 | return dequeueReusableCell(withReuseIdentifier: type.className, for: indexPath) as! T 78 | } 79 | 80 | func dequeueReusableView(with type: T.Type, 81 | for indexPath: IndexPath, 82 | ofKind kind: String = UICollectionView.elementKindSectionHeader) -> T { 83 | return dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: type.className, for: indexPath) as! T 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/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 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/SemiModalTransitioning/CollectionViewPresentAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewPresentAnimator.swift 3 | // CollectionViewSemiModalTransitioning 4 | // 5 | // Created by Yoichi on 2019/03/13. 6 | // Copyright © 2019 Yoichi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class CollectionViewPresentAnimator: NSObject, UIViewControllerAnimatedTransitioning { 12 | struct AnimationCellData { 13 | enum TargetType { 14 | case prev 15 | case target 16 | case next 17 | } 18 | 19 | let frame: CGRect 20 | let tag: Int 21 | let color: UIColor? 22 | 23 | init(cell: UICollectionViewCell, targetConvertFrame: CGRect, targetType: TargetType, cellSpacing: CGFloat) { 24 | switch targetType { 25 | case .target: 26 | frame = targetConvertFrame 27 | case .prev: 28 | frame = targetConvertFrame.offsetBy(dx: -targetConvertFrame.width - cellSpacing, dy: 0) 29 | case .next: 30 | frame = targetConvertFrame.offsetBy(dx: targetConvertFrame.width + cellSpacing, dy: 0) 31 | } 32 | tag = cell.tag 33 | color = cell.contentView.backgroundColor 34 | } 35 | } 36 | 37 | let isPresent: Bool 38 | 39 | init(isPresent: Bool) { 40 | self.isPresent = isPresent 41 | super.init() 42 | } 43 | 44 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 45 | return 0.3 46 | } 47 | 48 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 49 | if isPresent { 50 | presentTransition(using: transitionContext) 51 | } else { 52 | dismissalTransition(using: transitionContext) 53 | } 54 | } 55 | 56 | /// Present Transition Animator 57 | private func presentTransition(using transitionContext: UIViewControllerContextTransitioning) { 58 | let fromVC = transitionContext.viewController(forKey: .from) as! ViewController 59 | let toNC = transitionContext.viewController(forKey: .to) as! UINavigationController 60 | let toVC = toNC.viewControllers.first as! CollectionSemiModalViewController 61 | let finalToVCFrame = toVC.view.frame 62 | let containerView = transitionContext.containerView 63 | 64 | let selectedIndexPath = fromVC.collectionView.indexPathsForSelectedItems!.first! 65 | 66 | // 通常、このタイミングで取得できる[遷移先]のvisibleCellsは先頭2つのCellとなる。本来はタップしたCell+前後のCellがほしい。 67 | // snapshotView(afterScreenUpdates: true)によりスナップショットを取得することで、描画完了後のViewを生成するとともに目的のCellがvisibleCellsに格納されるようになる。 68 | if toVC.view.snapshotView(afterScreenUpdates: true) != nil { 69 | 70 | // 遷移元Cell関連 71 | // 遷移元Cellの座標をもとにアニメーション開始位置を決める。 72 | // 今回のアニメーションでは、遷移後の横並びに合わせ、アニメーション開始位置はタップされたCellの両脇を開始位置とする。 73 | // そのため、左右のセルが改行の関係で上下に位置する場合を考慮し、タップされたCellをもとにCGRectを生成する。 74 | // なお、遷移元のCell位置関係の取得はCollectionViewが一つであることを想定した実装であるため、複数ある場合はそれを考慮した実装が必要になる。 75 | 76 | // 遷移元Cellの生成 TargetCellの前後の存在有無を確認した上でCellを生成 77 | // cellForItemでは取得出来ない場合(画面外にあるなど)はUICollectionViewCellを生成している。 78 | // Frame指定する際、前後のCellはCollectionViewの改行を考慮し、TargetCellの左右に並ぶよう調整している 79 | let targetCell = fromVC.collectionView.cellForItem(at: selectedIndexPath)! 80 | let targetConvertFrame = targetCell.convert(targetCell.bounds, to: fromVC.view) 81 | // TODO: minimumLineSpacingはLayoutによって実際のCell間隔とズレが生じる。改行があるため、単純に前後のCell.originの比較では無いため今回は妥協している。 82 | let cellSpacing = (fromVC.collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumLineSpacing ?? 0 83 | 84 | var fromCellDataList: [AnimationCellData] = [] 85 | // PrevCell 86 | let prevTag = targetCell.tag - 1 87 | if 0 <= prevTag { 88 | let prevCell = fromVC.collectionView.cellForItem(at: IndexPath(row: prevTag, section: selectedIndexPath.section)) ?? UICollectionViewCell() 89 | prevCell.tag = prevTag 90 | fromCellDataList.append(AnimationCellData(cell: prevCell, targetConvertFrame: targetConvertFrame, targetType: .prev, cellSpacing: cellSpacing)) 91 | } 92 | // TargetCell 93 | fromCellDataList.append(AnimationCellData(cell: targetCell, targetConvertFrame: targetConvertFrame, targetType: .target, cellSpacing: cellSpacing)) 94 | // NextCell 95 | let nextTag = targetCell.tag + 1 96 | if nextTag < fromVC.collectionView.numberOfItems(inSection: selectedIndexPath.section) { 97 | let nextCell = fromVC.collectionView.cellForItem(at: IndexPath(row: nextTag, section: selectedIndexPath.section)) ?? UICollectionViewCell() 98 | nextCell.tag = nextTag 99 | fromCellDataList.append(AnimationCellData(cell: nextCell, targetConvertFrame: targetConvertFrame, targetType: .next, cellSpacing: cellSpacing)) 100 | } 101 | 102 | // 遷移先View関連 103 | let toCells = toVC.collectionView.visibleCells.compactMap { cell -> CollectionSemiModalViewCell? in 104 | guard let castCell = cell as? CollectionSemiModalViewCell else { return nil } 105 | castCell.switchTitleColorView(isClear: true) 106 | return castCell 107 | }.sorted(by:{ $0.tag < $1.tag }) 108 | 109 | let finalToCellsFramesWithTag = toCells.map { toCell -> (frame: CGRect, tag: Int) in 110 | let frame = toCell.convert(toCell.bounds, to: toVC.view) 111 | return (frame, toCell.tag) 112 | } 113 | let finalColorViewsFramesWithTag = toCells.map { toCell -> (frame: CGRect, tag: Int) in 114 | let frame = toCell.titleColorView?.convert(toCell.titleColorView?.bounds ?? .zero, to: toVC.view) ?? .zero 115 | return (frame, toCell.tag) 116 | } 117 | 118 | // AnimationView関連(toVCからSnapshotを作成) 119 | let animationToCells = toCells.map { toCell -> UIView in 120 | let snapshotCell = toCell.resizableSnapshotView(from: toCell.bounds, afterScreenUpdates: true, withCapInsets: .zero) ?? UIView() 121 | snapshotCell.tag = toCell.tag 122 | snapshotCell.frame = fromCellDataList.first(where: {$0.tag == toCell.tag})?.frame ?? .zero 123 | snapshotCell.alpha = 0 124 | return snapshotCell 125 | } 126 | let animationColorViews = fromCellDataList.map { tuple -> UIView in 127 | let view = UIView(frame: tuple.frame) 128 | view.tag = tuple.tag 129 | view.backgroundColor = tuple.color 130 | return view 131 | } 132 | 133 | // アニメーションに関してtoVCを主に操作しているが、containerViewへ追加するのはあくまでUINavigationControllerのViewである必要がある。 134 | // toVCでも遷移自体は完了するが、遷移後画面がちらついたり詳細への遷移がおかしくなることがある。 135 | toNC.view.isHidden = true 136 | containerView.addSubview(toNC.view) 137 | animationToCells.forEach { containerView.addSubview($0) } 138 | animationColorViews.forEach { containerView.addSubview($0) } 139 | 140 | UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options:[.curveEaseInOut], animations: { 141 | animationToCells.forEach { animationCell in 142 | animationCell.frame = finalToCellsFramesWithTag.first(where: { $0.tag == animationCell.tag })?.frame ?? .zero 143 | animationCell.alpha = 1 144 | } 145 | animationColorViews.forEach { animationColorView in 146 | animationColorView.frame = finalColorViewsFramesWithTag.first(where: { $0.tag == animationColorView.tag })?.frame ?? .zero 147 | } 148 | }, completion: { _ in 149 | toNC.view.isHidden = false 150 | toCells.forEach { $0.switchTitleColorView(isClear: false) } 151 | animationToCells.forEach { $0.removeFromSuperview() } 152 | animationColorViews.forEach { $0.removeFromSuperview() } 153 | transitionContext.completeTransition(true) 154 | }) 155 | } else { 156 | // アニメーションさせる遷移先のSnapshotが取得出来なかった場合 157 | containerView.addSubview(toVC.view) 158 | toVC.view.frame = CGRect(origin: CGPoint(x: 0, y: finalToVCFrame.size.height), size: finalToVCFrame.size) 159 | UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseOut], animations: { 160 | toVC.view.frame = finalToVCFrame 161 | }, completion: { _ in 162 | transitionContext.completeTransition(true) 163 | }) 164 | } 165 | } 166 | 167 | // Dismissal Transition Animator 168 | private func dismissalTransition(using transitionContext: UIViewControllerContextTransitioning) { 169 | let fromNC = transitionContext.viewController(forKey: .from) as! UINavigationController 170 | let fromVC = fromNC.viewControllers.first as! CollectionSemiModalViewController 171 | let toVC = transitionContext.viewController(forKey: .to) as! ViewController 172 | let containerView = transitionContext.containerView 173 | 174 | // 遷移元Cell関連 175 | let fromCells = fromVC.collectionView.visibleCells.compactMap { cell -> CollectionSemiModalViewCell? in 176 | guard let castCell = cell as? CollectionSemiModalViewCell else { return nil } 177 | castCell.switchTitleColorView(isClear: true) 178 | return castCell 179 | }.sorted(by:{ $0.tag < $1.tag }) 180 | 181 | // 遷移先Cell関連 182 | let targetToIndexPath = IndexPath(row: fromVC.selectedIndex, section: 0) 183 | if toVC.collectionView.cellForItem(at: targetToIndexPath) == nil { 184 | // 遷移先対象Cellが画面外にいる場合、画面内にスクロールさせる。更にスナップショットをとることでcellForItemメソッドで参照可能な状態にしている。 185 | toVC.collectionView.scrollToItem(at: targetToIndexPath, at: .centeredVertically, animated: false) 186 | toVC.view.snapshotView(afterScreenUpdates: true) 187 | } 188 | let targetToCell = toVC.collectionView.cellForItem(at: targetToIndexPath)! 189 | let targetConvertFrame = targetToCell.convert(targetToCell.bounds, to: toVC.view) 190 | // TODO: minimumLineSpacingはLayoutによって実際のCell間隔とズレが生じる。改行があるため、単純に前後のCell.originの比較では無いため今回は妥協している。 191 | let cellSpacing = (fromVC.collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumLineSpacing ?? 0 192 | var toCellDataList: [AnimationCellData] = [] 193 | // PrevCell 194 | let prevTag = targetToCell.tag - 1 195 | if 0 <= prevTag { 196 | let prevCell = toVC.collectionView.cellForItem(at: IndexPath(row: prevTag, section: targetToIndexPath.section)) ?? UICollectionViewCell() 197 | prevCell.tag = prevTag 198 | toCellDataList.append(AnimationCellData(cell: prevCell, targetConvertFrame: targetConvertFrame, targetType: .prev, cellSpacing: cellSpacing)) 199 | } 200 | // TargetCell 201 | toCellDataList.append(AnimationCellData(cell: targetToCell, targetConvertFrame: targetConvertFrame, targetType: .target, cellSpacing: cellSpacing)) 202 | // NextCell 203 | let nextTag = targetToCell.tag + 1 204 | if nextTag < toVC.collectionView.numberOfItems(inSection: targetToIndexPath.section) { 205 | let nextCell = toVC.collectionView.cellForItem(at: IndexPath(row: nextTag, section: targetToIndexPath.section)) ?? UICollectionViewCell() 206 | nextCell.tag = nextTag 207 | toCellDataList.append(AnimationCellData(cell: nextCell, targetConvertFrame: targetConvertFrame, targetType: .next, cellSpacing: cellSpacing)) 208 | } 209 | 210 | // AnimationView関連(fromVCからSnapshotを作成) 211 | let animationColorViews = toCellDataList.map { toCellData -> UIView in 212 | let view = fromCells.first(where: {$0.tag == toCellData.tag})?.titleColorView ?? UIView() 213 | let snapshotView = view.snapshotView(afterScreenUpdates: true) ?? UIView() 214 | snapshotView.frame = view.convert(view.bounds, to: toVC.view) 215 | snapshotView.tag = toCellData.tag 216 | snapshotView.backgroundColor = toCellData.color 217 | return snapshotView 218 | } 219 | let animationFromCells = toCellDataList.map { toCellData -> UIView in 220 | let cell = fromCells.first(where: {$0.tag == toCellData.tag}) ?? UIView() 221 | let snapshotCell = cell.snapshotView(afterScreenUpdates: true) ?? UIView() 222 | snapshotCell.frame = cell.convert(cell.bounds, to: toVC.view) 223 | snapshotCell.tag = cell.tag 224 | return snapshotCell 225 | } 226 | 227 | fromVC.view.isHidden = true 228 | animationFromCells.forEach { containerView.addSubview($0) } 229 | animationColorViews.forEach { containerView.addSubview($0) } 230 | 231 | UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options:[.curveEaseInOut], animations: { 232 | animationFromCells.forEach { animationCell in 233 | animationCell.frame = toCellDataList.first(where: { $0.tag == animationCell.tag })?.frame ?? .zero 234 | animationCell.alpha = 0 235 | } 236 | animationColorViews.forEach { animationColorView in 237 | animationColorView.frame = toCellDataList.first(where: { $0.tag == animationColorView.tag })?.frame ?? .zero 238 | } 239 | }, completion: { _ in 240 | fromVC.view.isHidden = false 241 | fromCells.forEach { $0.switchTitleColorView(isClear: false) } 242 | animationFromCells.forEach { $0.removeFromSuperview() } 243 | animationColorViews.forEach { $0.removeFromSuperview() } 244 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 245 | }) 246 | } 247 | } 248 | 249 | 250 | 251 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/SemiModalTransitioning/DismissalTransitionable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// DismissTransition制御関連プロトコル 4 | protocol DismissalTransitionable where Self: UIViewController { 5 | // Dismiss実行閾値(縦スクロール量の比率) 6 | var percentThreshold: CGFloat { get } 7 | // Dismiss実行速度閾値 8 | var shouldFinishVerocityY: CGFloat { get } 9 | // DismissTransitionの状態を保持 10 | var interactor: DismissalTransitioningInteractor { get } 11 | } 12 | 13 | extension DismissalTransitionable { 14 | /// Dismiss開始までの上下スワイプによるアニメーションと、Dismiss実行、中止を制御している 15 | /// 16 | /// - Parameters: 17 | /// - sender: CollectionViewのPanGestureRecognizer 18 | /// - tableViewContentOffsetY: CollectionViewCell内部のTableViewスクロール位置 19 | func handleTransitionGesture(_ sender: UIPanGestureRecognizer, tableViewContentOffsetY: CGFloat) { 20 | let translation = sender.translation(in: view) 21 | interactor.updateStateWithTranslation(y: translation.y, tableViewContentOffsetY: tableViewContentOffsetY) 22 | if interactor.shouldStopInteraction { return } 23 | 24 | // 上下スクロール量の割合を計算 25 | let dismisalOffsetY = translation.y - interactor.startInteractionTranslationY 26 | let verticalMovement = dismisalOffsetY / view.bounds.height 27 | let downwardMovement = fmaxf(Float(verticalMovement), 0.0) 28 | let downwardMovementPercent = fminf(downwardMovement, 1.0) 29 | let progress = CGFloat(downwardMovementPercent) 30 | 31 | // UIPanGestureRecognizer.state によるinteractor.stateの更新 32 | switch sender.state { 33 | case .changed: 34 | interactor.changed(by: dismisalOffsetY) 35 | if progress > percentThreshold || sender.velocity(in: view).y > shouldFinishVerocityY { 36 | // スクロール量の割合が閾値を超えた、もしくは、スクロール速度がしきい値を超えた場合 37 | interactor.state = .shouldFinish 38 | } else { 39 | interactor.state = .hasStarted 40 | } 41 | case .cancelled: 42 | interactor.reset() 43 | case .ended: 44 | // パンジェスチャー終了時のinteractor.stateによりDismiss実行有無を判定 45 | switch interactor.state { 46 | case .shouldFinish: 47 | interactor.finish() 48 | case .hasStarted, .none: 49 | interactor.reset() 50 | } 51 | default: 52 | break 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/SemiModalTransitioning/DismissalTransitioningInteractor.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class DismissalTransitioningInteractor { 4 | enum State { 5 | case none 6 | case hasStarted 7 | case shouldFinish 8 | } 9 | 10 | var state: State = .none 11 | 12 | var startInteractionTranslationY: CGFloat = 0 13 | 14 | var startHandler: (() -> Void)? 15 | 16 | var changedHandler: ((_ offsetY: CGFloat) -> Void)? 17 | 18 | var finishHandler: (() -> Void)? 19 | 20 | var resetHandler: (() -> Void)? 21 | 22 | var shouldStopInteraction: Bool { 23 | switch state { 24 | case .none: return true 25 | case .hasStarted, .shouldFinish: return false 26 | } 27 | } 28 | 29 | /// スクロール位置によるState更新 30 | /// 31 | /// - Parameters: 32 | /// - translationY: CollectionViewGestrueTranslationY 33 | /// - tableViewContentOffsetY: TableViewのScrollContentOffsetY ドラッグによる更新されたOffsetY (慣性スクロールは含まない) 34 | func updateStateWithTranslation(y translationY: CGFloat, tableViewContentOffsetY: CGFloat) { 35 | switch state { 36 | case .none: 37 | if tableViewContentOffsetY <= 0 { 38 | // Interaction開始できる状態になったら、現在のCollectionViewGestureのtranslationYを記憶し、Interaction中のstateへ更新 39 | // startInteractionTranslationYを記憶することで、TableViewスクロール中から連続的にDismissアニメーションにつなげることができる 40 | startInteractionTranslationY = translationY 41 | state = .hasStarted 42 | startHandler?() 43 | } 44 | case .hasStarted, .shouldFinish: 45 | // 初期位置よりも上へのスクロールの場合、インタラクション終了 46 | if translationY - startInteractionTranslationY < 0 { 47 | state = .none 48 | reset() 49 | } 50 | } 51 | } 52 | 53 | func changed(by offsetY: CGFloat) { 54 | changedHandler?(offsetY) 55 | } 56 | 57 | func finish() { 58 | finishHandler?() 59 | } 60 | 61 | func reset() { 62 | state = .none 63 | startInteractionTranslationY = 0 64 | resetHandler?() 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/SemiModalTransitioning/ModalPresentationController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ModalPresentationController: UIPresentationController { 4 | private let overlayView = UIView() 5 | 6 | override func presentationTransitionWillBegin() { 7 | super.presentationTransitionWillBegin() 8 | 9 | overlayView.frame = containerView!.bounds 10 | overlayView.backgroundColor = .black 11 | overlayView.alpha = 0.0 12 | containerView!.insertSubview(overlayView, at: 0) 13 | presentedViewController.transitionCoordinator?.animate(alongsideTransition: { [unowned self] _ in 14 | self.overlayView.alpha = 0.5 15 | }) 16 | } 17 | 18 | override func dismissalTransitionWillBegin() { 19 | super.dismissalTransitionWillBegin() 20 | 21 | presentedViewController.transitionCoordinator?.animate(alongsideTransition: { [unowned self] _ in 22 | self.overlayView.alpha = 0.0 23 | }) 24 | } 25 | 26 | override func dismissalTransitionDidEnd(_ completed: Bool) { 27 | super.dismissalTransitionDidEnd(completed) 28 | 29 | if completed { 30 | overlayView.removeFromSuperview() 31 | } 32 | } 33 | 34 | override var frameOfPresentedViewInContainerView: CGRect { 35 | return containerView!.bounds 36 | } 37 | 38 | override func containerViewWillLayoutSubviews() { 39 | super.containerViewWillLayoutSubviews() 40 | 41 | overlayView.frame = containerView!.bounds 42 | presentedView!.frame = frameOfPresentedViewInContainerView 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/SemiModalTransitioning/SemiModalTransitioningDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverCurrentTransitioning.swift 3 | // CollectionViewSemiModalTransitioning 4 | // 5 | // Created by 本山洋一 on 2019/06/22. 6 | // Copyright © 2019 Yoichi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class SemiModalTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate { 12 | var isInteractiveDismissal: Bool = true 13 | 14 | func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { 15 | return ModalPresentationController(presentedViewController: presented, presenting: presenting) 16 | } 17 | 18 | func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 19 | return CollectionViewPresentAnimator(isPresent: true) 20 | } 21 | 22 | func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 23 | return isInteractiveDismissal ? CollectionViewPresentAnimator(isPresent: false) : nil 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /CollectionViewSemiModalTransitioning/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // CollectionViewSemiModalTransitioning 4 | // 5 | // Created by Yoichi on 2019/02/12. 6 | // Copyright © 2019 Yoichi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | @IBOutlet weak var collectionView: UICollectionView! 14 | 15 | private var dataList: [ViewData] = [] 16 | private let customTransition = SemiModalTransitioningDelegate() 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | setupViews() 21 | generateDataList() 22 | } 23 | 24 | func setupViews() { 25 | collectionView.delegate = self 26 | collectionView.dataSource = self 27 | collectionView.register(cellType: UICollectionViewCell.self) 28 | 29 | let layout = UICollectionViewFlowLayout() 30 | layout.sectionInset = UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15) 31 | collectionView.collectionViewLayout = layout 32 | } 33 | 34 | func generateDataList() { 35 | dataList = [ 36 | ViewData(color: #colorLiteral(red: 1, green: 0.1857388616, blue: 0.5733950138, alpha: 1), title: "Strawberry"), 37 | ViewData(color: #colorLiteral(red: 0, green: 0.9914394021, blue: 1, alpha: 1), title: "Turquoise"), 38 | ViewData(color: #colorLiteral(red: 0, green: 0.9810667634, blue: 0.5736914277, alpha: 1), title: "SeaFoam"), 39 | ViewData(color: #colorLiteral(red: 1, green: 0.1491314173, blue: 0, alpha: 1), title: "Maraschino"), 40 | ViewData(color: #colorLiteral(red: 1, green: 0.8323456645, blue: 0.4732058644, alpha: 1), title: "Cantaloupe"), 41 | ViewData(color: #colorLiteral(red: 0, green: 0.5898008943, blue: 1, alpha: 1), title: "Aqua"), 42 | ViewData(color: #colorLiteral(red: 1, green: 0.2527923882, blue: 1, alpha: 1), title: "Magenta"), 43 | ViewData(color: #colorLiteral(red: 1, green: 0.5781051517, blue: 0, alpha: 1), title: "Tangerine"), 44 | ViewData(color: #colorLiteral(red: 0.8446564078, green: 0.5145705342, blue: 1, alpha: 1), title: "Lavender"), 45 | ViewData(color: #colorLiteral(red: 0, green: 0.5628422499, blue: 0.3188166618, alpha: 1), title: "Moss"), 46 | ViewData(color: #colorLiteral(red: 1, green: 0.4932718873, blue: 0.4739984274, alpha: 1), title: "Salmon"), 47 | ViewData(color: #colorLiteral(red: 0.4500938654, green: 0.9813225865, blue: 0.4743030667, alpha: 1), title: "Flora"), 48 | ] 49 | } 50 | } 51 | 52 | extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { 53 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 54 | return dataList.count 55 | } 56 | 57 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 58 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) 59 | cell.contentView.backgroundColor = dataList[indexPath.row].color 60 | cell.tag = indexPath.row 61 | return cell 62 | } 63 | 64 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 65 | let horizontalSpace : CGFloat = 20 66 | let cellSize : CGFloat = view.bounds.width / 2 - horizontalSpace 67 | return CGSize(width: cellSize, height: cellSize) 68 | } 69 | 70 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 71 | 72 | let vc = CollectionSemiModalViewController.make(dataList: dataList, selectedIndex: indexPath.row) 73 | let nv = UINavigationController(rootViewController: vc) 74 | nv.transitioningDelegate = customTransition 75 | nv.modalPresentationStyle = .custom 76 | present(nv, animated: true, completion: nil) 77 | } 78 | } 79 | 80 | struct ViewData { 81 | let color: UIColor 82 | let title: String 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CollectionViewSemiModalTransitioning 2 | This sample is SemiModal Transitioning from collectionView like Apple Books App. 3 | 4 | ![demo](https://github.com/iincho/CollectionViewSemiModalTransitioning/blob/media/Media/Gif/50Percent/demo.gif) 5 | --------------------------------------------------------------------------------