├── .github └── FUNDING.yml ├── .gitignore ├── FluidPhoto.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── FluidPhoto ├── Animation │ ├── ZoomAnimator.swift │ ├── ZoomDismissalInteractionController.swift │ └── ZoomTransitionController.swift ├── AppDelegate.swift ├── Assets.xcassets │ ├── 1.imageset │ │ ├── Contents.json │ │ └── s__satiqdkzeq-clem-onojeghuo.jpg │ ├── 10.imageset │ │ ├── Contents.json │ │ └── s_jktv__bqmaa-brooke-lark.jpg │ ├── 11.imageset │ │ ├── Contents.json │ │ └── s_ko1g3nera2o-anchor-lee.jpg │ ├── 12.imageset │ │ ├── Contents.json │ │ └── s_kujkutxr0z4-ridham-nagralawala.jpg │ ├── 13.imageset │ │ ├── Contents.json │ │ └── s_lzoij-a4u-anchor-lee.jpg │ ├── 14.imageset │ │ ├── Contents.json │ │ └── s_nwfuayecnus-ethan-robertson.jpg │ ├── 15.imageset │ │ ├── Contents.json │ │ └── s_ooumcn6jsoq-jay-cee.jpg │ ├── 16.imageset │ │ ├── Contents.json │ │ └── s_qtggylug6cw-saso-tusar.jpg │ ├── 17.imageset │ │ ├── Contents.json │ │ └── s_vwbmxol3h8s-joel-filipe.jpg │ ├── 18.imageset │ │ ├── Contents.json │ │ └── s_xn_crzwxgdm-andreas-p.jpg │ ├── 2.imageset │ │ ├── Contents.json │ │ └── s_1zmk5bezlyc-i-m-priscilla.jpg │ ├── 3.imageset │ │ ├── Contents.json │ │ └── s_2tcy8pqfxse-mpho-mojapelo.jpg │ ├── 4.imageset │ │ ├── Contents.json │ │ └── s_aez1-a7ys7s-mario-klassen.jpg │ ├── 5.imageset │ │ ├── Contents.json │ │ └── s_earyikg21d4-maja-petric.jpg │ ├── 6.imageset │ │ ├── Contents.json │ │ └── s_ey0k7-kqsqy-scott-webb.jpg │ ├── 7.imageset │ │ ├── Contents.json │ │ └── s_g3eh_ge1pl4-jaromir-kavan.jpg │ ├── 8.imageset │ │ ├── Contents.json │ │ └── s_hqnlazeunhi-sime-basioli.jpg │ ├── 9.imageset │ │ ├── Contents.json │ │ └── s_jjrjx9mq6q0-clem-onojeghuo.jpg │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── View │ └── PhotoCollectionViewCell.swift └── ViewController │ ├── PhotoPageContainerViewController.swift │ ├── PhotoZoomViewController.swift │ └── ViewController.swift ├── LICENSE ├── README.md └── Screenshots └── fluidphoto.gif /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [masamichiueta] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## Build generated 8 | build/ 9 | DerivedData/ 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | 22 | ## Other 23 | *.moved-aside 24 | *.xcuserstate 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /FluidPhoto.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 247378861E0D6302000F7F2A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 247378851E0D6302000F7F2A /* AppDelegate.swift */; }; 11 | 247378881E0D6302000F7F2A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 247378871E0D6302000F7F2A /* ViewController.swift */; }; 12 | 2473788B1E0D6302000F7F2A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 247378891E0D6302000F7F2A /* Main.storyboard */; }; 13 | 2473788D1E0D6302000F7F2A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2473788C1E0D6302000F7F2A /* Assets.xcassets */; }; 14 | 247378901E0D6302000F7F2A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2473788E1E0D6302000F7F2A /* LaunchScreen.storyboard */; }; 15 | 247378981E0D6369000F7F2A /* PhotoCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 247378971E0D6369000F7F2A /* PhotoCollectionViewCell.swift */; }; 16 | 2473789A1E0D656D000F7F2A /* PhotoZoomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 247378991E0D656D000F7F2A /* PhotoZoomViewController.swift */; }; 17 | 2473789C1E0D6591000F7F2A /* PhotoPageContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2473789B1E0D6591000F7F2A /* PhotoPageContainerViewController.swift */; }; 18 | 2473789E1E0D6DB5000F7F2A /* ZoomAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2473789D1E0D6DB5000F7F2A /* ZoomAnimator.swift */; }; 19 | 24C6D4721E14196A00F01715 /* ZoomDismissalInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24C6D4711E14196A00F01715 /* ZoomDismissalInteractionController.swift */; }; 20 | 24C6D4751E14FC6F00F01715 /* ZoomTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24C6D4741E14FC6F00F01715 /* ZoomTransitionController.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXFileReference section */ 24 | 247378821E0D6302000F7F2A /* FluidPhoto.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FluidPhoto.app; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | 247378851E0D6302000F7F2A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 26 | 247378871E0D6302000F7F2A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 27 | 2473788A1E0D6302000F7F2A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 28 | 2473788C1E0D6302000F7F2A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 29 | 2473788F1E0D6302000F7F2A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 30 | 247378911E0D6302000F7F2A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 31 | 247378971E0D6369000F7F2A /* PhotoCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionViewCell.swift; sourceTree = ""; }; 32 | 247378991E0D656D000F7F2A /* PhotoZoomViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoZoomViewController.swift; sourceTree = ""; }; 33 | 2473789B1E0D6591000F7F2A /* PhotoPageContainerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoPageContainerViewController.swift; sourceTree = ""; }; 34 | 2473789D1E0D6DB5000F7F2A /* ZoomAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomAnimator.swift; sourceTree = ""; }; 35 | 24C6D4711E14196A00F01715 /* ZoomDismissalInteractionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomDismissalInteractionController.swift; sourceTree = ""; }; 36 | 24C6D4741E14FC6F00F01715 /* ZoomTransitionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomTransitionController.swift; sourceTree = ""; }; 37 | /* End PBXFileReference section */ 38 | 39 | /* Begin PBXFrameworksBuildPhase section */ 40 | 2473787F1E0D6302000F7F2A /* Frameworks */ = { 41 | isa = PBXFrameworksBuildPhase; 42 | buildActionMask = 2147483647; 43 | files = ( 44 | ); 45 | runOnlyForDeploymentPostprocessing = 0; 46 | }; 47 | /* End PBXFrameworksBuildPhase section */ 48 | 49 | /* Begin PBXGroup section */ 50 | 244C0A3520A7F831002DCB64 /* Animation */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | 2473789D1E0D6DB5000F7F2A /* ZoomAnimator.swift */, 54 | 24C6D4711E14196A00F01715 /* ZoomDismissalInteractionController.swift */, 55 | 24C6D4741E14FC6F00F01715 /* ZoomTransitionController.swift */, 56 | ); 57 | path = Animation; 58 | sourceTree = ""; 59 | }; 60 | 244C0A3620A7F83D002DCB64 /* ViewController */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 2473789B1E0D6591000F7F2A /* PhotoPageContainerViewController.swift */, 64 | 247378991E0D656D000F7F2A /* PhotoZoomViewController.swift */, 65 | 247378871E0D6302000F7F2A /* ViewController.swift */, 66 | ); 67 | path = ViewController; 68 | sourceTree = ""; 69 | }; 70 | 244C0A3720A7F847002DCB64 /* View */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 247378971E0D6369000F7F2A /* PhotoCollectionViewCell.swift */, 74 | ); 75 | path = View; 76 | sourceTree = ""; 77 | }; 78 | 247378791E0D6302000F7F2A = { 79 | isa = PBXGroup; 80 | children = ( 81 | 247378841E0D6302000F7F2A /* FluidPhoto */, 82 | 247378831E0D6302000F7F2A /* Products */, 83 | ); 84 | sourceTree = ""; 85 | }; 86 | 247378831E0D6302000F7F2A /* Products */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 247378821E0D6302000F7F2A /* FluidPhoto.app */, 90 | ); 91 | name = Products; 92 | sourceTree = ""; 93 | }; 94 | 247378841E0D6302000F7F2A /* FluidPhoto */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 244C0A3520A7F831002DCB64 /* Animation */, 98 | 247378851E0D6302000F7F2A /* AppDelegate.swift */, 99 | 2473788C1E0D6302000F7F2A /* Assets.xcassets */, 100 | 247378911E0D6302000F7F2A /* Info.plist */, 101 | 2473788E1E0D6302000F7F2A /* LaunchScreen.storyboard */, 102 | 247378891E0D6302000F7F2A /* Main.storyboard */, 103 | 244C0A3720A7F847002DCB64 /* View */, 104 | 244C0A3620A7F83D002DCB64 /* ViewController */, 105 | ); 106 | path = FluidPhoto; 107 | sourceTree = ""; 108 | }; 109 | /* End PBXGroup section */ 110 | 111 | /* Begin PBXNativeTarget section */ 112 | 247378811E0D6302000F7F2A /* FluidPhoto */ = { 113 | isa = PBXNativeTarget; 114 | buildConfigurationList = 247378941E0D6302000F7F2A /* Build configuration list for PBXNativeTarget "FluidPhoto" */; 115 | buildPhases = ( 116 | 2473787E1E0D6302000F7F2A /* Sources */, 117 | 2473787F1E0D6302000F7F2A /* Frameworks */, 118 | 247378801E0D6302000F7F2A /* Resources */, 119 | ); 120 | buildRules = ( 121 | ); 122 | dependencies = ( 123 | ); 124 | name = FluidPhoto; 125 | productName = FluidPhoto; 126 | productReference = 247378821E0D6302000F7F2A /* FluidPhoto.app */; 127 | productType = "com.apple.product-type.application"; 128 | }; 129 | /* End PBXNativeTarget section */ 130 | 131 | /* Begin PBXProject section */ 132 | 2473787A1E0D6302000F7F2A /* Project object */ = { 133 | isa = PBXProject; 134 | attributes = { 135 | LastSwiftUpdateCheck = 0820; 136 | LastUpgradeCheck = 1420; 137 | ORGANIZATIONNAME = "Masmichi Ueta"; 138 | TargetAttributes = { 139 | 247378811E0D6302000F7F2A = { 140 | CreatedOnToolsVersion = 8.2.1; 141 | DevelopmentTeam = 39FM6RSP8J; 142 | LastSwiftMigration = 0930; 143 | ProvisioningStyle = Automatic; 144 | }; 145 | }; 146 | }; 147 | buildConfigurationList = 2473787D1E0D6302000F7F2A /* Build configuration list for PBXProject "FluidPhoto" */; 148 | compatibilityVersion = "Xcode 3.2"; 149 | developmentRegion = en; 150 | hasScannedForEncodings = 0; 151 | knownRegions = ( 152 | en, 153 | Base, 154 | ); 155 | mainGroup = 247378791E0D6302000F7F2A; 156 | productRefGroup = 247378831E0D6302000F7F2A /* Products */; 157 | projectDirPath = ""; 158 | projectRoot = ""; 159 | targets = ( 160 | 247378811E0D6302000F7F2A /* FluidPhoto */, 161 | ); 162 | }; 163 | /* End PBXProject section */ 164 | 165 | /* Begin PBXResourcesBuildPhase section */ 166 | 247378801E0D6302000F7F2A /* Resources */ = { 167 | isa = PBXResourcesBuildPhase; 168 | buildActionMask = 2147483647; 169 | files = ( 170 | 247378901E0D6302000F7F2A /* LaunchScreen.storyboard in Resources */, 171 | 2473788D1E0D6302000F7F2A /* Assets.xcassets in Resources */, 172 | 2473788B1E0D6302000F7F2A /* Main.storyboard in Resources */, 173 | ); 174 | runOnlyForDeploymentPostprocessing = 0; 175 | }; 176 | /* End PBXResourcesBuildPhase section */ 177 | 178 | /* Begin PBXSourcesBuildPhase section */ 179 | 2473787E1E0D6302000F7F2A /* Sources */ = { 180 | isa = PBXSourcesBuildPhase; 181 | buildActionMask = 2147483647; 182 | files = ( 183 | 2473789C1E0D6591000F7F2A /* PhotoPageContainerViewController.swift in Sources */, 184 | 247378881E0D6302000F7F2A /* ViewController.swift in Sources */, 185 | 247378861E0D6302000F7F2A /* AppDelegate.swift in Sources */, 186 | 2473789A1E0D656D000F7F2A /* PhotoZoomViewController.swift in Sources */, 187 | 247378981E0D6369000F7F2A /* PhotoCollectionViewCell.swift in Sources */, 188 | 24C6D4751E14FC6F00F01715 /* ZoomTransitionController.swift in Sources */, 189 | 24C6D4721E14196A00F01715 /* ZoomDismissalInteractionController.swift in Sources */, 190 | 2473789E1E0D6DB5000F7F2A /* ZoomAnimator.swift in Sources */, 191 | ); 192 | runOnlyForDeploymentPostprocessing = 0; 193 | }; 194 | /* End PBXSourcesBuildPhase section */ 195 | 196 | /* Begin PBXVariantGroup section */ 197 | 247378891E0D6302000F7F2A /* Main.storyboard */ = { 198 | isa = PBXVariantGroup; 199 | children = ( 200 | 2473788A1E0D6302000F7F2A /* Base */, 201 | ); 202 | name = Main.storyboard; 203 | sourceTree = ""; 204 | }; 205 | 2473788E1E0D6302000F7F2A /* LaunchScreen.storyboard */ = { 206 | isa = PBXVariantGroup; 207 | children = ( 208 | 2473788F1E0D6302000F7F2A /* Base */, 209 | ); 210 | name = LaunchScreen.storyboard; 211 | sourceTree = ""; 212 | }; 213 | /* End PBXVariantGroup section */ 214 | 215 | /* Begin XCBuildConfiguration section */ 216 | 247378921E0D6302000F7F2A /* Debug */ = { 217 | isa = XCBuildConfiguration; 218 | buildSettings = { 219 | ALWAYS_SEARCH_USER_PATHS = NO; 220 | CLANG_ANALYZER_NONNULL = YES; 221 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 222 | CLANG_CXX_LIBRARY = "libc++"; 223 | CLANG_ENABLE_MODULES = YES; 224 | CLANG_ENABLE_OBJC_ARC = YES; 225 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 226 | CLANG_WARN_BOOL_CONVERSION = YES; 227 | CLANG_WARN_COMMA = YES; 228 | CLANG_WARN_CONSTANT_CONVERSION = YES; 229 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 230 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 231 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 232 | CLANG_WARN_EMPTY_BODY = YES; 233 | CLANG_WARN_ENUM_CONVERSION = YES; 234 | CLANG_WARN_INFINITE_RECURSION = YES; 235 | CLANG_WARN_INT_CONVERSION = YES; 236 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 237 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 238 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 239 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 240 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 241 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 242 | CLANG_WARN_STRICT_PROTOTYPES = YES; 243 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 244 | CLANG_WARN_UNREACHABLE_CODE = YES; 245 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 246 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 247 | COPY_PHASE_STRIP = NO; 248 | DEBUG_INFORMATION_FORMAT = dwarf; 249 | ENABLE_STRICT_OBJC_MSGSEND = YES; 250 | ENABLE_TESTABILITY = YES; 251 | GCC_C_LANGUAGE_STANDARD = gnu99; 252 | GCC_DYNAMIC_NO_PIC = NO; 253 | GCC_NO_COMMON_BLOCKS = YES; 254 | GCC_OPTIMIZATION_LEVEL = 0; 255 | GCC_PREPROCESSOR_DEFINITIONS = ( 256 | "DEBUG=1", 257 | "$(inherited)", 258 | ); 259 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 260 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 261 | GCC_WARN_UNDECLARED_SELECTOR = YES; 262 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 263 | GCC_WARN_UNUSED_FUNCTION = YES; 264 | GCC_WARN_UNUSED_VARIABLE = YES; 265 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 266 | MTL_ENABLE_DEBUG_INFO = YES; 267 | ONLY_ACTIVE_ARCH = YES; 268 | SDKROOT = iphoneos; 269 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 270 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 271 | TARGETED_DEVICE_FAMILY = "1,2"; 272 | }; 273 | name = Debug; 274 | }; 275 | 247378931E0D6302000F7F2A /* Release */ = { 276 | isa = XCBuildConfiguration; 277 | buildSettings = { 278 | ALWAYS_SEARCH_USER_PATHS = NO; 279 | CLANG_ANALYZER_NONNULL = YES; 280 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 281 | CLANG_CXX_LIBRARY = "libc++"; 282 | CLANG_ENABLE_MODULES = YES; 283 | CLANG_ENABLE_OBJC_ARC = YES; 284 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 285 | CLANG_WARN_BOOL_CONVERSION = YES; 286 | CLANG_WARN_COMMA = YES; 287 | CLANG_WARN_CONSTANT_CONVERSION = YES; 288 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 289 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 290 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 291 | CLANG_WARN_EMPTY_BODY = YES; 292 | CLANG_WARN_ENUM_CONVERSION = YES; 293 | CLANG_WARN_INFINITE_RECURSION = YES; 294 | CLANG_WARN_INT_CONVERSION = YES; 295 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 296 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 297 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 298 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 299 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 300 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 301 | CLANG_WARN_STRICT_PROTOTYPES = YES; 302 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 303 | CLANG_WARN_UNREACHABLE_CODE = YES; 304 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 305 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 306 | COPY_PHASE_STRIP = NO; 307 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 308 | ENABLE_NS_ASSERTIONS = NO; 309 | ENABLE_STRICT_OBJC_MSGSEND = YES; 310 | GCC_C_LANGUAGE_STANDARD = gnu99; 311 | GCC_NO_COMMON_BLOCKS = YES; 312 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 313 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 314 | GCC_WARN_UNDECLARED_SELECTOR = YES; 315 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 316 | GCC_WARN_UNUSED_FUNCTION = YES; 317 | GCC_WARN_UNUSED_VARIABLE = YES; 318 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 319 | MTL_ENABLE_DEBUG_INFO = NO; 320 | SDKROOT = iphoneos; 321 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 322 | TARGETED_DEVICE_FAMILY = "1,2"; 323 | VALIDATE_PRODUCT = YES; 324 | }; 325 | name = Release; 326 | }; 327 | 247378951E0D6302000F7F2A /* Debug */ = { 328 | isa = XCBuildConfiguration; 329 | buildSettings = { 330 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 331 | DEVELOPMENT_TEAM = 39FM6RSP8J; 332 | INFOPLIST_FILE = FluidPhoto/Info.plist; 333 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 334 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 335 | PRODUCT_BUNDLE_IDENTIFIER = me.masamichi.FluidPhoto; 336 | PRODUCT_NAME = "$(TARGET_NAME)"; 337 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 338 | SWIFT_VERSION = 5.0; 339 | }; 340 | name = Debug; 341 | }; 342 | 247378961E0D6302000F7F2A /* Release */ = { 343 | isa = XCBuildConfiguration; 344 | buildSettings = { 345 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 346 | DEVELOPMENT_TEAM = 39FM6RSP8J; 347 | INFOPLIST_FILE = FluidPhoto/Info.plist; 348 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 349 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 350 | PRODUCT_BUNDLE_IDENTIFIER = me.masamichi.FluidPhoto; 351 | PRODUCT_NAME = "$(TARGET_NAME)"; 352 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 353 | SWIFT_VERSION = 5.0; 354 | }; 355 | name = Release; 356 | }; 357 | /* End XCBuildConfiguration section */ 358 | 359 | /* Begin XCConfigurationList section */ 360 | 2473787D1E0D6302000F7F2A /* Build configuration list for PBXProject "FluidPhoto" */ = { 361 | isa = XCConfigurationList; 362 | buildConfigurations = ( 363 | 247378921E0D6302000F7F2A /* Debug */, 364 | 247378931E0D6302000F7F2A /* Release */, 365 | ); 366 | defaultConfigurationIsVisible = 0; 367 | defaultConfigurationName = Release; 368 | }; 369 | 247378941E0D6302000F7F2A /* Build configuration list for PBXNativeTarget "FluidPhoto" */ = { 370 | isa = XCConfigurationList; 371 | buildConfigurations = ( 372 | 247378951E0D6302000F7F2A /* Debug */, 373 | 247378961E0D6302000F7F2A /* Release */, 374 | ); 375 | defaultConfigurationIsVisible = 0; 376 | defaultConfigurationName = Release; 377 | }; 378 | /* End XCConfigurationList section */ 379 | }; 380 | rootObject = 2473787A1E0D6302000F7F2A /* Project object */; 381 | } 382 | -------------------------------------------------------------------------------- /FluidPhoto.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FluidPhoto.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /FluidPhoto/Animation/ZoomAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZoomAnimator.swift 3 | // FluidPhoto 4 | // 5 | // Created by Masamichi Ueta on 2016/12/23. 6 | // Copyright © 2016 Masmichi Ueta. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ZoomAnimatorDelegate: AnyObject { 12 | func transitionWillStartWith(zoomAnimator: ZoomAnimator) 13 | func transitionDidEndWith(zoomAnimator: ZoomAnimator) 14 | func referenceImageView(for zoomAnimator: ZoomAnimator) -> UIImageView? 15 | func referenceImageViewFrameInTransitioningView(for zoomAnimator: ZoomAnimator) -> CGRect? 16 | } 17 | 18 | class ZoomAnimator: NSObject { 19 | 20 | weak var fromDelegate: ZoomAnimatorDelegate? 21 | weak var toDelegate: ZoomAnimatorDelegate? 22 | 23 | var transitionImageView: UIImageView? 24 | var isPresenting: Bool = true 25 | 26 | fileprivate func animateZoomInTransition(using transitionContext: UIViewControllerContextTransitioning) { 27 | 28 | let containerView = transitionContext.containerView 29 | 30 | guard let toVC = transitionContext.viewController(forKey: .to), 31 | let fromVC = transitionContext.viewController(forKey: .from), 32 | let fromReferenceImageView = self.fromDelegate?.referenceImageView(for: self), 33 | let toReferenceImageView = self.toDelegate?.referenceImageView(for: self), 34 | let fromReferenceImageViewFrame = self.fromDelegate?.referenceImageViewFrameInTransitioningView(for: self) 35 | else { 36 | return 37 | } 38 | 39 | self.fromDelegate?.transitionWillStartWith(zoomAnimator: self) 40 | self.toDelegate?.transitionWillStartWith(zoomAnimator: self) 41 | 42 | toVC.view.alpha = 0 43 | toReferenceImageView.isHidden = true 44 | containerView.addSubview(toVC.view) 45 | 46 | let referenceImage = fromReferenceImageView.image! 47 | 48 | if self.transitionImageView == nil { 49 | let transitionImageView = UIImageView(image: referenceImage) 50 | transitionImageView.contentMode = .scaleAspectFill 51 | transitionImageView.clipsToBounds = true 52 | transitionImageView.frame = fromReferenceImageViewFrame 53 | self.transitionImageView = transitionImageView 54 | containerView.addSubview(transitionImageView) 55 | } 56 | 57 | fromReferenceImageView.isHidden = true 58 | 59 | let finalTransitionSize = calculateZoomInImageFrame(image: referenceImage, forView: toVC.view) 60 | 61 | UIView.animate(withDuration: transitionDuration(using: transitionContext), 62 | delay: 0, 63 | usingSpringWithDamping: 0.8, 64 | initialSpringVelocity: 0, 65 | options: [UIView.AnimationOptions.transitionCrossDissolve], 66 | animations: { 67 | self.transitionImageView?.frame = finalTransitionSize 68 | toVC.view.alpha = 1.0 69 | fromVC.tabBarController?.tabBar.alpha = 0 70 | }, 71 | completion: { completed in 72 | 73 | self.transitionImageView?.removeFromSuperview() 74 | toReferenceImageView.isHidden = false 75 | fromReferenceImageView.isHidden = false 76 | 77 | self.transitionImageView = nil 78 | 79 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 80 | self.toDelegate?.transitionDidEndWith(zoomAnimator: self) 81 | self.fromDelegate?.transitionDidEndWith(zoomAnimator: self) 82 | }) 83 | } 84 | 85 | fileprivate func animateZoomOutTransition(using transitionContext: UIViewControllerContextTransitioning) { 86 | let containerView = transitionContext.containerView 87 | 88 | guard let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to), 89 | let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from), 90 | let fromReferenceImageView = self.fromDelegate?.referenceImageView(for: self), 91 | let toReferenceImageView = self.toDelegate?.referenceImageView(for: self), 92 | let fromReferenceImageViewFrame = self.fromDelegate?.referenceImageViewFrameInTransitioningView(for: self), 93 | let toReferenceImageViewFrame = self.toDelegate?.referenceImageViewFrameInTransitioningView(for: self) 94 | else { 95 | return 96 | } 97 | 98 | self.fromDelegate?.transitionWillStartWith(zoomAnimator: self) 99 | self.toDelegate?.transitionWillStartWith(zoomAnimator: self) 100 | 101 | toReferenceImageView.isHidden = true 102 | 103 | let referenceImage = fromReferenceImageView.image! 104 | 105 | if self.transitionImageView == nil { 106 | let transitionImageView = UIImageView(image: referenceImage) 107 | transitionImageView.contentMode = .scaleAspectFill 108 | transitionImageView.clipsToBounds = true 109 | transitionImageView.frame = fromReferenceImageViewFrame 110 | self.transitionImageView = transitionImageView 111 | containerView.addSubview(transitionImageView) 112 | } 113 | 114 | containerView.insertSubview(toVC.view, belowSubview: fromVC.view) 115 | fromReferenceImageView.isHidden = true 116 | 117 | let finalTransitionSize = toReferenceImageViewFrame 118 | 119 | UIView.animate(withDuration: transitionDuration(using: transitionContext), 120 | delay: 0, 121 | options: [], 122 | animations: { 123 | fromVC.view.alpha = 0 124 | self.transitionImageView?.frame = finalTransitionSize 125 | toVC.tabBarController?.tabBar.alpha = 1 126 | }, completion: { completed in 127 | 128 | self.transitionImageView?.removeFromSuperview() 129 | toReferenceImageView.isHidden = false 130 | fromReferenceImageView.isHidden = false 131 | 132 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 133 | self.toDelegate?.transitionDidEndWith(zoomAnimator: self) 134 | self.fromDelegate?.transitionDidEndWith(zoomAnimator: self) 135 | 136 | }) 137 | } 138 | 139 | private func calculateZoomInImageFrame(image: UIImage, forView view: UIView) -> CGRect { 140 | 141 | let viewRatio = view.frame.size.width / view.frame.size.height 142 | let imageRatio = image.size.width / image.size.height 143 | let touchesSides = (imageRatio > viewRatio) 144 | 145 | if touchesSides { 146 | let height = view.frame.width / imageRatio 147 | let yPoint = view.frame.minY + (view.frame.height - height) / 2 148 | return CGRect(x: 0, y: yPoint, width: view.frame.width, height: height) 149 | } else { 150 | let width = view.frame.height * imageRatio 151 | let xPoint = view.frame.minX + (view.frame.width - width) / 2 152 | return CGRect(x: xPoint, y: 0, width: width, height: view.frame.height) 153 | } 154 | } 155 | } 156 | 157 | extension ZoomAnimator: UIViewControllerAnimatedTransitioning { 158 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 159 | if self.isPresenting { 160 | return 0.5 161 | } else { 162 | return 0.25 163 | } 164 | } 165 | 166 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 167 | if self.isPresenting { 168 | animateZoomInTransition(using: transitionContext) 169 | } else { 170 | animateZoomOutTransition(using: transitionContext) 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /FluidPhoto/Animation/ZoomDismissalInteractionController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZoomDismissalInteractionController.swift 3 | // FluidPhoto 4 | // 5 | // Created by Masamichi Ueta on 2016/12/29. 6 | // Copyright © 2016 Masmichi Ueta. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ZoomDismissalInteractionController: NSObject { 12 | 13 | var transitionContext: UIViewControllerContextTransitioning? 14 | var animator: UIViewControllerAnimatedTransitioning? 15 | 16 | var fromReferenceImageViewFrame: CGRect? 17 | var toReferenceImageViewFrame: CGRect? 18 | 19 | func didPanWith(gestureRecognizer: UIPanGestureRecognizer) { 20 | 21 | guard let transitionContext = self.transitionContext, 22 | let animator = self.animator as? ZoomAnimator, 23 | let transitionImageView = animator.transitionImageView, 24 | let fromVC = transitionContext.viewController(forKey: .from), 25 | let toVC = transitionContext.viewController(forKey: .to), 26 | let fromReferenceImageView = animator.fromDelegate?.referenceImageView(for: animator), 27 | let toReferenceImageView = animator.toDelegate?.referenceImageView(for: animator), 28 | let fromReferenceImageViewFrame = self.fromReferenceImageViewFrame, 29 | let toReferenceImageViewFrame = self.toReferenceImageViewFrame else { 30 | return 31 | } 32 | 33 | 34 | fromReferenceImageView.isHidden = true 35 | 36 | let anchorPoint = CGPoint(x: fromReferenceImageViewFrame.midX, y: fromReferenceImageViewFrame.midY) 37 | let translatedPoint = gestureRecognizer.translation(in: fromReferenceImageView) 38 | let verticalDelta : CGFloat = translatedPoint.y < 0 ? 0 : translatedPoint.y 39 | 40 | let backgroundAlpha = backgroundAlphaFor(view: fromVC.view, withPanningVerticalDelta: verticalDelta) 41 | let scale = scaleFor(view: fromVC.view, withPanningVerticalDelta: verticalDelta) 42 | 43 | fromVC.view.alpha = backgroundAlpha 44 | 45 | transitionImageView.transform = CGAffineTransform(scaleX: scale, y: scale) 46 | let newCenter = CGPoint(x: anchorPoint.x + translatedPoint.x, y: anchorPoint.y + translatedPoint.y - transitionImageView.frame.height * (1 - scale) / 2.0) 47 | transitionImageView.center = newCenter 48 | 49 | toReferenceImageView.isHidden = true 50 | 51 | transitionContext.updateInteractiveTransition(1 - scale) 52 | 53 | toVC.tabBarController?.tabBar.alpha = 1 - backgroundAlpha 54 | 55 | if gestureRecognizer.state == .ended { 56 | 57 | let velocity = gestureRecognizer.velocity(in: fromVC.view) 58 | if velocity.y < 0 || newCenter.y < anchorPoint.y { 59 | 60 | //cancel 61 | UIView.animate( 62 | withDuration: 0.5, 63 | delay: 0, 64 | usingSpringWithDamping: 0.9, 65 | initialSpringVelocity: 0, 66 | options: [], 67 | animations: { 68 | transitionImageView.frame = fromReferenceImageViewFrame 69 | fromVC.view.alpha = 1.0 70 | toVC.tabBarController?.tabBar.alpha = 0 71 | }, 72 | completion: { completed in 73 | 74 | toReferenceImageView.isHidden = false 75 | fromReferenceImageView.isHidden = false 76 | transitionImageView.removeFromSuperview() 77 | animator.transitionImageView = nil 78 | transitionContext.cancelInteractiveTransition() 79 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 80 | animator.toDelegate?.transitionDidEndWith(zoomAnimator: animator) 81 | animator.fromDelegate?.transitionDidEndWith(zoomAnimator: animator) 82 | self.transitionContext = nil 83 | }) 84 | return 85 | } 86 | 87 | //start animation 88 | let finalTransitionSize = toReferenceImageViewFrame 89 | 90 | UIView.animate(withDuration: 0.25, 91 | delay: 0, 92 | options: [], 93 | animations: { 94 | fromVC.view.alpha = 0 95 | transitionImageView.frame = finalTransitionSize 96 | toVC.tabBarController?.tabBar.alpha = 1 97 | 98 | }, completion: { completed in 99 | 100 | transitionImageView.removeFromSuperview() 101 | toReferenceImageView.isHidden = false 102 | fromReferenceImageView.isHidden = false 103 | 104 | self.transitionContext?.finishInteractiveTransition() 105 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 106 | animator.toDelegate?.transitionDidEndWith(zoomAnimator: animator) 107 | animator.fromDelegate?.transitionDidEndWith(zoomAnimator: animator) 108 | self.transitionContext = nil 109 | }) 110 | } 111 | } 112 | 113 | func backgroundAlphaFor(view: UIView, withPanningVerticalDelta verticalDelta: CGFloat) -> CGFloat { 114 | let startingAlpha:CGFloat = 1.0 115 | let finalAlpha: CGFloat = 0.0 116 | let totalAvailableAlpha = startingAlpha - finalAlpha 117 | 118 | let maximumDelta = view.bounds.height / 4.0 119 | let deltaAsPercentageOfMaximun = min(abs(verticalDelta) / maximumDelta, 1.0) 120 | 121 | return startingAlpha - (deltaAsPercentageOfMaximun * totalAvailableAlpha) 122 | } 123 | 124 | func scaleFor(view: UIView, withPanningVerticalDelta verticalDelta: CGFloat) -> CGFloat { 125 | let startingScale:CGFloat = 1.0 126 | let finalScale: CGFloat = 0.5 127 | let totalAvailableScale = startingScale - finalScale 128 | 129 | let maximumDelta = view.bounds.height / 2.0 130 | let deltaAsPercentageOfMaximun = min(abs(verticalDelta) / maximumDelta, 1.0) 131 | 132 | return startingScale - (deltaAsPercentageOfMaximun * totalAvailableScale) 133 | } 134 | } 135 | 136 | extension ZoomDismissalInteractionController: UIViewControllerInteractiveTransitioning { 137 | func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) { 138 | self.transitionContext = transitionContext 139 | 140 | let containerView = transitionContext.containerView 141 | 142 | guard let animator = self.animator as? ZoomAnimator, 143 | let fromVC = transitionContext.viewController(forKey: .from), 144 | let toVC = transitionContext.viewController(forKey: .to), 145 | let fromReferenceImageViewFrame = animator.fromDelegate?.referenceImageViewFrameInTransitioningView(for: animator), 146 | let toReferenceImageViewFrame = animator.toDelegate?.referenceImageViewFrameInTransitioningView(for: animator), 147 | let fromReferenceImageView = animator.fromDelegate?.referenceImageView(for: animator) 148 | else { 149 | return 150 | } 151 | 152 | animator.fromDelegate?.transitionWillStartWith(zoomAnimator: animator) 153 | animator.toDelegate?.transitionWillStartWith(zoomAnimator: animator) 154 | 155 | self.fromReferenceImageViewFrame = fromReferenceImageViewFrame 156 | self.toReferenceImageViewFrame = toReferenceImageViewFrame 157 | 158 | let referenceImage = fromReferenceImageView.image! 159 | 160 | containerView.insertSubview(toVC.view, belowSubview: fromVC.view) 161 | if animator.transitionImageView == nil { 162 | let transitionImageView = UIImageView(image: referenceImage) 163 | transitionImageView.contentMode = .scaleAspectFill 164 | transitionImageView.clipsToBounds = true 165 | transitionImageView.frame = fromReferenceImageViewFrame 166 | animator.transitionImageView = transitionImageView 167 | containerView.addSubview(transitionImageView) 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /FluidPhoto/Animation/ZoomTransitionController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZoomTransitionController.swift 3 | // FluidPhoto 4 | // 5 | // Created by Masamichi Ueta on 2016/12/29. 6 | // Copyright © 2016 Masmichi Ueta. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ZoomTransitionController: NSObject { 12 | 13 | let animator: ZoomAnimator 14 | let interactionController: ZoomDismissalInteractionController 15 | var isInteractive: Bool = false 16 | 17 | weak var fromDelegate: ZoomAnimatorDelegate? 18 | weak var toDelegate: ZoomAnimatorDelegate? 19 | 20 | override init() { 21 | animator = ZoomAnimator() 22 | interactionController = ZoomDismissalInteractionController() 23 | super.init() 24 | } 25 | 26 | func didPanWith(gestureRecognizer: UIPanGestureRecognizer) { 27 | self.interactionController.didPanWith(gestureRecognizer: gestureRecognizer) 28 | } 29 | } 30 | 31 | extension ZoomTransitionController: UIViewControllerTransitioningDelegate { 32 | func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 33 | self.animator.isPresenting = true 34 | self.animator.fromDelegate = fromDelegate 35 | self.animator.toDelegate = toDelegate 36 | return self.animator 37 | } 38 | 39 | func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 40 | self.animator.isPresenting = false 41 | let tmp = self.fromDelegate 42 | self.animator.fromDelegate = self.toDelegate 43 | self.animator.toDelegate = tmp 44 | return self.animator 45 | } 46 | 47 | func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 48 | if !self.isInteractive { 49 | return nil 50 | } 51 | 52 | self.interactionController.animator = animator 53 | return self.interactionController 54 | } 55 | 56 | } 57 | 58 | extension ZoomTransitionController: UINavigationControllerDelegate { 59 | func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { 60 | 61 | if operation == .push { 62 | self.animator.isPresenting = true 63 | self.animator.fromDelegate = fromDelegate 64 | self.animator.toDelegate = toDelegate 65 | } else { 66 | self.animator.isPresenting = false 67 | let tmp = self.fromDelegate 68 | self.animator.fromDelegate = self.toDelegate 69 | self.animator.toDelegate = tmp 70 | } 71 | 72 | return self.animator 73 | } 74 | 75 | func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 76 | 77 | if !self.isInteractive { 78 | return nil 79 | } 80 | 81 | self.interactionController.animator = animator 82 | return self.interactionController 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /FluidPhoto/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // FluidPhoto 4 | // 5 | // Created by Masamichi Ueta on 2016/12/23. 6 | // Copyright © 2016 Masmichi Ueta. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | return true 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s__satiqdkzeq-clem-onojeghuo.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/1.imageset/s__satiqdkzeq-clem-onojeghuo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/1.imageset/s__satiqdkzeq-clem-onojeghuo.jpg -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/10.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s_jktv__bqmaa-brooke-lark.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/10.imageset/s_jktv__bqmaa-brooke-lark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/10.imageset/s_jktv__bqmaa-brooke-lark.jpg -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/11.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s_ko1g3nera2o-anchor-lee.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/11.imageset/s_ko1g3nera2o-anchor-lee.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/11.imageset/s_ko1g3nera2o-anchor-lee.jpg -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/12.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s_kujkutxr0z4-ridham-nagralawala.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/12.imageset/s_kujkutxr0z4-ridham-nagralawala.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/12.imageset/s_kujkutxr0z4-ridham-nagralawala.jpg -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/13.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s_lzoij-a4u-anchor-lee.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/13.imageset/s_lzoij-a4u-anchor-lee.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/13.imageset/s_lzoij-a4u-anchor-lee.jpg -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/14.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s_nwfuayecnus-ethan-robertson.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/14.imageset/s_nwfuayecnus-ethan-robertson.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/14.imageset/s_nwfuayecnus-ethan-robertson.jpg -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/15.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s_ooumcn6jsoq-jay-cee.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/15.imageset/s_ooumcn6jsoq-jay-cee.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/15.imageset/s_ooumcn6jsoq-jay-cee.jpg -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/16.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s_qtggylug6cw-saso-tusar.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/16.imageset/s_qtggylug6cw-saso-tusar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/16.imageset/s_qtggylug6cw-saso-tusar.jpg -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/17.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s_vwbmxol3h8s-joel-filipe.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/17.imageset/s_vwbmxol3h8s-joel-filipe.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/17.imageset/s_vwbmxol3h8s-joel-filipe.jpg -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/18.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s_xn_crzwxgdm-andreas-p.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/18.imageset/s_xn_crzwxgdm-andreas-p.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/18.imageset/s_xn_crzwxgdm-andreas-p.jpg -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s_1zmk5bezlyc-i-m-priscilla.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/2.imageset/s_1zmk5bezlyc-i-m-priscilla.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/2.imageset/s_1zmk5bezlyc-i-m-priscilla.jpg -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s_2tcy8pqfxse-mpho-mojapelo.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/3.imageset/s_2tcy8pqfxse-mpho-mojapelo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/3.imageset/s_2tcy8pqfxse-mpho-mojapelo.jpg -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s_aez1-a7ys7s-mario-klassen.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/4.imageset/s_aez1-a7ys7s-mario-klassen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/4.imageset/s_aez1-a7ys7s-mario-klassen.jpg -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s_earyikg21d4-maja-petric.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/5.imageset/s_earyikg21d4-maja-petric.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/5.imageset/s_earyikg21d4-maja-petric.jpg -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/6.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s_ey0k7-kqsqy-scott-webb.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/6.imageset/s_ey0k7-kqsqy-scott-webb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/6.imageset/s_ey0k7-kqsqy-scott-webb.jpg -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/7.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s_g3eh_ge1pl4-jaromir-kavan.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/7.imageset/s_g3eh_ge1pl4-jaromir-kavan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/7.imageset/s_g3eh_ge1pl4-jaromir-kavan.jpg -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/8.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s_hqnlazeunhi-sime-basioli.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/8.imageset/s_hqnlazeunhi-sime-basioli.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/8.imageset/s_hqnlazeunhi-sime-basioli.jpg -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/9.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "s_jjrjx9mq6q0-clem-onojeghuo.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/9.imageset/s_jjrjx9mq6q0-clem-onojeghuo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/FluidPhoto/Assets.xcassets/9.imageset/s_jjrjx9mq6q0-clem-onojeghuo.jpg -------------------------------------------------------------------------------- /FluidPhoto/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" : "1x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "29x29", 26 | "scale" : "3x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "40x40", 36 | "scale" : "3x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "57x57", 41 | "scale" : "1x" 42 | }, 43 | { 44 | "idiom" : "iphone", 45 | "size" : "57x57", 46 | "scale" : "2x" 47 | }, 48 | { 49 | "idiom" : "iphone", 50 | "size" : "60x60", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "iphone", 55 | "size" : "60x60", 56 | "scale" : "3x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "20x20", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "20x20", 66 | "scale" : "2x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "29x29", 71 | "scale" : "1x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "29x29", 76 | "scale" : "2x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "40x40", 81 | "scale" : "1x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "40x40", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ipad", 90 | "size" : "50x50", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "idiom" : "ipad", 95 | "size" : "50x50", 96 | "scale" : "2x" 97 | }, 98 | { 99 | "idiom" : "ipad", 100 | "size" : "72x72", 101 | "scale" : "1x" 102 | }, 103 | { 104 | "idiom" : "ipad", 105 | "size" : "72x72", 106 | "scale" : "2x" 107 | }, 108 | { 109 | "idiom" : "ipad", 110 | "size" : "76x76", 111 | "scale" : "1x" 112 | }, 113 | { 114 | "idiom" : "ipad", 115 | "size" : "76x76", 116 | "scale" : "2x" 117 | }, 118 | { 119 | "idiom" : "ipad", 120 | "size" : "83.5x83.5", 121 | "scale" : "2x" 122 | }, 123 | { 124 | "idiom" : "ios-marketing", 125 | "size" : "1024x1024", 126 | "scale" : "1x" 127 | } 128 | ], 129 | "info" : { 130 | "version" : 1, 131 | "author" : "xcode" 132 | } 133 | } -------------------------------------------------------------------------------- /FluidPhoto/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /FluidPhoto/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 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /FluidPhoto/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 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /FluidPhoto/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 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 | -------------------------------------------------------------------------------- /FluidPhoto/View/PhotoCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoCollectionViewCell.swift 3 | // FluidPhoto 4 | // 5 | // Created by Masamichi Ueta on 2016/12/23. 6 | // Copyright © 2016 Masmichi Ueta. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PhotoCollectionViewCell: UICollectionViewCell { 12 | 13 | @IBOutlet weak var imageView: UIImageView! 14 | 15 | } 16 | -------------------------------------------------------------------------------- /FluidPhoto/ViewController/PhotoPageContainerViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoPageContainerViewController.swift 3 | // FluidPhoto 4 | // 5 | // Created by Masamichi Ueta on 2016/12/23. 6 | // Copyright © 2016 Masmichi Ueta. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol PhotoPageContainerViewControllerDelegate: AnyObject { 12 | func containerViewController(_ containerViewController: PhotoPageContainerViewController, indexDidUpdate currentIndex: Int) 13 | } 14 | 15 | class PhotoPageContainerViewController: UIViewController, UIGestureRecognizerDelegate { 16 | 17 | enum ScreenMode { 18 | case full, normal 19 | } 20 | var currentMode: ScreenMode = .normal 21 | 22 | weak var delegate: PhotoPageContainerViewControllerDelegate? 23 | 24 | var pageViewController: UIPageViewController { 25 | return self.children[0] as! UIPageViewController 26 | } 27 | 28 | var currentViewController: PhotoZoomViewController { 29 | return self.pageViewController.viewControllers![0] as! PhotoZoomViewController 30 | } 31 | 32 | var photos: [UIImage]! 33 | var currentIndex = 0 34 | var nextIndex: Int? 35 | 36 | var panGestureRecognizer: UIPanGestureRecognizer! 37 | var singleTapGestureRecognizer: UITapGestureRecognizer! 38 | 39 | var transitionController = ZoomTransitionController() 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | 44 | self.pageViewController.delegate = self 45 | self.pageViewController.dataSource = self 46 | self.panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didPanWith(gestureRecognizer:))) 47 | self.panGestureRecognizer.delegate = self 48 | self.pageViewController.view.addGestureRecognizer(self.panGestureRecognizer) 49 | 50 | self.singleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didSingleTapWith(gestureRecognizer:))) 51 | self.pageViewController.view.addGestureRecognizer(self.singleTapGestureRecognizer) 52 | 53 | let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "\(PhotoZoomViewController.self)") as! PhotoZoomViewController 54 | vc.delegate = self 55 | vc.index = self.currentIndex 56 | vc.image = self.photos[self.currentIndex] 57 | self.singleTapGestureRecognizer.require(toFail: vc.doubleTapGestureRecognizer) 58 | let viewControllers = [ 59 | vc 60 | ] 61 | 62 | self.pageViewController.setViewControllers(viewControllers, direction: .forward, animated: true, completion: nil) 63 | } 64 | 65 | func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 66 | 67 | if let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer { 68 | let velocity = gestureRecognizer.velocity(in: self.view) 69 | 70 | var velocityCheck : Bool = false 71 | 72 | if UIDevice.current.orientation.isLandscape { 73 | velocityCheck = velocity.x < 0 74 | } 75 | else { 76 | velocityCheck = velocity.y < 0 77 | } 78 | if velocityCheck { 79 | return false 80 | } 81 | } 82 | 83 | return true 84 | } 85 | 86 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 87 | 88 | if otherGestureRecognizer == self.currentViewController.scrollView.panGestureRecognizer { 89 | if self.currentViewController.scrollView.contentOffset.y == 0 { 90 | return true 91 | } 92 | } 93 | 94 | return false 95 | } 96 | 97 | override func didReceiveMemoryWarning() { 98 | super.didReceiveMemoryWarning() 99 | // Dispose of any resources that can be recreated. 100 | } 101 | 102 | @objc func didPanWith(gestureRecognizer: UIPanGestureRecognizer) { 103 | switch gestureRecognizer.state { 104 | case .began: 105 | self.currentViewController.scrollView.isScrollEnabled = false 106 | self.transitionController.isInteractive = true 107 | let _ = self.navigationController?.popViewController(animated: true) 108 | case .ended: 109 | if self.transitionController.isInteractive { 110 | self.currentViewController.scrollView.isScrollEnabled = true 111 | self.transitionController.isInteractive = false 112 | self.transitionController.didPanWith(gestureRecognizer: gestureRecognizer) 113 | } 114 | default: 115 | if self.transitionController.isInteractive { 116 | self.transitionController.didPanWith(gestureRecognizer: gestureRecognizer) 117 | } 118 | } 119 | } 120 | 121 | @objc func didSingleTapWith(gestureRecognizer: UITapGestureRecognizer) { 122 | if self.currentMode == .full { 123 | changeScreenMode(to: .normal) 124 | self.currentMode = .normal 125 | } else { 126 | changeScreenMode(to: .full) 127 | self.currentMode = .full 128 | } 129 | 130 | } 131 | 132 | func changeScreenMode(to: ScreenMode) { 133 | if to == .full { 134 | self.navigationController?.setNavigationBarHidden(true, animated: false) 135 | UIView.animate(withDuration: 0.25, 136 | animations: { 137 | self.view.backgroundColor = .black 138 | 139 | }, completion: { completed in 140 | }) 141 | } else { 142 | self.navigationController?.setNavigationBarHidden(false, animated: false) 143 | UIView.animate(withDuration: 0.25, 144 | animations: { 145 | if #available(iOS 13.0, *) { 146 | self.view.backgroundColor = .systemBackground 147 | } else { 148 | self.view.backgroundColor = .white 149 | } 150 | }, completion: { completed in 151 | }) 152 | } 153 | } 154 | } 155 | 156 | extension PhotoPageContainerViewController: UIPageViewControllerDelegate, UIPageViewControllerDataSource { 157 | 158 | func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { 159 | 160 | if currentIndex == 0 { 161 | return nil 162 | } 163 | 164 | let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "\(PhotoZoomViewController.self)") as! PhotoZoomViewController 165 | vc.delegate = self 166 | vc.image = self.photos[currentIndex - 1] 167 | vc.index = currentIndex - 1 168 | self.singleTapGestureRecognizer.require(toFail: vc.doubleTapGestureRecognizer) 169 | return vc 170 | 171 | } 172 | 173 | func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { 174 | 175 | if currentIndex == (self.photos.count - 1) { 176 | return nil 177 | } 178 | 179 | let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "\(PhotoZoomViewController.self)") as! PhotoZoomViewController 180 | vc.delegate = self 181 | self.singleTapGestureRecognizer.require(toFail: vc.doubleTapGestureRecognizer) 182 | vc.image = self.photos[currentIndex + 1] 183 | vc.index = currentIndex + 1 184 | return vc 185 | 186 | } 187 | 188 | func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { 189 | 190 | guard let nextVC = pendingViewControllers.first as? PhotoZoomViewController else { 191 | return 192 | } 193 | 194 | self.nextIndex = nextVC.index 195 | } 196 | 197 | func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { 198 | 199 | if (completed && self.nextIndex != nil) { 200 | previousViewControllers.forEach { vc in 201 | let zoomVC = vc as! PhotoZoomViewController 202 | zoomVC.scrollView.zoomScale = zoomVC.scrollView.minimumZoomScale 203 | } 204 | 205 | self.currentIndex = self.nextIndex! 206 | self.delegate?.containerViewController(self, indexDidUpdate: self.currentIndex) 207 | } 208 | 209 | self.nextIndex = nil 210 | } 211 | 212 | } 213 | 214 | extension PhotoPageContainerViewController: PhotoZoomViewControllerDelegate { 215 | 216 | func photoZoomViewController(_ photoZoomViewController: PhotoZoomViewController, scrollViewDidScroll scrollView: UIScrollView) { 217 | if scrollView.zoomScale != scrollView.minimumZoomScale && self.currentMode != .full { 218 | self.changeScreenMode(to: .full) 219 | self.currentMode = .full 220 | } 221 | } 222 | } 223 | 224 | extension PhotoPageContainerViewController: ZoomAnimatorDelegate { 225 | 226 | func transitionWillStartWith(zoomAnimator: ZoomAnimator) { 227 | } 228 | 229 | func transitionDidEndWith(zoomAnimator: ZoomAnimator) { 230 | } 231 | 232 | func referenceImageView(for zoomAnimator: ZoomAnimator) -> UIImageView? { 233 | return self.currentViewController.imageView 234 | } 235 | 236 | func referenceImageViewFrameInTransitioningView(for zoomAnimator: ZoomAnimator) -> CGRect? { 237 | return self.currentViewController.scrollView.convert(self.currentViewController.imageView.frame, to: self.currentViewController.view) 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /FluidPhoto/ViewController/PhotoZoomViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoZoomViewController.swift 3 | // FluidPhoto 4 | // 5 | // Created by Masamichi Ueta on 2016/12/23. 6 | // Copyright © 2016 Masmichi Ueta. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol PhotoZoomViewControllerDelegate: AnyObject { 12 | func photoZoomViewController(_ photoZoomViewController: PhotoZoomViewController, scrollViewDidScroll scrollView: UIScrollView) 13 | } 14 | 15 | class PhotoZoomViewController: UIViewController { 16 | 17 | @IBOutlet weak var imageViewBottomConstraint: NSLayoutConstraint! 18 | @IBOutlet weak var imageViewLeadingConstraint: NSLayoutConstraint! 19 | @IBOutlet weak var imageViewTopConstraint: NSLayoutConstraint! 20 | @IBOutlet weak var imageViewTrailingConstraint: NSLayoutConstraint! 21 | @IBOutlet weak var scrollView: UIScrollView! 22 | @IBOutlet weak var imageView: UIImageView! 23 | 24 | weak var delegate: PhotoZoomViewControllerDelegate? 25 | 26 | var image: UIImage! 27 | var index: Int = 0 28 | 29 | var doubleTapGestureRecognizer: UITapGestureRecognizer! 30 | 31 | required init?(coder aDecoder: NSCoder) { 32 | super.init(coder: aDecoder) 33 | self.doubleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didDoubleTapWith(gestureRecognizer:))) 34 | self.doubleTapGestureRecognizer.numberOfTapsRequired = 2 35 | } 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | self.scrollView.delegate = self 40 | if #available(iOS 11, *) { 41 | self.scrollView.contentInsetAdjustmentBehavior = .never 42 | } 43 | self.imageView.image = self.image 44 | self.imageView.frame = CGRect(x: self.imageView.frame.origin.x, 45 | y: self.imageView.frame.origin.y, 46 | width: self.image.size.width, 47 | height: self.image.size.height) 48 | self.view.addGestureRecognizer(self.doubleTapGestureRecognizer) 49 | } 50 | 51 | override func viewDidLayoutSubviews() { 52 | super.viewDidLayoutSubviews() 53 | updateZoomScaleForSize(view.bounds.size) 54 | updateConstraintsForSize(view.bounds.size) 55 | } 56 | 57 | override func viewDidAppear(_ animated: Bool) { 58 | super.viewDidAppear(animated) 59 | updateZoomScaleForSize(view.bounds.size) 60 | updateConstraintsForSize(view.bounds.size) 61 | } 62 | 63 | override func viewSafeAreaInsetsDidChange() { 64 | 65 | //When this view's safeAreaInsets change, propagate this information 66 | //to the previous ViewController so the collectionView contentInsets 67 | //can be updated accordingly. This is necessary in order to properly 68 | //calculate the frame position for the dismiss (swipe down) animation 69 | 70 | if #available(iOS 11, *) { 71 | 72 | //Get the parent view controller (ViewController) from the navigation controller 73 | guard let parentVC = self.navigationController?.viewControllers.first as? ViewController else { 74 | return 75 | } 76 | 77 | //Update the ViewController's left and right local safeAreaInset variables 78 | //with the safeAreaInsets for this current view. These will be used to 79 | //update the contentInsets of the collectionView inside ViewController 80 | parentVC.currentLeftSafeAreaInset = self.view.safeAreaInsets.left 81 | parentVC.currentRightSafeAreaInset = self.view.safeAreaInsets.right 82 | 83 | } 84 | 85 | } 86 | 87 | override func didReceiveMemoryWarning() { 88 | super.didReceiveMemoryWarning() 89 | } 90 | 91 | @objc func didDoubleTapWith(gestureRecognizer: UITapGestureRecognizer) { 92 | let pointInView = gestureRecognizer.location(in: self.imageView) 93 | var newZoomScale = self.scrollView.maximumZoomScale 94 | 95 | if self.scrollView.zoomScale >= newZoomScale || abs(self.scrollView.zoomScale - newZoomScale) <= 0.01 { 96 | newZoomScale = self.scrollView.minimumZoomScale 97 | } 98 | 99 | let width = self.scrollView.bounds.width / newZoomScale 100 | let height = self.scrollView.bounds.height / newZoomScale 101 | let originX = pointInView.x - (width / 2.0) 102 | let originY = pointInView.y - (height / 2.0) 103 | 104 | let rectToZoomTo = CGRect(x: originX, y: originY, width: width, height: height) 105 | self.scrollView.zoom(to: rectToZoomTo, animated: true) 106 | } 107 | 108 | fileprivate func updateZoomScaleForSize(_ size: CGSize) { 109 | 110 | let widthScale = size.width / imageView.bounds.width 111 | let heightScale = size.height / imageView.bounds.height 112 | let minScale = min(widthScale, heightScale) 113 | scrollView.minimumZoomScale = minScale 114 | 115 | scrollView.zoomScale = minScale 116 | scrollView.maximumZoomScale = minScale * 4 117 | } 118 | 119 | fileprivate func updateConstraintsForSize(_ size: CGSize) { 120 | let yOffset = max(0, (size.height - imageView.frame.height) / 2) 121 | imageViewTopConstraint.constant = yOffset 122 | imageViewBottomConstraint.constant = yOffset 123 | 124 | let xOffset = max(0, (size.width - imageView.frame.width) / 2) 125 | imageViewLeadingConstraint.constant = xOffset 126 | imageViewTrailingConstraint.constant = xOffset 127 | 128 | let contentHeight = yOffset * 2 + self.imageView.frame.height 129 | view.layoutIfNeeded() 130 | self.scrollView.contentSize = CGSize(width: self.scrollView.contentSize.width, height: contentHeight) 131 | } 132 | } 133 | 134 | extension PhotoZoomViewController: UIScrollViewDelegate { 135 | func viewForZooming(in scrollView: UIScrollView) -> UIView? { 136 | return imageView 137 | } 138 | 139 | func scrollViewDidZoom(_ scrollView: UIScrollView) { 140 | updateConstraintsForSize(self.view.bounds.size) 141 | } 142 | 143 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 144 | self.delegate?.photoZoomViewController(self, scrollViewDidScroll: scrollView) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /FluidPhoto/ViewController/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // FluidPhoto 4 | // 5 | // Created by Masamichi Ueta on 2016/12/23. 6 | // Copyright © 2016 Masmichi Ueta. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | @IBOutlet weak var collectionView: UICollectionView! 14 | 15 | var photos: [UIImage]! 16 | 17 | var selectedIndexPath: IndexPath! 18 | 19 | //These variables are used to hold any updates to the safeAreaInsets 20 | //that might not have been propagated to this ViewController. This is required 21 | //for supporting devices running on >= iOS 11. These will be set manually from 22 | //PhotoZoomViewController.swift to ensure any changes to the safeAreaInsets 23 | //after the device rotates are pushed to this ViewController. This is required 24 | //to ensure the collectionView.convert() function calculates the proper 25 | //frame result inside referenceImageViewFrameInTransitioningView() 26 | var currentLeftSafeAreaInset : CGFloat = 0.0 27 | var currentRightSafeAreaInset : CGFloat = 0.0 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | self.photos = [ 33 | #imageLiteral(resourceName: "1"), 34 | #imageLiteral(resourceName: "2"), 35 | #imageLiteral(resourceName: "3"), 36 | #imageLiteral(resourceName: "4"), 37 | #imageLiteral(resourceName: "5"), 38 | #imageLiteral(resourceName: "6"), 39 | #imageLiteral(resourceName: "7"), 40 | #imageLiteral(resourceName: "8"), 41 | #imageLiteral(resourceName: "9"), 42 | #imageLiteral(resourceName: "10"), 43 | #imageLiteral(resourceName: "11"), 44 | #imageLiteral(resourceName: "12"), 45 | #imageLiteral(resourceName: "13"), 46 | #imageLiteral(resourceName: "14"), 47 | #imageLiteral(resourceName: "15"), 48 | #imageLiteral(resourceName: "16"), 49 | #imageLiteral(resourceName: "17"), 50 | #imageLiteral(resourceName: "18"), 51 | #imageLiteral(resourceName: "1"), 52 | #imageLiteral(resourceName: "2"), 53 | #imageLiteral(resourceName: "3"), 54 | #imageLiteral(resourceName: "4"), 55 | #imageLiteral(resourceName: "5"), 56 | #imageLiteral(resourceName: "6"), 57 | #imageLiteral(resourceName: "7"), 58 | #imageLiteral(resourceName: "8"), 59 | #imageLiteral(resourceName: "9"), 60 | #imageLiteral(resourceName: "10"), 61 | #imageLiteral(resourceName: "11"), 62 | #imageLiteral(resourceName: "12"), 63 | #imageLiteral(resourceName: "13"), 64 | #imageLiteral(resourceName: "14"), 65 | #imageLiteral(resourceName: "15"), 66 | #imageLiteral(resourceName: "16"), 67 | #imageLiteral(resourceName: "17"), 68 | #imageLiteral(resourceName: "18") 69 | ] 70 | 71 | //Manually set the collectionView frame to the size of the view bounds 72 | //(this is required to support iOS 10 devices and earlier) 73 | self.collectionView.frame = self.view.bounds 74 | 75 | } 76 | 77 | override func viewWillAppear(_ animated: Bool) { 78 | super.viewWillAppear(animated) 79 | self.tabBarController?.tabBar.isHidden = false 80 | self.navigationController?.setNavigationBarHidden(false, animated: true) 81 | } 82 | 83 | override func viewSafeAreaInsetsDidChange() { 84 | 85 | //if the application launches in landscape mode, the safeAreaInsets 86 | //need to be updated from 0.0 if the device is an iPhone X model. At 87 | //application launch this function is called before viewWillLayoutSubviews() 88 | if #available(iOS 11, *) { 89 | 90 | self.currentLeftSafeAreaInset = self.view.safeAreaInsets.left 91 | self.currentRightSafeAreaInset = self.view.safeAreaInsets.right 92 | } 93 | 94 | } 95 | 96 | override func viewWillLayoutSubviews() { 97 | 98 | //Only perform these changes for devices running iOS 11 and later. This is called 99 | //inside viewWillLayoutSubviews() instead of viewWillTransition() because when the 100 | //device rotates, the navBarHeight and statusBarHeight will be calculated inside 101 | //viewWillTransition() using the current orientation, and not the orientation 102 | //that the device will be at the end of the transition. 103 | 104 | //By the time that viewWillLayoutSubviews() is called, the views frames have been 105 | //properly updated for the new orientation, so the navBar and statusBar height values 106 | //can be calculated and applied directly as per the code below 107 | 108 | if #available(iOS 11, *) { 109 | 110 | self.view.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: self.view.bounds.size) 111 | self.collectionView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: self.view.bounds.size) 112 | 113 | self.collectionView.contentInsetAdjustmentBehavior = .never 114 | let statusBarHeight : CGFloat = UIApplication.shared.statusBarFrame.height 115 | let navBarHeight : CGFloat = navigationController?.navigationBar.frame.height ?? 0 116 | self.edgesForExtendedLayout = .all 117 | let tabBarHeight = self.tabBarController?.tabBar.frame.height ?? 0 118 | 119 | if UIDevice.current.orientation.isLandscape { 120 | self.collectionView.contentInset = UIEdgeInsets(top: (navBarHeight) + statusBarHeight, left: self.currentLeftSafeAreaInset, bottom: tabBarHeight, right: self.currentRightSafeAreaInset) 121 | } 122 | else { 123 | self.collectionView.contentInset = UIEdgeInsets(top: (navBarHeight) + statusBarHeight, left: 0.0, bottom: tabBarHeight, right: 0.0) 124 | } 125 | } 126 | } 127 | 128 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 129 | super.viewWillTransition(to: size, with: coordinator) 130 | 131 | if #available(iOS 11, *) { 132 | //Do nothing 133 | } 134 | else { 135 | 136 | //Support for devices running iOS 10 and below 137 | 138 | //Check to see if the view is currently visible, and if so, 139 | //animate the frame transition to the new orientation 140 | if self.viewIfLoaded?.window != nil { 141 | 142 | coordinator.animate(alongsideTransition: { _ in 143 | 144 | //This needs to be called inside viewWillTransition() instead of viewWillLayoutSubviews() 145 | //for devices running iOS 10.0 and earlier otherwise the frames for the view and the 146 | //collectionView will not be calculated properly. 147 | self.view.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: size) 148 | self.collectionView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: size) 149 | 150 | }, completion: { _ in 151 | 152 | //Invalidate the collectionViewLayout 153 | self.collectionView.collectionViewLayout.invalidateLayout() 154 | 155 | }) 156 | 157 | } 158 | //Otherwise, do not animate the transition 159 | else { 160 | 161 | self.view.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: size) 162 | self.collectionView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: size) 163 | 164 | //Invalidate the collectionViewLayout 165 | self.collectionView.collectionViewLayout.invalidateLayout() 166 | 167 | } 168 | } 169 | 170 | } 171 | 172 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 173 | if segue.identifier == "ShowPhotoPageView" { 174 | let nav = self.navigationController 175 | let vc = segue.destination as! PhotoPageContainerViewController 176 | nav?.delegate = vc.transitionController 177 | vc.transitionController.fromDelegate = self 178 | vc.transitionController.toDelegate = vc 179 | vc.delegate = self 180 | vc.currentIndex = self.selectedIndexPath.row 181 | vc.photos = self.photos 182 | } 183 | } 184 | 185 | @IBAction func backToViewController(segue: UIStoryboardSegue) { 186 | 187 | } 188 | } 189 | 190 | extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource { 191 | 192 | func numberOfSections(in collectionView: UICollectionView) -> Int { 193 | return 1 194 | } 195 | 196 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 197 | return self.photos.count 198 | } 199 | 200 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 201 | 202 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "\(PhotoCollectionViewCell.self)", for: indexPath) as! PhotoCollectionViewCell 203 | 204 | cell.imageView.image = self.photos[indexPath.row] 205 | return cell 206 | } 207 | 208 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 209 | self.selectedIndexPath = indexPath 210 | self.performSegue(withIdentifier: "ShowPhotoPageView", sender: self) 211 | } 212 | 213 | //This function prevents the collectionView from accessing a deallocated cell. In the event 214 | //that the cell for the selectedIndexPath is nil, a default UIImageView is returned in its place 215 | func getImageViewFromCollectionViewCell(for selectedIndexPath: IndexPath) -> UIImageView { 216 | 217 | //Get the array of visible cells in the collectionView 218 | let visibleCells = self.collectionView.indexPathsForVisibleItems 219 | 220 | //If the current indexPath is not visible in the collectionView, 221 | //scroll the collectionView to the cell to prevent it from returning a nil value 222 | if !visibleCells.contains(self.selectedIndexPath) { 223 | 224 | //Scroll the collectionView to the current selectedIndexPath which is offscreen 225 | self.collectionView.scrollToItem(at: self.selectedIndexPath, at: .centeredVertically, animated: false) 226 | 227 | //Reload the items at the newly visible indexPaths 228 | self.collectionView.reloadItems(at: self.collectionView.indexPathsForVisibleItems) 229 | self.collectionView.layoutIfNeeded() 230 | 231 | //Guard against nil values 232 | guard let guardedCell = (self.collectionView.cellForItem(at: self.selectedIndexPath) as? PhotoCollectionViewCell) else { 233 | //Return a default UIImageView 234 | return UIImageView(frame: CGRect(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY, width: 100.0, height: 100.0)) 235 | } 236 | //The PhotoCollectionViewCell was found in the collectionView, return the image 237 | return guardedCell.imageView 238 | } 239 | else { 240 | 241 | //Guard against nil return values 242 | guard let guardedCell = self.collectionView.cellForItem(at: self.selectedIndexPath) as? PhotoCollectionViewCell else { 243 | //Return a default UIImageView 244 | return UIImageView(frame: CGRect(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY, width: 100.0, height: 100.0)) 245 | } 246 | //The PhotoCollectionViewCell was found in the collectionView, return the image 247 | return guardedCell.imageView 248 | } 249 | 250 | } 251 | 252 | //This function prevents the collectionView from accessing a deallocated cell. In the 253 | //event that the cell for the selectedIndexPath is nil, a default CGRect is returned in its place 254 | func getFrameFromCollectionViewCell(for selectedIndexPath: IndexPath) -> CGRect { 255 | 256 | //Get the currently visible cells from the collectionView 257 | let visibleCells = self.collectionView.indexPathsForVisibleItems 258 | 259 | //If the current indexPath is not visible in the collectionView, 260 | //scroll the collectionView to the cell to prevent it from returning a nil value 261 | if !visibleCells.contains(self.selectedIndexPath) { 262 | 263 | //Scroll the collectionView to the cell that is currently offscreen 264 | self.collectionView.scrollToItem(at: self.selectedIndexPath, at: .centeredVertically, animated: false) 265 | 266 | //Reload the items at the newly visible indexPaths 267 | self.collectionView.reloadItems(at: self.collectionView.indexPathsForVisibleItems) 268 | self.collectionView.layoutIfNeeded() 269 | 270 | //Prevent the collectionView from returning a nil value 271 | guard let guardedCell = (self.collectionView.cellForItem(at: self.selectedIndexPath) as? PhotoCollectionViewCell) else { 272 | return CGRect(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY, width: 100.0, height: 100.0) 273 | } 274 | 275 | return guardedCell.frame 276 | } 277 | //Otherwise the cell should be visible 278 | else { 279 | //Prevent the collectionView from returning a nil value 280 | guard let guardedCell = (self.collectionView.cellForItem(at: self.selectedIndexPath) as? PhotoCollectionViewCell) else { 281 | return CGRect(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY, width: 100.0, height: 100.0) 282 | } 283 | //The cell was found successfully 284 | return guardedCell.frame 285 | } 286 | } 287 | 288 | } 289 | 290 | 291 | extension ViewController: PhotoPageContainerViewControllerDelegate { 292 | 293 | func containerViewController(_ containerViewController: PhotoPageContainerViewController, indexDidUpdate currentIndex: Int) { 294 | self.selectedIndexPath = IndexPath(row: currentIndex, section: 0) 295 | self.collectionView.scrollToItem(at: self.selectedIndexPath, at: .centeredVertically, animated: false) 296 | } 297 | } 298 | 299 | extension ViewController: ZoomAnimatorDelegate { 300 | 301 | func transitionWillStartWith(zoomAnimator: ZoomAnimator) { 302 | 303 | } 304 | 305 | func transitionDidEndWith(zoomAnimator: ZoomAnimator) { 306 | let cell = self.collectionView.cellForItem(at: self.selectedIndexPath) as! PhotoCollectionViewCell 307 | 308 | let cellFrame = self.collectionView.convert(cell.frame, to: self.view) 309 | 310 | if cellFrame.minY < self.collectionView.contentInset.top { 311 | self.collectionView.scrollToItem(at: self.selectedIndexPath, at: .top, animated: false) 312 | } else if cellFrame.maxY > self.view.frame.height - self.collectionView.contentInset.bottom { 313 | self.collectionView.scrollToItem(at: self.selectedIndexPath, at: .bottom, animated: false) 314 | } 315 | } 316 | 317 | func referenceImageView(for zoomAnimator: ZoomAnimator) -> UIImageView? { 318 | 319 | //Get a guarded reference to the cell's UIImageView 320 | let referenceImageView = getImageViewFromCollectionViewCell(for: self.selectedIndexPath) 321 | 322 | return referenceImageView 323 | } 324 | 325 | func referenceImageViewFrameInTransitioningView(for zoomAnimator: ZoomAnimator) -> CGRect? { 326 | 327 | self.view.layoutIfNeeded() 328 | self.collectionView.layoutIfNeeded() 329 | 330 | //Get a guarded reference to the cell's frame 331 | let unconvertedFrame = getFrameFromCollectionViewCell(for: self.selectedIndexPath) 332 | 333 | let cellFrame = self.collectionView.convert(unconvertedFrame, to: self.view) 334 | 335 | if cellFrame.minY < self.collectionView.contentInset.top { 336 | return CGRect(x: cellFrame.minX, y: self.collectionView.contentInset.top, width: cellFrame.width, height: cellFrame.height - (self.collectionView.contentInset.top - cellFrame.minY)) 337 | } 338 | 339 | return cellFrame 340 | } 341 | 342 | } 343 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Masamichi Ueta 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FluidPhoto 2 | FluidPhoto is a sample photo browsing application with zoom transition and interactive transition. 3 | The best sample to create transitions and interactions like Photos app. 4 | 5 | ![Screen Shot](https://github.com/masamichiueta/FluidPhoto/blob/master/Screenshots/fluidphoto.gif) 6 | 7 | ## Articles 8 | - [Create transition and interaction like iOS Photos app](https://medium.com/@masamichiueta/create-transition-and-interaction-like-ios-photos-app-2b9f16313d3) 9 | - [iOSの写真アプリのような画面遷移とインタラクションを実装する](https://qiita.com/masamichiueta/items/a7336ad13de5f0c34831) 10 | 11 | ## Presentations 12 | - [Cloning photos app fluid interface](https://speakerdeck.com/masamichi/cloning-photos-app-fluid-interface) 13 | 14 | ## Author 15 | [@masamichiueta](https://twitter.com/masamichiueta) 16 | -------------------------------------------------------------------------------- /Screenshots/fluidphoto.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masamichiueta/FluidPhoto/f63b94e7c6ae3dab88d3093900dc337503f7e4e7/Screenshots/fluidphoto.gif --------------------------------------------------------------------------------