├── .gitignore ├── Example ├── Xpandr.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── Xpandr │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── TableViewController.swift │ └── ViewController.swift └── screencapture.gif ├── LICENSE ├── Package.swift ├── README.md └── Sources └── DAExpandAnimation └── DAExpandAnimation.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | .DS_Store 5 | Package.resolved 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | *.moved-aside 17 | DerivedData 18 | *.hmap 19 | *.ipa 20 | *.xcuserstate -------------------------------------------------------------------------------- /Example/Xpandr.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 9289A2222D4A07CD00402CB9 /* DAExpandAnimation in Frameworks */ = {isa = PBXBuildFile; productRef = 9289A2212D4A07CD00402CB9 /* DAExpandAnimation */; }; 11 | 92D7D6191B98128D00AD9BC9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D7D6181B98128D00AD9BC9 /* AppDelegate.swift */; }; 12 | 92D7D61B1B98128D00AD9BC9 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D7D61A1B98128D00AD9BC9 /* ViewController.swift */; }; 13 | 92D7D61E1B98128D00AD9BC9 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 92D7D61C1B98128D00AD9BC9 /* Main.storyboard */; }; 14 | 92D7D6201B98128D00AD9BC9 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 92D7D61F1B98128D00AD9BC9 /* Images.xcassets */; }; 15 | 92D7D6231B98128D00AD9BC9 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 92D7D6211B98128D00AD9BC9 /* LaunchScreen.xib */; }; 16 | 92D7D6391B9815D100AD9BC9 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D7D6381B9815D100AD9BC9 /* TableViewController.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 922269781DD5B8FF00E90E40 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = SOURCE_ROOT; }; 21 | 92D7D6131B98128D00AD9BC9 /* Xpandr.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Xpandr.app; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 92D7D6171B98128D00AD9BC9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23 | 92D7D6181B98128D00AD9BC9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | 92D7D61A1B98128D00AD9BC9 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 25 | 92D7D61D1B98128D00AD9BC9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 26 | 92D7D61F1B98128D00AD9BC9 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 27 | 92D7D6221B98128D00AD9BC9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 28 | 92D7D6381B9815D100AD9BC9 /* TableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; 29 | 92D845641B98DE3800EF2E0D /* screencapture.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = screencapture.gif; sourceTree = SOURCE_ROOT; }; 30 | /* End PBXFileReference section */ 31 | 32 | /* Begin PBXFrameworksBuildPhase section */ 33 | 92D7D6101B98128D00AD9BC9 /* Frameworks */ = { 34 | isa = PBXFrameworksBuildPhase; 35 | buildActionMask = 2147483647; 36 | files = ( 37 | 9289A2222D4A07CD00402CB9 /* DAExpandAnimation in Frameworks */, 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXFrameworksBuildPhase section */ 42 | 43 | /* Begin PBXGroup section */ 44 | 92D7D60A1B98128D00AD9BC9 = { 45 | isa = PBXGroup; 46 | children = ( 47 | 92D7D6151B98128D00AD9BC9 /* Xpandr */, 48 | 92D7D6141B98128D00AD9BC9 /* Products */, 49 | ); 50 | sourceTree = ""; 51 | }; 52 | 92D7D6141B98128D00AD9BC9 /* Products */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 92D7D6131B98128D00AD9BC9 /* Xpandr.app */, 56 | ); 57 | name = Products; 58 | sourceTree = ""; 59 | }; 60 | 92D7D6151B98128D00AD9BC9 /* Xpandr */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 92D7D6181B98128D00AD9BC9 /* AppDelegate.swift */, 64 | 92D7D6381B9815D100AD9BC9 /* TableViewController.swift */, 65 | 92D7D61A1B98128D00AD9BC9 /* ViewController.swift */, 66 | 92D7D61C1B98128D00AD9BC9 /* Main.storyboard */, 67 | 92D7D6161B98128D00AD9BC9 /* Supporting Files */, 68 | ); 69 | path = Xpandr; 70 | sourceTree = ""; 71 | }; 72 | 92D7D6161B98128D00AD9BC9 /* Supporting Files */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 92D7D61F1B98128D00AD9BC9 /* Images.xcassets */, 76 | 92D7D6211B98128D00AD9BC9 /* LaunchScreen.xib */, 77 | 92D7D6171B98128D00AD9BC9 /* Info.plist */, 78 | 92D845641B98DE3800EF2E0D /* screencapture.gif */, 79 | 922269781DD5B8FF00E90E40 /* README.md */, 80 | ); 81 | name = "Supporting Files"; 82 | sourceTree = ""; 83 | }; 84 | /* End PBXGroup section */ 85 | 86 | /* Begin PBXNativeTarget section */ 87 | 92D7D6121B98128D00AD9BC9 /* Xpandr */ = { 88 | isa = PBXNativeTarget; 89 | buildConfigurationList = 92D7D6321B98128D00AD9BC9 /* Build configuration list for PBXNativeTarget "Xpandr" */; 90 | buildPhases = ( 91 | 92D7D60F1B98128D00AD9BC9 /* Sources */, 92 | 92D7D6101B98128D00AD9BC9 /* Frameworks */, 93 | 92D7D6111B98128D00AD9BC9 /* Resources */, 94 | ); 95 | buildRules = ( 96 | ); 97 | dependencies = ( 98 | ); 99 | name = Xpandr; 100 | productName = Xpandr; 101 | productReference = 92D7D6131B98128D00AD9BC9 /* Xpandr.app */; 102 | productType = "com.apple.product-type.application"; 103 | }; 104 | /* End PBXNativeTarget section */ 105 | 106 | /* Begin PBXProject section */ 107 | 92D7D60B1B98128D00AD9BC9 /* Project object */ = { 108 | isa = PBXProject; 109 | attributes = { 110 | BuildIndependentTargetsInParallel = YES; 111 | LastSwiftMigration = 0700; 112 | LastSwiftUpdateCheck = 0700; 113 | LastUpgradeCheck = 1610; 114 | ORGANIZATIONNAME = "Denis Avdeev"; 115 | TargetAttributes = { 116 | 92D7D6121B98128D00AD9BC9 = { 117 | CreatedOnToolsVersion = 6.4; 118 | LastSwiftMigration = 1140; 119 | }; 120 | }; 121 | }; 122 | buildConfigurationList = 92D7D60E1B98128D00AD9BC9 /* Build configuration list for PBXProject "Xpandr" */; 123 | compatibilityVersion = "Xcode 3.2"; 124 | developmentRegion = en; 125 | hasScannedForEncodings = 0; 126 | knownRegions = ( 127 | en, 128 | Base, 129 | ); 130 | mainGroup = 92D7D60A1B98128D00AD9BC9; 131 | packageReferences = ( 132 | 9289A2202D4A07CD00402CB9 /* XCRemoteSwiftPackageReference "DAExpandAnimation" */, 133 | ); 134 | productRefGroup = 92D7D6141B98128D00AD9BC9 /* Products */; 135 | projectDirPath = ""; 136 | projectRoot = ""; 137 | targets = ( 138 | 92D7D6121B98128D00AD9BC9 /* Xpandr */, 139 | ); 140 | }; 141 | /* End PBXProject section */ 142 | 143 | /* Begin PBXResourcesBuildPhase section */ 144 | 92D7D6111B98128D00AD9BC9 /* Resources */ = { 145 | isa = PBXResourcesBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | 92D7D61E1B98128D00AD9BC9 /* Main.storyboard in Resources */, 149 | 92D7D6231B98128D00AD9BC9 /* LaunchScreen.xib in Resources */, 150 | 92D7D6201B98128D00AD9BC9 /* Images.xcassets in Resources */, 151 | ); 152 | runOnlyForDeploymentPostprocessing = 0; 153 | }; 154 | /* End PBXResourcesBuildPhase section */ 155 | 156 | /* Begin PBXSourcesBuildPhase section */ 157 | 92D7D60F1B98128D00AD9BC9 /* Sources */ = { 158 | isa = PBXSourcesBuildPhase; 159 | buildActionMask = 2147483647; 160 | files = ( 161 | 92D7D61B1B98128D00AD9BC9 /* ViewController.swift in Sources */, 162 | 92D7D6391B9815D100AD9BC9 /* TableViewController.swift in Sources */, 163 | 92D7D6191B98128D00AD9BC9 /* AppDelegate.swift in Sources */, 164 | ); 165 | runOnlyForDeploymentPostprocessing = 0; 166 | }; 167 | /* End PBXSourcesBuildPhase section */ 168 | 169 | /* Begin PBXVariantGroup section */ 170 | 92D7D61C1B98128D00AD9BC9 /* Main.storyboard */ = { 171 | isa = PBXVariantGroup; 172 | children = ( 173 | 92D7D61D1B98128D00AD9BC9 /* Base */, 174 | ); 175 | name = Main.storyboard; 176 | sourceTree = ""; 177 | }; 178 | 92D7D6211B98128D00AD9BC9 /* LaunchScreen.xib */ = { 179 | isa = PBXVariantGroup; 180 | children = ( 181 | 92D7D6221B98128D00AD9BC9 /* Base */, 182 | ); 183 | name = LaunchScreen.xib; 184 | sourceTree = ""; 185 | }; 186 | /* End PBXVariantGroup section */ 187 | 188 | /* Begin XCBuildConfiguration section */ 189 | 92D7D6301B98128D00AD9BC9 /* Debug */ = { 190 | isa = XCBuildConfiguration; 191 | buildSettings = { 192 | ALWAYS_SEARCH_USER_PATHS = NO; 193 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 194 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 195 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 196 | CLANG_CXX_LIBRARY = "libc++"; 197 | CLANG_ENABLE_MODULES = YES; 198 | CLANG_ENABLE_OBJC_ARC = YES; 199 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 200 | CLANG_WARN_BOOL_CONVERSION = YES; 201 | CLANG_WARN_COMMA = YES; 202 | CLANG_WARN_CONSTANT_CONVERSION = YES; 203 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 204 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 205 | CLANG_WARN_EMPTY_BODY = YES; 206 | CLANG_WARN_ENUM_CONVERSION = YES; 207 | CLANG_WARN_INFINITE_RECURSION = YES; 208 | CLANG_WARN_INT_CONVERSION = YES; 209 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 210 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 211 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 212 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 213 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 214 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 215 | CLANG_WARN_STRICT_PROTOTYPES = YES; 216 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 217 | CLANG_WARN_UNREACHABLE_CODE = YES; 218 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 219 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 220 | COPY_PHASE_STRIP = NO; 221 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 222 | ENABLE_STRICT_OBJC_MSGSEND = YES; 223 | ENABLE_TESTABILITY = YES; 224 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 225 | GCC_C_LANGUAGE_STANDARD = gnu99; 226 | GCC_DYNAMIC_NO_PIC = NO; 227 | GCC_NO_COMMON_BLOCKS = YES; 228 | GCC_OPTIMIZATION_LEVEL = 0; 229 | GCC_PREPROCESSOR_DEFINITIONS = ( 230 | "DEBUG=1", 231 | "$(inherited)", 232 | ); 233 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 234 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 235 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 236 | GCC_WARN_UNDECLARED_SELECTOR = YES; 237 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 238 | GCC_WARN_UNUSED_FUNCTION = YES; 239 | GCC_WARN_UNUSED_VARIABLE = YES; 240 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 241 | MTL_ENABLE_DEBUG_INFO = YES; 242 | ONLY_ACTIVE_ARCH = YES; 243 | SDKROOT = iphoneos; 244 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 245 | TARGETED_DEVICE_FAMILY = "1,2"; 246 | }; 247 | name = Debug; 248 | }; 249 | 92D7D6311B98128D00AD9BC9 /* Release */ = { 250 | isa = XCBuildConfiguration; 251 | buildSettings = { 252 | ALWAYS_SEARCH_USER_PATHS = NO; 253 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 254 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 255 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 256 | CLANG_CXX_LIBRARY = "libc++"; 257 | CLANG_ENABLE_MODULES = YES; 258 | CLANG_ENABLE_OBJC_ARC = YES; 259 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 260 | CLANG_WARN_BOOL_CONVERSION = YES; 261 | CLANG_WARN_COMMA = YES; 262 | CLANG_WARN_CONSTANT_CONVERSION = YES; 263 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 264 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 265 | CLANG_WARN_EMPTY_BODY = YES; 266 | CLANG_WARN_ENUM_CONVERSION = YES; 267 | CLANG_WARN_INFINITE_RECURSION = YES; 268 | CLANG_WARN_INT_CONVERSION = YES; 269 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 270 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 271 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 272 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 273 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 274 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 275 | CLANG_WARN_STRICT_PROTOTYPES = YES; 276 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 277 | CLANG_WARN_UNREACHABLE_CODE = YES; 278 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 279 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 280 | COPY_PHASE_STRIP = NO; 281 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 282 | ENABLE_NS_ASSERTIONS = NO; 283 | ENABLE_STRICT_OBJC_MSGSEND = YES; 284 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 285 | GCC_C_LANGUAGE_STANDARD = gnu99; 286 | GCC_NO_COMMON_BLOCKS = YES; 287 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 288 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 289 | GCC_WARN_UNDECLARED_SELECTOR = YES; 290 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 291 | GCC_WARN_UNUSED_FUNCTION = YES; 292 | GCC_WARN_UNUSED_VARIABLE = YES; 293 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 294 | MTL_ENABLE_DEBUG_INFO = NO; 295 | SDKROOT = iphoneos; 296 | SWIFT_COMPILATION_MODE = wholemodule; 297 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 298 | TARGETED_DEVICE_FAMILY = "1,2"; 299 | VALIDATE_PRODUCT = YES; 300 | }; 301 | name = Release; 302 | }; 303 | 92D7D6331B98128D00AD9BC9 /* Debug */ = { 304 | isa = XCBuildConfiguration; 305 | buildSettings = { 306 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 307 | DEVELOPMENT_TEAM = ""; 308 | INFOPLIST_FILE = Xpandr/Info.plist; 309 | LD_RUNPATH_SEARCH_PATHS = ( 310 | "$(inherited)", 311 | "@executable_path/Frameworks", 312 | ); 313 | PRODUCT_BUNDLE_IDENTIFIER = "com.denisavdeev.$(PRODUCT_NAME:rfc1034identifier)"; 314 | PRODUCT_NAME = "$(TARGET_NAME)"; 315 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 316 | SWIFT_VERSION = 5.0; 317 | }; 318 | name = Debug; 319 | }; 320 | 92D7D6341B98128D00AD9BC9 /* Release */ = { 321 | isa = XCBuildConfiguration; 322 | buildSettings = { 323 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 324 | DEVELOPMENT_TEAM = ""; 325 | INFOPLIST_FILE = Xpandr/Info.plist; 326 | LD_RUNPATH_SEARCH_PATHS = ( 327 | "$(inherited)", 328 | "@executable_path/Frameworks", 329 | ); 330 | PRODUCT_BUNDLE_IDENTIFIER = "com.denisavdeev.$(PRODUCT_NAME:rfc1034identifier)"; 331 | PRODUCT_NAME = "$(TARGET_NAME)"; 332 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 333 | SWIFT_VERSION = 5.0; 334 | }; 335 | name = Release; 336 | }; 337 | /* End XCBuildConfiguration section */ 338 | 339 | /* Begin XCConfigurationList section */ 340 | 92D7D60E1B98128D00AD9BC9 /* Build configuration list for PBXProject "Xpandr" */ = { 341 | isa = XCConfigurationList; 342 | buildConfigurations = ( 343 | 92D7D6301B98128D00AD9BC9 /* Debug */, 344 | 92D7D6311B98128D00AD9BC9 /* Release */, 345 | ); 346 | defaultConfigurationIsVisible = 0; 347 | defaultConfigurationName = Release; 348 | }; 349 | 92D7D6321B98128D00AD9BC9 /* Build configuration list for PBXNativeTarget "Xpandr" */ = { 350 | isa = XCConfigurationList; 351 | buildConfigurations = ( 352 | 92D7D6331B98128D00AD9BC9 /* Debug */, 353 | 92D7D6341B98128D00AD9BC9 /* Release */, 354 | ); 355 | defaultConfigurationIsVisible = 0; 356 | defaultConfigurationName = Release; 357 | }; 358 | /* End XCConfigurationList section */ 359 | 360 | /* Begin XCRemoteSwiftPackageReference section */ 361 | 9289A2202D4A07CD00402CB9 /* XCRemoteSwiftPackageReference "DAExpandAnimation" */ = { 362 | isa = XCRemoteSwiftPackageReference; 363 | repositoryURL = "https://github.com/ifitdoesntwork/DAExpandAnimation"; 364 | requirement = { 365 | kind = upToNextMajorVersion; 366 | minimumVersion = 1.0.0; 367 | }; 368 | }; 369 | /* End XCRemoteSwiftPackageReference section */ 370 | 371 | /* Begin XCSwiftPackageProductDependency section */ 372 | 9289A2212D4A07CD00402CB9 /* DAExpandAnimation */ = { 373 | isa = XCSwiftPackageProductDependency; 374 | package = 9289A2202D4A07CD00402CB9 /* XCRemoteSwiftPackageReference "DAExpandAnimation" */; 375 | productName = DAExpandAnimation; 376 | }; 377 | /* End XCSwiftPackageProductDependency section */ 378 | }; 379 | rootObject = 92D7D60B1B98128D00AD9BC9 /* Project object */; 380 | } 381 | -------------------------------------------------------------------------------- /Example/Xpandr.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Xpandr/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Xpandr 4 | // 5 | // Created by Denis Avdeev on 03.09.15. 6 | // Copyright (c) 2015 - 2025 Denis Avdeev. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /Example/Xpandr/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Example/Xpandr/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 | -------------------------------------------------------------------------------- /Example/Xpandr/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Example/Xpandr/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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Example/Xpandr/TableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewController.swift 3 | // Xpandr 4 | // 5 | // Created by Denis Avdeev on 03.09.15. 6 | // Copyright (c) 2015 - 2025 Denis Avdeev. All rights reserved. 7 | // 8 | 9 | import DAExpandAnimation 10 | import UIKit 11 | 12 | class TableViewController: UITableViewController, UIViewControllerTransitioningDelegate { 13 | 14 | private struct Constants { 15 | static let rowsCount = 20 16 | static let cellColors: [UIColor] = [ 17 | .systemGreen, 18 | .systemBlue, 19 | .systemOrange, 20 | .brown, 21 | .systemRed, 22 | .systemYellow, 23 | .systemTeal, 24 | .systemPink 25 | ] 26 | static let demoAnimationDuration = { 27 | Double.random(in: 0.1...1) 28 | } 29 | } 30 | 31 | // MARK: - Table view data source 32 | 33 | override func numberOfSections(in tableView: UITableView) -> Int { 34 | 1 35 | } 36 | 37 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 38 | Constants.rowsCount 39 | } 40 | 41 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 42 | let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath) 43 | 44 | let colorIndex = indexPath.row % Constants.cellColors.count 45 | cell.backgroundColor = Constants.cellColors[colorIndex] 46 | 47 | return cell 48 | } 49 | 50 | // MARK: - Navigation 51 | 52 | private let animationController = DAExpandAnimation() 53 | 54 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 55 | guard let selectedCell = sender as? UITableViewCell else { 56 | return 57 | } 58 | 59 | let toViewController = segue.destination 60 | toViewController.transitioningDelegate = self 61 | toViewController.modalPresentationStyle = .custom 62 | toViewController.view.backgroundColor = selectedCell.backgroundColor 63 | 64 | animationController.collapsedViewFrame = { 65 | selectedCell.frame 66 | } 67 | animationController.animationDuration = Constants.demoAnimationDuration() 68 | } 69 | 70 | func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 71 | animationController 72 | } 73 | 74 | func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 75 | animationController 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Example/Xpandr/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Xpandr 4 | // 5 | // Created by Denis Avdeev on 03.09.15. 6 | // Copyright (c) 2015 - 2025 Denis Avdeev. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | @IBAction func dismiss(_ sender: UITapGestureRecognizer) { 14 | if sender.state == .ended { 15 | dismiss(animated: true) 16 | } 17 | } 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Example/screencapture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifitdoesntwork/DAExpandAnimation/da38e69088b6d4635f1523c3ef1cd0674d020e0d/Example/screencapture.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015 - 2025 Denis Avdeev 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 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | // 4 | // Package.swift 5 | // Xpandr 6 | // 7 | // Created by Denis Avdeev on 18.04.2020. 8 | // Copyright © 2020-2025 Denis Avdeev. All rights reserved. 9 | // 10 | 11 | import PackageDescription 12 | 13 | let package = Package( 14 | name: "DAExpandAnimation", 15 | platforms: [ 16 | .iOS(.v8) 17 | ], 18 | products: [ 19 | .library(name: "DAExpandAnimation", targets: ["DAExpandAnimation"]) 20 | ], 21 | targets: [ 22 | .target(name: "DAExpandAnimation") 23 | ], 24 | swiftLanguageVersions: [.v5, .version("6")] 25 | ) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![SPM supported](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://swift.org/package-manager) 2 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fifitdoesntwork%2FDAExpandAnimation%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/ifitdoesntwork/DAExpandAnimation) 3 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fifitdoesntwork%2FDAExpandAnimation%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/ifitdoesntwork/DAExpandAnimation) 4 | # DAExpandAnimation 5 | A custom modal transition that presents a controller with an expanding effect while sliding out the presenter remnants. 6 | # Screenshot 7 | ![DAExpandAnimation](https://raw.githubusercontent.com/ifitdoesntwork/DAExpandAnimation/master/Example/screencapture.gif) 8 | # Installation 9 | Simply copy the `Sources/DAExpandAnimation/DAExpandAnimation.swift` file into your project. 10 | ### Swift Package Manager 11 | DAExpandAnimation is also available through [Swift Package Manager](https://github.com/apple/swift-package-manager/). 12 | ```swift 13 | .package(url: "https://github.com/ifitdoesntwork/DAExpandAnimation.git", from: "1.0.0") 14 | ``` 15 | # Usage 16 | Try the example project! 17 | 18 | Have your view controller conform to UIViewControllerTransitioningDelegate. Optionally set the `collapsedViewFrame`, the `expandedViewFrame`, the `slidingPart` and the `animationDuration`. 19 | ```swift 20 | private let animationController = DAExpandAnimation() 21 | 22 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 23 | guard let selectedCell = sender as? UITableViewCell else { 24 | return 25 | } 26 | 27 | let toViewController = segue.destination 28 | toViewController.transitioningDelegate = self 29 | toViewController.modalPresentationStyle = .custom 30 | toViewController.view.backgroundColor = selectedCell.backgroundColor 31 | 32 | animationController.collapsedViewFrame = { 33 | selectedCell.frame 34 | } 35 | animationController.animationDuration = Constants.demoAnimationDuration() 36 | } 37 | 38 | func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 39 | animationController 40 | } 41 | 42 | func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 43 | animationController 44 | } 45 | ``` 46 | # Protocols 47 | 48 | Adopting `DAExpandAnimationPresentingViewAdapter` provides the following optional delegate methods for tailoring the presenter's UX. 49 | 50 | ```swift 51 | /// Determines whether the animations include sliding the presenter's view apart. 52 | /// Defaults to `true`. 53 | var shouldSlideApart: Bool { get } 54 | 55 | /// Notifies the presenter's view adapter that animations are about to occur. 56 | func animationsWillBegin(in view: UIView, presenting isPresentation: Bool) 57 | 58 | /// Notifies the presenter's view adapter that animations are just completed. 59 | func animationsDidEnd(presenting isPresentation: Bool) 60 | ``` 61 | Adopting `DAExpandAnimationPresentedViewAdapter` provides the following optional delegate methods for tailoring the presentation of a new view controller. 62 | 63 | ```swift 64 | /// Gives the presented view adapter a chance to prepare 65 | /// the expanding `view` before the animations. 66 | func prepare(expanding view: UIView) 67 | 68 | /// Gives the presented view adapter ability to change 69 | /// properties of the expanding `view` alongside the animations. 70 | func animate(expanding view: UIView) 71 | 72 | /// Gives the presented view adapter ability to clean the expanded `view` up 73 | /// after the animations are performed. 74 | func cleanup(expanding view: UIView) 75 | 76 | /// Gives the presented view adapter a chance to prepare 77 | /// the collapsing `view` before the animations. 78 | func prepare(collapsing view: UIView) 79 | 80 | /// Gives the presented view adapter ability to change 81 | /// properties of the collapsing `view` alongside the animations. 82 | func animate(collapsing view: UIView) 83 | 84 | /// Gives the presented view adapter ability to clean the collapsed `view` 85 | /// up after the animations are performed. 86 | func cleanup(collapsing view: UIView) 87 | ``` 88 | -------------------------------------------------------------------------------- /Sources/DAExpandAnimation/DAExpandAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DAExpandAnimation.swift 3 | // 4 | // Copyright (c) 2015 - 2025 Denis Avdeev. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a 7 | // copy of this software and associated documentation files (the "Software"), 8 | // to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included 15 | // in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import UIKit 27 | 28 | public class DAExpandAnimation: NSObject { 29 | 30 | /// The delegate for adapting the presenter's view to the transition. 31 | public weak var presentingViewAdapter: DAExpandAnimationPresentingViewAdapter? 32 | 33 | /// The delegate for adapting the presented view to the transition. 34 | public weak var presentedViewAdapter: DAExpandAnimationPresentedViewAdapter? 35 | 36 | /// The frame of the view to expand, in presenter's view coordinates. 37 | /// The closure is required to get the actual frame to collapse to. 38 | /// When set to `nil`, the view expands from the center of presenter's view. 39 | public var collapsedViewFrame: (() -> CGRect)? 40 | 41 | /// Desired final frame for the expanding view, in the window coordinates. 42 | /// When set to `nil`, the view covers the whole window. 43 | public var expandedViewFrame: CGRect? 44 | 45 | /// Creates a presenter's view top or bottom sliding part 46 | /// - Parameters: 47 | /// - $0: presenter's view 48 | /// - $1: sliding part frame, in presenter's view coordinates 49 | @MainActor public var slidingPart: (UIView, CGRect) -> UIView? = { 50 | $0.resizableSnapshotView( 51 | from: $1, 52 | afterScreenUpdates: false, 53 | withCapInsets: .zero 54 | ) 55 | } 56 | 57 | /// The total duration of the animations, measured in seconds. 58 | /// Defaults to an approximation of the system modal view presentation duration. 59 | public var animationDuration = 0.24 60 | } 61 | 62 | extension DAExpandAnimation: UIViewControllerAnimatedTransitioning { 63 | 64 | public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 65 | animationDuration 66 | } 67 | 68 | public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 69 | let fromViewController = transitionContext.viewController(forKey: .from)! 70 | let toViewController = transitionContext.viewController(forKey: .to)! 71 | let isPresentation = toViewController.presentationController?.presentingViewController == fromViewController 72 | let backgroundView = (isPresentation ? fromViewController : toViewController).view! 73 | let frontView = (isPresentation ? toViewController : fromViewController).view! 74 | let inView = transitionContext.containerView 75 | 76 | // Figure the actual collapsed and expanded view frames. 77 | backgroundView.layoutIfNeeded() 78 | var collapsedFrame = collapsedViewFrame?() ?? CGRect( 79 | x: backgroundView.bounds.origin.x, 80 | y: backgroundView.bounds.midY, 81 | width: backgroundView.bounds.width, 82 | height: .zero 83 | ) 84 | if collapsedFrame.maxY < backgroundView.bounds.origin.y { 85 | collapsedFrame.origin.y = backgroundView.bounds.origin.y - collapsedFrame.height 86 | } 87 | if collapsedFrame.origin.y > backgroundView.bounds.maxY { 88 | collapsedFrame.origin.y = backgroundView.bounds.maxY 89 | } 90 | let expandedFrame = expandedViewFrame ?? inView.bounds 91 | 92 | // Create the sliding views and add them to the scene. 93 | let topSlidingViewFrame = CGRect( 94 | x: backgroundView.bounds.origin.x, 95 | y: backgroundView.bounds.origin.y, 96 | width: backgroundView.bounds.width, 97 | height: max(collapsedFrame.origin.y, backgroundView.bounds.origin.y) 98 | ) 99 | let topSlidingView = slidingPart(backgroundView, topSlidingViewFrame) 100 | topSlidingView?.frame = topSlidingViewFrame 101 | 102 | let bottomSlidingViewOriginY = min(collapsedFrame.maxY, backgroundView.bounds.maxY) 103 | let bottomSlidingViewFrame = CGRect( 104 | x: backgroundView.bounds.origin.x, 105 | y: bottomSlidingViewOriginY, 106 | width: backgroundView.bounds.width, 107 | height: backgroundView.bounds.maxY - bottomSlidingViewOriginY 108 | ) 109 | let bottomSlidingView = slidingPart(backgroundView, bottomSlidingViewFrame) 110 | bottomSlidingView?.frame = bottomSlidingViewFrame 111 | 112 | let topSlidingDistance = collapsedFrame.origin.y - backgroundView.bounds.origin.y 113 | let bottomSlidingDistance = backgroundView.bounds.maxY - collapsedFrame.maxY 114 | if !isPresentation { 115 | topSlidingView?.center.y -= topSlidingDistance 116 | bottomSlidingView?.center.y += bottomSlidingDistance 117 | } 118 | topSlidingView?.frame = backgroundView.convert(topSlidingView!.frame, to: inView) 119 | bottomSlidingView?.frame = backgroundView.convert(bottomSlidingView!.frame, to: inView) 120 | if presentingViewAdapter?.shouldSlideApart != false, let topSlidingView, let bottomSlidingView { 121 | inView.addSubview(topSlidingView) 122 | inView.addSubview(bottomSlidingView) 123 | } 124 | 125 | // Add the expanding view to the scene. 126 | inView.addSubview(frontView) 127 | collapsedFrame = backgroundView.convert(collapsedFrame, to: inView) 128 | if isPresentation { 129 | frontView.frame = collapsedFrame 130 | presentedViewAdapter?.prepare(expanding: frontView) 131 | } else { 132 | presentedViewAdapter?.prepare(collapsing: frontView) 133 | } 134 | 135 | // Slide the cell views offscreen and expand the presented view. 136 | presentingViewAdapter?.animationsWillBegin(in: inView, presenting: isPresentation) 137 | UIView.animate( 138 | withDuration: transitionDuration(using: transitionContext), 139 | animations: { 140 | if isPresentation { 141 | topSlidingView?.center.y -= topSlidingDistance 142 | bottomSlidingView?.center.y += bottomSlidingDistance 143 | frontView.frame = expandedFrame 144 | frontView.layoutIfNeeded() 145 | self.presentedViewAdapter?.animate(expanding: frontView) 146 | } else { 147 | topSlidingView?.center.y += topSlidingDistance 148 | bottomSlidingView?.center.y -= bottomSlidingDistance 149 | frontView.frame = collapsedFrame 150 | self.presentedViewAdapter?.animate(collapsing: frontView) 151 | } 152 | }, 153 | completion: { _ in 154 | topSlidingView?.removeFromSuperview() 155 | bottomSlidingView?.removeFromSuperview() 156 | if !isPresentation { 157 | frontView.removeFromSuperview() 158 | } 159 | transitionContext.completeTransition(true) 160 | self.presentingViewAdapter?.animationsDidEnd(presenting: isPresentation) 161 | if isPresentation { 162 | self.presentedViewAdapter?.cleanup(expanding: frontView) 163 | } else { 164 | self.presentedViewAdapter?.cleanup(collapsing: frontView) 165 | } 166 | } 167 | ) 168 | } 169 | 170 | } 171 | 172 | public protocol DAExpandAnimationPresentingViewAdapter: AnyObject { 173 | 174 | /// Determines whether the animations include sliding the presenter's view apart. 175 | /// Defaults to `true`. 176 | var shouldSlideApart: Bool { get } 177 | 178 | /// Notifies the presenter's view adapter that animations are about to occur. 179 | func animationsWillBegin(in view: UIView, presenting isPresentation: Bool) 180 | 181 | /// Notifies the presenter's view adapter that animations are just completed. 182 | func animationsDidEnd(presenting isPresentation: Bool) 183 | 184 | } 185 | 186 | public protocol DAExpandAnimationPresentedViewAdapter: AnyObject { 187 | 188 | /// Gives the presented view adapter a chance to prepare 189 | /// the expanding `view` before the animations. 190 | func prepare(expanding view: UIView) 191 | 192 | /// Gives the presented view adapter ability to change 193 | /// properties of the expanding `view` alongside the animations. 194 | func animate(expanding view: UIView) 195 | 196 | /// Gives the presented view adapter ability to clean the expanded `view` up 197 | /// after the animations are performed. 198 | func cleanup(expanding view: UIView) 199 | 200 | /// Gives the presented view adapter a chance to prepare 201 | /// the collapsing `view` before the animations. 202 | func prepare(collapsing view: UIView) 203 | 204 | /// Gives the presented view adapter ability to change 205 | /// properties of the collapsing `view` alongside the animations. 206 | func animate(collapsing view: UIView) 207 | 208 | /// Gives the presented view adapter ability to clean the collapsed `view` 209 | /// up after the animations are performed. 210 | func cleanup(collapsing view: UIView) 211 | 212 | } 213 | 214 | // Default protocol implementations 215 | 216 | public extension DAExpandAnimationPresentingViewAdapter { 217 | 218 | var shouldSlideApart: Bool { true } 219 | func animationsWillBegin(in view: UIView, presenting isPresentation: Bool) {} 220 | func animationsDidEnd(presenting isPresentation: Bool) {} 221 | 222 | } 223 | 224 | public extension DAExpandAnimationPresentedViewAdapter { 225 | 226 | func prepare(expanding view: UIView) {} 227 | func animate(expanding view: UIView) {} 228 | func cleanup(expanding view: UIView) {} 229 | func prepare(collapsing view: UIView) {} 230 | func animate(collapsing view: UIView) {} 231 | func cleanup(collapsing view: UIView) {} 232 | 233 | } 234 | --------------------------------------------------------------------------------