├── .clang-format ├── .codecov.yml ├── .gitignore ├── .kokoro ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MotionTransitioning.podspec ├── Podfile ├── Podfile.lock ├── README.md ├── examples ├── ContextualExample.swift ├── CustomPresentationExample.swift ├── FadeExample.h ├── FadeExample.m ├── FadeExample.swift ├── MenuExample.swift ├── NavControllerFadeExample.swift ├── PhotoAlbumExample.swift ├── PhotoAlbumTransition.swift ├── PhotoCollectionViewCell.swift ├── apps │ └── Catalog │ │ ├── Catalog │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── Info.plist │ │ └── PhotoAlbum.xcassets │ │ │ ├── image0.imageset │ │ │ ├── Contents.json │ │ │ └── image0.jpg │ │ │ ├── image1.imageset │ │ │ ├── Contents.json │ │ │ └── image1.jpg │ │ │ ├── image2.imageset │ │ │ ├── Contents.json │ │ │ └── image2.jpg │ │ │ ├── image3.imageset │ │ │ ├── Contents.json │ │ │ └── image3.jpg │ │ │ ├── image4.imageset │ │ │ ├── Contents.json │ │ │ └── image4.jpg │ │ │ ├── image5.imageset │ │ │ ├── Contents.json │ │ │ └── image5.jpg │ │ │ ├── image6.imageset │ │ │ ├── Contents.json │ │ │ └── image6.jpg │ │ │ ├── image7.imageset │ │ │ ├── Contents.json │ │ │ └── image7.jpg │ │ │ ├── image8.imageset │ │ │ ├── Contents.json │ │ │ └── image8.jpg │ │ │ └── image9.imageset │ │ │ ├── Contents.json │ │ │ └── image9.jpg │ │ ├── TableOfContents.swift │ │ ├── TestHarness │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ │ ├── TransitionsCatalog.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ ├── TransitionsCatalog.xcscheme │ │ │ └── UnitTests.xcscheme │ │ └── UnitTests │ │ └── Info.plist ├── supplemental │ ├── ExampleViewController.swift │ ├── ExampleViews.swift │ ├── HexColor.swift │ ├── Layout.swift │ ├── ModalViewController.swift │ ├── Photo.swift │ └── PhotoAlbum.swift └── transitions │ ├── CompositeTransition.swift │ ├── FadeTransition.swift │ ├── SlideUpTransition.swift │ ├── SpringFrameTransition.swift │ └── TransitionTarget.swift ├── src ├── MDMTransition.h ├── MDMTransitionContext.h ├── MDMTransitionController.h ├── MDMTransitionNavigationControllerDelegate.h ├── MDMTransitionNavigationControllerDelegate.m ├── MDMTransitionPresentationController.h ├── MDMTransitionPresentationController.m ├── MDMTransitionViewSnapshotter.h ├── MDMTransitionViewSnapshotter.m ├── MotionTransitioning.h ├── UIViewController+TransitionController.h ├── UIViewController+TransitionController.m └── private │ ├── MDMViewControllerTransitionController.h │ ├── MDMViewControllerTransitionController.m │ ├── MDMViewControllerTransitionCoordinator.h │ └── MDMViewControllerTransitionCoordinator.m └── tests └── unit ├── TransitionTests.swift ├── TransitionWithCustomDurationTests.swift ├── TransitionWithPresentationTests.swift └── Transitions ├── CompositeTransition.swift ├── FallbackTransition.swift └── InstantCompletionTransition.swift /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | 3 | AllowShortFunctionsOnASingleLine: Inline 4 | AllowShortIfStatementsOnASingleLine: false 5 | AllowShortLoopsOnASingleLine: false 6 | AlwaysBreakBeforeMultilineStrings: false 7 | BinPackParameters: false 8 | ColumnLimit: 0 9 | IndentWrappedFunctionNames: true 10 | ObjCSpaceBeforeProtocolList: true 11 | PointerBindsToType: false 12 | SortIncludes: true 13 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | ignore: 3 | - "examples/" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bazel-* 2 | .kokoro-ios-runner 3 | 4 | # Jazzy 5 | docs/ 6 | 7 | # Xcode 8 | # 9 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 10 | 11 | ## Build generated 12 | build/ 13 | DerivedData/ 14 | 15 | ## Various settings 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | xcuserdata/ 25 | 26 | ## Other 27 | *.moved-aside 28 | *.xcuserstate 29 | 30 | ## Obj-C/Swift specific 31 | *.hmap 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | ## Playgrounds 37 | timeline.xctimeline 38 | playground.xcworkspace 39 | 40 | # Swift Package Manager 41 | # 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | .build/ 45 | 46 | # CocoaPods 47 | # 48 | # We recommend against adding the Pods directory to your .gitignore. However 49 | # you should judge for yourself, the pros and cons are mentioned at: 50 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 51 | # 52 | Pods/ 53 | *.xcworkspace 54 | 55 | # Carthage 56 | # 57 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 58 | # Carthage/Checkouts 59 | 60 | Carthage/Build 61 | 62 | # fastlane 63 | # 64 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 65 | # screenshots whenever they are needed. 66 | # For more information about the recommended setup visit: 67 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 68 | 69 | fastlane/report.xml 70 | fastlane/Preview.html 71 | fastlane/screenshots 72 | fastlane/test_output 73 | -------------------------------------------------------------------------------- /.kokoro: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2017-present The Material Motion Authors. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Fail on any error. 18 | set -e 19 | 20 | # Display commands to stderr. 21 | set -x 22 | 23 | BAZEL_VERSION="0.20.0" 24 | IOS_MINIMUM_OS="9.0" 25 | IOS_CPUS="i386,x86_64" 26 | 27 | get_xcode_version_from_path() { 28 | path="$1" 29 | cat "$path/version.plist" \ 30 | | grep "CFBundleShortVersionString" -A1 \ 31 | | grep string \ 32 | | cut -d'>' -f2 \ 33 | | cut -d'<' -f1 34 | } 35 | 36 | run_bazel() { 37 | echo "Running bazel builds..." 38 | 39 | if [ -n "$KOKORO_BUILD_NUMBER" ]; then 40 | bazel version 41 | use_bazel.sh "$BAZEL_VERSION" 42 | bazel version 43 | 44 | # Move into our cloned repo 45 | cd github/repo 46 | fi 47 | 48 | # Run against whichever Xcode is currently selected. 49 | selected_xcode_developer_path=$(xcode-select -p) 50 | selected_xcode_contents_path=$(dirname "$selected_xcode_developer_path") 51 | 52 | xcode_version=$(get_xcode_version_from_path "$selected_xcode_contents_path") 53 | 54 | bazel clean 55 | bazel test //... \ 56 | --xcode_version "$xcode_version" \ 57 | --ios_minimum_os="$IOS_MINIMUM_OS" \ 58 | --ios_multi_cpus="$IOS_CPUS" 59 | } 60 | 61 | fix_bazel_imports() { 62 | if [ -z "$KOKORO_BUILD_NUMBER" ]; then 63 | repo_prefix="" 64 | else 65 | repo_prefix="github/repo/" 66 | fi 67 | 68 | # Fixes a bug in bazel where objc_library targets have a _ prefix. 69 | find "${repo_prefix}tests/unit" -type f -name '*.swift' -exec sed -i '' -E "s/import Motion(.+)/import _Motion\1/" {} + || true 70 | stashed_dir=$(pwd) 71 | reset_imports() { 72 | # Undoes our source changes from above. 73 | find "${stashed_dir}/${tests_dir_prefix}tests/unit" -type f -name '*.swift' -exec sed -i '' -E "s/import _Motion(.+)/import Motion\1/" {} + || true 74 | } 75 | trap reset_imports EXIT 76 | } 77 | 78 | fix_bazel_imports 79 | run_bazel 80 | 81 | echo "Success!" 82 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | sudo: false 3 | env: 4 | global: 5 | - LC_CTYPE=en_US.UTF-8 6 | - LANG=en_US.UTF-8 7 | - LANGUAGE=en_US.UTF-8 8 | matrix: 9 | include: 10 | - osx_image: xcode12.2 11 | env: COVERAGE=code_coverage SDK="iphonesimulator14.2" DESTINATION="name=iPhone 6s,OS=11.4" 12 | - osx_image: xcode12.2 13 | env: SDK="iphonesimulator14.2" DESTINATION="name=iPhone 6s,OS=10.3.1" 14 | before_install: 15 | - gem install cocoapods --no-document --quiet 16 | - pod install --repo-update 17 | script: 18 | - set -o pipefail 19 | - xcodebuild test -workspace MotionTransitioning.xcworkspace -scheme TransitionsCatalog -sdk "$SDK" -destination "$DESTINATION" ONLY_ACTIVE_ARCH=YES | xcpretty -c; 20 | after_success: 21 | - bash <(curl -s https://codecov.io/bash) 22 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of Material Motion Transitioning authors for copyright purposes. 2 | # 3 | # This does not necessarily list everyone who has contributed code, since in 4 | # some cases, their employer may be the copyright holder. To see the full list 5 | # of contributors, see the revision history with git log. 6 | 7 | Google Inc. 8 | and other contributors 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 8.0.0 2 | 3 | This major release drops official support for iOS 9 and bazel. 4 | 5 | # 7.0.1 6 | 7 | This patch release fixes a bug on iOS 13 where the presented view controller would not be added to 8 | the view hierarchy. 9 | 10 | ## Source changes 11 | 12 | * [Always add the toView to the container. (#68)](https://github.com/material-motion/motion-transitioning-objc/commit/0a9568b21375bb5e04e5cf10123eaa06b63f80bd) (featherless) 13 | 14 | 15 | # 7.0.0 16 | 17 | This major release drops official support for iOS 8. 18 | 19 | ## Non-source changes 20 | 21 | * [Drop support for iOS 8 (#65)](https://github.com/material-motion/motion-transitioning-objc/commit/7be635014d25dead64862822df899d46fda4f248) (featherless) 22 | 23 | # 6.0.0 24 | 25 | This major release upgrades the bazel dependencies and workspace. This change is breaking for anyone 26 | using bazel to build this library. In order to use this library with bazel, you will also need to 27 | upgrade your workspace versions to match the ones now used in this library's `WORKSPACE` file. 28 | 29 | ## Source changes 30 | 31 | * [Add missing import (#60)](https://github.com/material-motion/motion-transitioning-objc/commit/7708bb26c383b88f79a60a8737a26d12cdea498d) (Louis Romero) 32 | 33 | ## Non-source changes 34 | 35 | * [Update bazel workspace to latest versions. (#63)](https://github.com/material-motion/motion-transitioning-objc/commit/09350359468b6e5de09634a67130491761d8fffc) (featherless) 36 | 37 | # 5.0.0 38 | 39 | This major change introduces a breaking API change for Swift clients. 40 | 41 | ## Breaking changes 42 | 43 | **Swift only**: The UIViewController extension property `transitionController` has been renamed to `mdm_transitionController`. 44 | 45 | ## Source changes 46 | 47 | * [[breaking] Rename the transitionController Swift API to mdm_transitionController. (#59)](https://github.com/material-motion/motion-transitioning-objc/commit/7b3f0c28bc43ed25248fad2e197228fc815b0909) (featherless) 48 | 49 | ## API changes 50 | 51 | ### UIViewController 52 | 53 | **renamed (swift)** method: `transitionController` to `mdm_transitionController`. 54 | 55 | # 4.0.2 56 | 57 | This patch release fixes a bug where the frames of custom presented views would be incorrectly set 58 | to the view controller's frame. 59 | 60 | ## Source changes 61 | 62 | * [Add support for transitions with custom presented views. (#55)](https://github.com/material-motion/motion-transitioning-objc/commit/2564bfdf42a2ba7c550656b95bc7dc98019468bb) (featherless) 63 | 64 | ## Non-source changes 65 | 66 | * [Standardize the kokoro and bazel files. (#51)](https://github.com/material-motion/motion-transitioning-objc/commit/588b63dfae7471f3377041caa08496ef1fa74ced) (featherless) 67 | 68 | # 4.0.1 69 | 70 | This patch release resolves a build warning and migrates the project's continuous integration to 71 | bazel and kokoro. 72 | 73 | ## Source changes 74 | 75 | * [Replace arc with bazel and kokoro build runner for continuous integration. (#47)](https://github.com/material-motion/motion-transitioning-objc/commit/a40eb1667a4c5c9b13e3770c1bd98f0ca15d5b7d) (featherless) 76 | * [Reorder if statement to avoid nullability warning. (#48)](https://github.com/material-motion/motion-transitioning-objc/commit/0406d3c933574b0b9f3d7a0ae1cc3e7556640ccb) (ianegordon) 77 | 78 | # 4.0.0 79 | 80 | This major release adds support for composable transitions. See the catalog app for a variety of 81 | examples making use of this new functionality. 82 | 83 | ## Fixed issues 84 | 85 | - [Transitions would not complete if the presentation controller didn't implement the startWithContext method](https://github.com/material-motion/transitioning-objc/pull/45) 86 | 87 | ## Breaking changes 88 | 89 | - `MDMTransitionWithFallback`'s return value is now nonnull. If you depended on the nil behavior, 90 | you must now conform to the new protocol `MDMTransitionWithFeasibility` and return `NO` for 91 | `canPerformTransitionWithContext:`. 92 | - `MDMTransitionDirection` has been renamed to `TransitionDirection` in Swift. 93 | 94 | ## New features 95 | 96 | `MDMTransitionWithFeasibility` allows a transition to indicate whether it is capable of performing 97 | the transition with a given context. 98 | 99 | The new `composeWithTransition:` API on `MDMTransitionContext` makes it possible to build modular 100 | transition objects that delegate responsibility out to other transition objects. View the 101 | `PhotoAlbumTransition` example transition to see the following code in action: 102 | 103 | ```swift 104 | context.compose(with: FadeTransition(target: .foreView, style: .fadeIn)) 105 | context.compose(with: SpringFrameTransition(target: .target(snapshotContextView), 106 | size: fitSize)) 107 | 108 | if let toolbar = foreDelegate.toolbar(for: self) { 109 | context.compose(with: SlideUpTransition(target: .target(toolbar))) 110 | } 111 | ``` 112 | 113 | ## Source changes 114 | 115 | * [Add nullability annotations to MDMTransitionNavigationControllerDelegate. (#46)](https://github.com/material-motion/motion-transitioning-objc/commit/302d3c4ec526ffa942d23937fdfe8ef5163d473d) (featherless) 116 | * [Update Xcode build settings to Xcode 9 warnings and resolve build error.](https://github.com/material-motion/transitioning-objc/commit/5ed85cdc795ae6660901c5e2ae237732f04649e1) (Jeff Verkoeyen) 117 | * [Rework multi-transition support using composition. (#43)](https://github.com/material-motion/transitioning-objc/commit/0b57361557476c7d3ecb8f4c9878da21a2e735ab) (featherless) 118 | * [Fix the Swift symbol name for MDMTransitionDirection. (#44)](https://github.com/material-motion/transitioning-objc/commit/4cdcf4ca0324a1f83d572440887fe5a5d18ee00b) (featherless) 119 | * [Fix bug where transitions would not complete if the presentation controller didn't implement the startWithContext method. (#45)](https://github.com/material-motion/transitioning-objc/commit/784328dae8509df0a2beb3a5afa9701f1e275950) (featherless) 120 | * [Fix broken unit tests.](https://github.com/material-motion/transitioning-objc/commit/46c92ebcab642969ba70ea43aa512cac1cc3cad4) (Jeff Verkoeyen) 121 | * [Add multi-transition support. (#40)](https://github.com/material-motion/transitioning-objc/commit/8653958a5a9419891861fb6fd7648791ca3c744c) (featherless) 122 | * [Remove unused protocol forward declaration.](https://github.com/material-motion/transitioning-objc/commit/74c1655fc3614e5e9788db8b53e8bff83691137a) (Jeff Verkoeyen) 123 | 124 | ## API changes 125 | 126 | ### MDMTransitionWithCustomDuration 127 | 128 | *changed* protocol `MDMTransitionWithCustomDuration` now conforms to `MDMTransition`. 129 | 130 | ### MDMTransitionWithFallback 131 | 132 | *changed* protocol `MDMTransitionWithFallback` now conforms to `MDMTransition`. 133 | 134 | ### MDMTransitionWithFeasibility 135 | 136 | *new* protocol `MDMTransitionWithFeasibility`. 137 | 138 | ### MDMTransitionContext 139 | 140 | *new* method `composeWithTransition:` 141 | 142 | ## Non-source changes 143 | 144 | * [Add platform to the Podfile per pod install recommendation.](https://github.com/material-motion/transitioning-objc/commit/7384187b2ddd6a2760f5279cabb5032ea3b1e24e) (Jeff Verkoeyen) 145 | 146 | # 3.3.0 147 | 148 | This minor release deprecates some behavior and replaces it with a new API. 149 | 150 | ## New deprecations 151 | 152 | - `MDMTransitionWithFallback` nil behavior is now deprecated. In order to fall back to system 153 | transitions you must now conform to `MDMTransitionWithFeasibility` and return NO. 154 | 155 | ## Source changes 156 | 157 | * [Backport MDMTransitionWithFeasibility from the v4.0.0 release for v3.1 clients.](https://github.com/material-motion/transitioning-objc/commit/1f994d03c7971001cc8faafe61b3ed2f55bca118) (Jeff Verkoeyen) 158 | 159 | ## API changes 160 | 161 | ### MDMTransitionWithFeasibility 162 | 163 | *new* protocol `MDMTransitionWithFeasibility`. 164 | 165 | # 3.2.1 166 | 167 | This patch release resolves Xcode 9 compiler warnings. 168 | 169 | ## Source changes 170 | 171 | * [Explicitly include void for block parameters. (#41)](https://github.com/material-motion/transitioning-objc/commit/eabe53db2a113e548c876247e2c2ff3e04afc58f) (ianegordon) 172 | 173 | # 3.2.0 174 | 175 | This minor release introduces new features for presentation, view snapshotting, and defered transition work. There is also a new photo album example demonstrating how to build a contextual transition in which the context may change. 176 | 177 | ## New features 178 | 179 | Transition context now has a `deferToCompletion:` API for deferring work to the completion of the transition. 180 | 181 | ```swift 182 | // Example (Swift): 183 | foreImageView.isHidden = true 184 | context.defer { 185 | foreImageView.isHidden = false 186 | } 187 | ``` 188 | 189 | `MDMTransitionPresentationController` is a presentation controller that supports presenting view controllers at custom frames and showing an overlay scrim view. 190 | 191 | The new `MDMTransitionViewSnapshotter` class can be used to create and manage snapshot views during a transition. 192 | 193 | ```swift 194 | let snapshotter = TransitionViewSnapshotter(containerView: context.containerView) 195 | context.defer { 196 | snapshotter.removeAllSnapshots() 197 | } 198 | 199 | let snapshotView = snapshotter.snapshot(of: view, isAppearing: context.direction == .forward) 200 | ``` 201 | 202 | ## Source changes 203 | 204 | * [Add a snapshotting API and contextual transition example (#37)](https://github.com/material-motion/transitioning-objc/commit/a6ae314ddd5ff4e6f0ca9a8711348f8682d95e66) (featherless) 205 | * [Store the presentation controller as a weak reference. (#34)](https://github.com/material-motion/transitioning-objc/commit/9f73e70e382ef8291f3ad85f7ccac25994f06e43) (featherless) 206 | * [Add a stock presentation controller implementation. (#35)](https://github.com/material-motion/transitioning-objc/commit/6c98fa24f7e733262dc802b1e7c6b30134a29936) (featherless) 207 | * [Minor formatting adjustment.](https://github.com/material-motion/transitioning-objc/commit/28f6e2e72534c8e0e77b60a98140be3bc06cd37a) (Jeff Verkoeyen) 208 | 209 | ## API changes 210 | 211 | ## MDMTransitionContext 212 | 213 | *new* method: `deferToCompletion:`. Defers execution of the provided work until the completion of the transition. 214 | 215 | ## MDMTransitionPresentationController 216 | 217 | *new* class: `MDMTransitionPresentationController`. A transition presentation controller implementation that supports animation delegation, a darkened overlay view, and custom presentation frames. 218 | 219 | ## MDMTransitionViewSnapshotter 220 | 221 | *new* class: `MDMTransitionViewSnapshotter`. A view snapshotter creates visual replicas of views so that they may be animated during a transition without adversely affecting the original view hierarchy. 222 | 223 | ## Non-source changes 224 | 225 | * [Add photo album example. (#38)](https://github.com/material-motion/transitioning-objc/commit/a1d49a6f432b7fddf8d15c90a5ea185fd8e03c5a) (featherless) 226 | * [Add some organization to the transition examples. (#36)](https://github.com/material-motion/transitioning-objc/commit/27756b1e578cb8be3fa6d727a3aefafe9b1aa496) (featherless) 227 | 228 | # 3.1.0 229 | 230 | This minor release resolves a build warning and introduces the ability to customize navigation 231 | controller transitions. 232 | 233 | ## New features 234 | 235 | - MDMTransitionNavigationControllerDelegate makes it possible to customize transitions in a 236 | UINavigationController. 237 | 238 | ## Source changes 239 | 240 | * [Add transition navigation controller delegate (#29)](https://github.com/material-motion/transitioning-objc/commit/c1c212030bb8ef8abc3eaaccc315e1880b1b01a1) (featherless) 241 | * [Fix null dereference caught by the static analyzer (#30)](https://github.com/material-motion/transitioning-objc/commit/1aef0121ec4b5313ba3883a3fd3425551c19af14) (ianegordon) 242 | 243 | ## API changes 244 | 245 | ## MDMTransitionNavigationControllerDelegate 246 | 247 | *new* class: MDMTransitionNavigationControllerDelegate. 248 | 249 | # 3.0.1 250 | 251 | Fixed the umbrella header name to match the library name. 252 | 253 | # 3.0.0 (MotionTransitioning) 254 | 255 | The library has been renamed to MotionTransitioning. 256 | 257 | --- 258 | 259 | Prior releases under the library name `MaterialMotionTransitioning`. 260 | 261 | # 2.0.0 (MaterialMotionTransitioning) 262 | 263 | The library has been renamed to MaterialMotionTransitioning. 264 | 265 | ## New features 266 | 267 | - `TransitionContext` now exposes a `presentationController` property. 268 | 269 | ## Source changes 270 | 271 | * [Rename the library to MaterialMotionTransitioning.](https://github.com/material-motion/material-motion-transitioning-objc/commit/ce3e250b052fc762ed8682cd2efa9ede437707d4) (Jeff Verkoeyen) 272 | * [Expose the presentation controller in TransitionContext (#26)](https://github.com/material-motion/material-motion-transitioning-objc/commit/f643cd58b845e2b428e3ef81020c18ea7fd387f6) (Eric Tang) 273 | 274 | ## API changes 275 | 276 | Auto-generated by running: 277 | 278 | apidiff origin/stable release-candidate objc src/MaterialMotionTransitioning.h 279 | 280 | ## MDMTransitionContext 281 | 282 | *new* property: `presentationController` in `MDMTransitionContext` 283 | 284 | ## Non-source changes 285 | 286 | * [Set version to 1.0.0.](https://github.com/material-motion/material-motion-transitioning-objc/commit/5f2804b0213d7720e43152abf0893d6f5fb50048) (Jeff Verkoeyen) 287 | 288 | --- 289 | 290 | Prior releases under the library name `Transitioning`. 291 | 292 | # 1.1.1 293 | 294 | This is a patch fix release to address build issues within Google's build environment. 295 | 296 | ## Source changes 297 | 298 | * [Add missing UIKit.h header imports.](https://github.com/material-motion/transitioning-objc/commit/3b653bdd1758a5c47d277af36369e977b3774095) (Jeff Verkoeyen) 299 | 300 | ## Non-source changes 301 | 302 | * [Update Podfile.lock.](https://github.com/material-motion/transitioning-objc/commit/8185ae402e6952e2727af8b7ff0cb4c712d05623) (Jeff Verkoeyen) 303 | * [Add sliding menu as an example (#21)](https://github.com/material-motion/transitioning-objc/commit/4654e4c9c4c4ff49ac007f4b16eaa2458d86f98c) (Eric Tang) 304 | 305 | # 1.1.0 306 | 307 | This minor release introduces two new features to the Transition protocol family. 308 | 309 | ## New features 310 | 311 | * [Add support for fallback transitioning. (#16)](https://github.com/material-motion/transitioning-objc/commit/e139cc2c5bb7234df6b40cc82bfb81ded57ccbf8) (featherless) 312 | * [Add support for customizing transition durations (#11)](https://github.com/material-motion/transitioning-objc/commit/cf1e7961f51f9f07a252343bf618a45b2a00d707) (Eric Tang) 313 | 314 | ## API changes 315 | 316 | ### MDMTransitionWithFallback 317 | 318 | *new* protocol: `MDMTransitionWithFallback` 319 | 320 | *new* method: `-fallbackTransitionWithContext:` in `MDMTransitionWithFallback` 321 | 322 | ### MDMTransitionWithCustomDuration 323 | 324 | *new* protocol: `MDMTransitionWithCustomDuration` 325 | 326 | *new* method: `-transitionDurationWithContext:` in `MDMTransitionWithCustomDuration` 327 | 328 | ### MDMTransitionController 329 | 330 | *new* property: `activeTransition` in `MDMTransitionController` 331 | 332 | # 1.0.0 333 | 334 | Initial release. 335 | 336 | Includes support for building simple view controller transitions and transitions that support custom presentation. 337 | 338 | ## Source changes 339 | 340 | * [Clarify the docs for default modal presentation styles. (#4)](https://github.com/material-motion/transitioning-objc/commit/84c23e5f7c490e2a7d299cca6c4046ac4f368551) (featherless) 341 | * [Initial implementation. (#1)](https://github.com/material-motion/transitioning-objc/commit/c1b760455779226ebc9749e06e528d25a6b444bc) (featherless) 342 | 343 | ## Non-source changes 344 | 345 | * [Simplify the frame calculation APIs in the example. (#5)](https://github.com/material-motion/transitioning-objc/commit/8688b045594ee38204744c7c644d4cce58165ec6) (featherless) 346 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at 2 | the end). 3 | 4 | ### Before you contribute 5 | 6 | Before we can use your code, you must sign the 7 | [Google Individual Contributor License Agreement] 8 | (https://cla.developers.google.com/about/google-individual) 9 | (CLA), which you can do online. The CLA is necessary mainly because you own the 10 | copyright to your changes, even after your contribution becomes part of our 11 | codebase, so we need your permission to use and distribute your code. We also 12 | need to be sure of various other things—for instance that you'll tell us if you 13 | know that your code infringes on other people's patents. You don't have to sign 14 | the CLA until after you've submitted your code for review and a member has 15 | approved it, but you must do it before we can put your code into our codebase. 16 | Before you start working on a larger contribution, you should get in touch with 17 | us first through the issue tracker with your idea so that we can help out and 18 | possibly guide you. Coordinating up front makes it much easier to avoid 19 | frustration later on. 20 | 21 | ### Code reviews 22 | 23 | All submissions, including submissions by project members, require review. 24 | We use GitHub pull requests for this purpose. 25 | 26 | ### The small print 27 | 28 | Contributions made by corporations are covered by a different agreement than 29 | the one above, the 30 | [Software Grant and Corporate Contributor License Agreement] 31 | (https://cla.developers.google.com/about/google-corporate). 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MotionTransitioning.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "MotionTransitioning" 3 | s.summary = "Light-weight API for building UIViewController transitions." 4 | s.version = "8.0.0" 5 | s.authors = "The Material Motion Authors" 6 | s.license = "Apache 2.0" 7 | s.homepage = "https://github.com/material-motion/transitioning-objc" 8 | s.source = { :git => "https://github.com/material-motion/transitioning-objc.git", :tag => "v" + s.version.to_s } 9 | s.platform = :ios, "10.0" 10 | s.requires_arc = true 11 | 12 | s.public_header_files = "src/*.h" 13 | s.source_files = "src/*.{h,m,mm}", "src/private/*.{h,m,mm}" 14 | end 15 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | workspace 'MotionTransitioning.xcworkspace' 2 | use_frameworks! 3 | platform :ios, '10.0' 4 | 5 | target "TransitionsCatalog" do 6 | pod 'CatalogByConvention' 7 | pod 'MotionTransitioning', :path => './' 8 | 9 | project 'examples/apps/Catalog/TransitionsCatalog.xcodeproj' 10 | end 11 | 12 | target "UnitTests" do 13 | pod 'MotionTransitioning', :path => './' 14 | 15 | project 'examples/apps/Catalog/TransitionsCatalog.xcodeproj' 16 | end 17 | 18 | post_install do |installer| 19 | installer.pods_project.targets.each do |target| 20 | target.build_configurations.each do |configuration| 21 | configuration.build_settings['SWIFT_VERSION'] = "5.0" 22 | if target.name.start_with?("MotionTransitioning") 23 | configuration.build_settings['WARNING_CFLAGS'] ="$(inherited) -Wall -Wcast-align -Wconversion -Werror -Wextra -Wimplicit-atomic-properties -Wmissing-prototypes -Wno-sign-conversion -Wno-unused-parameter -Woverlength-strings -Wshadow -Wstrict-selector-match -Wundeclared-selector -Wunreachable-code" 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - CatalogByConvention (2.5.2) 3 | - MotionTransitioning (8.0.0) 4 | 5 | DEPENDENCIES: 6 | - CatalogByConvention 7 | - MotionTransitioning (from `./`) 8 | 9 | SPEC REPOS: 10 | trunk: 11 | - CatalogByConvention 12 | 13 | EXTERNAL SOURCES: 14 | MotionTransitioning: 15 | :path: "./" 16 | 17 | SPEC CHECKSUMS: 18 | CatalogByConvention: ef713654160053be026fa4648dd28caf6b5ca4e1 19 | MotionTransitioning: c54913ff269a0d93291bb113612c2da325f45f74 20 | 21 | PODFILE CHECKSUM: d209f6834e4d16c2a6eca09a6ddcde7e50331cda 22 | 23 | COCOAPODS: 1.10.1 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Motion Transitioning 2 | 3 | > Light-weight API for building UIViewController transitions. 4 | 5 | [![Build Status](https://travis-ci.org/material-motion/transitioning-objc.svg?branch=develop)](https://travis-ci.org/material-motion/transitioning-objc) 6 | [![codecov](https://codecov.io/gh/material-motion/transitioning-objc/branch/develop/graph/badge.svg)](https://codecov.io/gh/material-motion/transitioning-objc) 7 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/MotionTransitioning.svg)](https://cocoapods.org/pods/MotionTransitioning) 8 | [![Platform](https://img.shields.io/cocoapods/p/MotionTransitioning.svg)](http://cocoadocs.org/docsets/MotionTransitioning) 9 | [![Docs](https://img.shields.io/cocoapods/metrics/doc-percent/MotionTransitioning.svg)](http://cocoadocs.org/docsets/MotionTransitioning) 10 | 11 | This library standardizes the way transitions are built on iOS so that with a single line of code 12 | you can pick the custom transition you want to use: 13 | 14 | ```swift 15 | let viewController = MyViewController() 16 | viewController.mdm_transitionController.transition = CustomTransition() 17 | present(viewController, animated: true) 18 | ``` 19 | 20 | ```objc 21 | MyViewController *viewController = [[MyViewController alloc] init]; 22 | viewController.mdm_transitionController.transition = [[CustomTransition alloc] init]; 23 | [self presentViewController:viewController animated:true completion:nil]; 24 | ``` 25 | 26 | The easiest way to make a transition with this library is to create a class that conforms to the 27 | `Transition` protocol: 28 | 29 | ```swift 30 | final class CustomTransition: NSObject, Transition { 31 | func start(with context: TransitionContext) { 32 | CATransaction.begin() 33 | 34 | CATransaction.setCompletionBlock { 35 | context.transitionDidEnd() 36 | } 37 | 38 | // Add animations... 39 | 40 | CATransaction.commit() 41 | } 42 | } 43 | ``` 44 | 45 | ```objc 46 | @interface CustomTransition: NSObject 47 | @end 48 | 49 | @implementation CustomTransition 50 | 51 | - (void)startWithContext:(id)context { 52 | [CATransaction begin]; 53 | [CATransaction setCompletionBlock:^{ 54 | [context transitionDidEnd]; 55 | }]; 56 | 57 | // Add animations... 58 | 59 | [CATransaction commit]; 60 | } 61 | 62 | @end 63 | ``` 64 | 65 | ## Installation 66 | 67 | ### Installation with CocoaPods 68 | 69 | > CocoaPods is a dependency manager for Objective-C and Swift libraries. CocoaPods automates the 70 | > process of using third-party libraries in your projects. See 71 | > [the Getting Started guide](https://guides.cocoapods.org/using/getting-started.html) for more 72 | > information. You can install it with the following command: 73 | > 74 | > gem install cocoapods 75 | 76 | Add `MotionTransitioning` to your `Podfile`: 77 | 78 | pod 'MotionTransitioning' 79 | 80 | Then run the following command: 81 | 82 | pod install 83 | 84 | ### Usage 85 | 86 | Import the framework: 87 | 88 | @import MotionTransitioning; 89 | 90 | You will now have access to all of the APIs. 91 | 92 | ## Example apps/unit tests 93 | 94 | Check out a local copy of the repo to access the Catalog application by running the following 95 | commands: 96 | 97 | git clone https://github.com/material-motion/transitioning-objc.git 98 | cd transitioning-objc 99 | pod install 100 | open MotionTransitioning.xcworkspace 101 | 102 | ## Guides 103 | 104 | 1. [Architecture](#architecture) 105 | 2. [How to create a fade transition](#how-to-create-a-fade-transition) 106 | 3. [How to customize presentation](#how-to-customize-presentation) 107 | 4. [How to customize navigation controller transitions](#how-to-customize-navigation-controller-transitions) 108 | 109 | ### Architecture 110 | 111 | > Background: Transitions in iOS are customized by setting a `transitioningDelegate` on a view 112 | > controller. When a view controller is presented, UIKit will ask the transitioning delegate for an 113 | > animation, interaction, and presentation controller. These controllers are then expected to 114 | > implement the transition's motion. 115 | 116 | MotionTransitioning provides a thin layer atop these protocols with the following advantages: 117 | 118 | - Every view controller has its own **transition controller**. This encourages choosing the 119 | transition based on the context. 120 | - Transitions are represented in terms of **backward/forward** rather than from/to. When presenting, 121 | we're moving forward. When dismissing, we're moving backward. This allows transition code to be 122 | written with fewer conditional branches of logic. 123 | - Transition objects can customize their behavior by conforming to the family of `TransitionWith*` protocols. 124 | 125 | ### How to create a fade transition 126 | 127 | We'll create a new fade transition so that the following lines of code customizes the presentation 128 | and dismissal of our view controller: 129 | 130 | ```swift 131 | let viewController = MyViewController() 132 | viewController.mdm_transitionController.transition = FadeTransition() 133 | present(viewController, animated: true) 134 | ``` 135 | 136 | #### Step 1: Define a new Transition type 137 | 138 | A transition is an `NSObject` subclass that conforms to the `Transition` protocol. 139 | 140 | The only method you have to implement is `start(with context:)`. This method is invoked each time 141 | the associated view controller is presented or dismissed. 142 | 143 | ```swift 144 | final class FadeTransition: NSObject, Transition { 145 | func start(with context: TransitionContext) { 146 | 147 | } 148 | } 149 | ``` 150 | 151 | #### Step 2: Invoke the completion handler once all animations are complete 152 | 153 | Every transition is provided with a transition context. The transition context must be told when the 154 | transition's motion has completed so that the context can then inform UIKit of the view controller 155 | transition's completion. 156 | 157 | If using explicit Core Animation animations: 158 | 159 | ```swift 160 | final class FadeTransition: NSObject, Transition { 161 | func start(with context: TransitionContext) { 162 | CATransaction.begin() 163 | 164 | CATransaction.setCompletionBlock { 165 | context.transitionDidEnd() 166 | } 167 | 168 | // Your motion... 169 | 170 | CATransaction.commit() 171 | } 172 | } 173 | ``` 174 | 175 | If using implicit UIView animations: 176 | 177 | ```swift 178 | final class FadeTransition: NSObject, Transition { 179 | func start(with context: TransitionContext) { 180 | UIView.animate(withDuration: context.duration, animations: { 181 | // Your motion... 182 | 183 | }, completion: { didComplete in 184 | context.transitionDidEnd() 185 | }) 186 | } 187 | } 188 | ``` 189 | 190 | #### Step 3: Implement the motion 191 | 192 | With the basic scaffolding in place, you can now implement your motion. For simplicity's sake we'll 193 | use implicit UIView animations in this example to build our motion, but you're free to use any 194 | animation system you prefer. 195 | 196 | ```swift 197 | final class FadeTransition: NSObject, Transition { 198 | func start(with context: TransitionContext) { 199 | // This is a fairly rudimentary way to calculate the values on either side of the transition. 200 | // You may want to try different patterns until you find one that you prefer. 201 | // Also consider trying the MotionAnimator library provided by the Material Motion team: 202 | // https://github.com/material-motion/motion-animator-objc 203 | let backOpacity = 0 204 | let foreOpacity = 1 205 | let initialOpacity = context.direction == .forward ? backOpacity : foreOpacity 206 | let finalOpacity = context.direction == .forward ? foreOpacity : backOpacity 207 | context.foreViewController.view.alpha = initialOpacity 208 | UIView.animate(withDuration: context.duration, animations: { 209 | context.foreViewController.view.alpha = finalOpacity 210 | 211 | }, completion: { didComplete in 212 | context.transitionDidEnd() 213 | }) 214 | } 215 | } 216 | ``` 217 | 218 | ### How to customize presentation 219 | 220 | Customize the presentation of a transition when you need to do any of the following: 221 | 222 | - Add views, such as dimming views, that live beyond the lifetime of the transition. 223 | - Change the destination frame of the presented view controller. 224 | 225 | You have two options for customizing presentation: 226 | 227 | 1. Use the provided `TransitionPresentationController` API. 228 | 2. Build your own UIPresentationController subclass. 229 | 230 | #### Option 2: Subclass UIPresentationController 231 | 232 | Start by defining a new presentation controller type: 233 | 234 | ```swift 235 | final class MyPresentationController: UIPresentationController { 236 | } 237 | ``` 238 | 239 | Your Transition type must conform to `TransitionWithPresentation` in order to customize 240 | presentation. Return your custom presentation controller class from the required methods and be sure 241 | to return the `.custom` presentation style, otherwise UIKit will not use your presentation 242 | controller. 243 | 244 | ```swift 245 | extension VerticalSheetTransition: TransitionWithPresentation { 246 | func defaultModalPresentationStyle() -> UIModalPresentationStyle { 247 | return .custom 248 | } 249 | 250 | func presentationController(forPresented presented: UIViewController, 251 | presenting: UIViewController, 252 | source: UIViewController?) -> UIPresentationController? { 253 | return MyPresentationController(presentedViewController: presented, presenting: presenting) 254 | } 255 | } 256 | ``` 257 | 258 | If your presentation controller needs to animate anything, you can conform to the `Transition` 259 | protocol in order to receive a `start` invocation each time a transition begins. The presentation 260 | controller's `start` will be invoked before the transition's `start`. 261 | 262 | > Note: Just like your transition, your presentation controller must eventually call 263 | > `transitionDidEnd` on its context, otherwise your transition will not complete. This is because 264 | > the transitioning controller waits until all associated transitions have completed before 265 | > informing UIKit of the view controller transition's completion. 266 | 267 | ```swift 268 | extension MyPresentationController: Transition { 269 | func start(with context: TransitionContext) { 270 | // Your motion... 271 | } 272 | } 273 | ``` 274 | 275 | ### How to customize navigation controller transitions 276 | 277 | `UINavigationController` ignores the `transitioningDelegate` property on any view 278 | controller pushed onto or popped off of the stack, instead relying on its delegate instance to 279 | customize any transitions. This means that our `transitionController` will be 280 | ignored by a navigation controller. 281 | 282 | In order to customize individual push/pop transitions with the `transitionController`, you 283 | can make use of the `TransitionNavigationControllerDelegate` singleton class. If you 284 | assign a shared delegate to your navigation controller's delegate, your navigation controller 285 | will honor the animation and interaction settings defined by your individual view controller's 286 | `transitionController`. 287 | 288 | ```swift 289 | navigationController.delegate = TransitionNavigationControllerDelegate.sharedDelegate() 290 | 291 | // Subsequent pushes and pops will honor the pushed/popped view controller's 292 | // transitionController settings as though the view controllers were being 293 | // presented/dismissed. 294 | ``` 295 | 296 | ## Contributing 297 | 298 | We welcome contributions! 299 | 300 | Check out our [upcoming milestones](https://github.com/material-motion/transitioning-objc/milestones). 301 | 302 | Learn more about [our team](https://material-motion.github.io/material-motion/team/), 303 | [our community](https://material-motion.github.io/material-motion/team/community/), and 304 | our [contributor essentials](https://material-motion.github.io/material-motion/team/essentials/). 305 | 306 | ## License 307 | 308 | Licensed under the Apache 2.0 license. See LICENSE for details. 309 | -------------------------------------------------------------------------------- /examples/ContextualExample.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import MotionTransitioning 19 | 20 | // This example demonstrates how to build a contextual transition. 21 | 22 | class ContextualExampleViewController: ExampleViewController { 23 | 24 | @objc func didTap(_ tapGesture: UITapGestureRecognizer) { 25 | let controller = DestinationViewController() 26 | 27 | // A contextual transition is provided with information relevant to the transition, such as the 28 | // view that is being expanded/collapsed. This information can be provided at initialization 29 | // time if it is unlikely to ever change (e.g. a static view on the screen as in this example). 30 | // 31 | // If it's possible for the context to change, then a delegate pattern is a preferred solution 32 | // because it will allow the delegate to request the new context each time the transition 33 | // begins. This can be helpful in building photo album transitions, for example. 34 | // 35 | // Note that in this example we're populating the contextual transition with the tapped view. 36 | // Our rudimentary transition will animate the context view to the center of the screen from its 37 | // current location. 38 | controller.mdm_transitionController.transition = CompositeTransition(transitions: [ 39 | FadeTransition(target: .foreView), 40 | ContextualTransition(contextView: tapGesture.view!) 41 | ]) 42 | 43 | present(controller, animated: true) 44 | } 45 | 46 | override func viewDidLoad() { 47 | super.viewDidLoad() 48 | 49 | let square = UIView(frame: .init(x: 16, y: 200, width: 128, height: 128)) 50 | square.autoresizingMask = [.flexibleLeftMargin, .flexibleTopMargin, 51 | .flexibleRightMargin, .flexibleBottomMargin] 52 | square.backgroundColor = .blue 53 | view.addSubview(square) 54 | 55 | let circle = UIView(frame: .init(x: 64, y: 400, width: 128, height: 128)) 56 | circle.autoresizingMask = [.flexibleLeftMargin, .flexibleTopMargin, 57 | .flexibleRightMargin, .flexibleBottomMargin] 58 | circle.backgroundColor = .red 59 | view.addSubview(circle) 60 | 61 | square.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTap(_:)))) 62 | circle.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTap(_:)))) 63 | } 64 | 65 | override func exampleInformation() -> ExampleInfo { 66 | return .init(title: type(of: self).catalogBreadcrumbs().last!, 67 | instructions: "Tap to present a modal transition.") 68 | } 69 | } 70 | 71 | private class ContextualTransition: NSObject, Transition { 72 | 73 | // Store the context for the lifetime of the transition. 74 | let contextView: UIView 75 | init(contextView: UIView) { 76 | self.contextView = contextView 77 | } 78 | 79 | func start(with context: TransitionContext) { 80 | // A small helper function for creating bi-directional animations. 81 | // See https://github.com/material-motion/motion-animator-objc for a more versatile 82 | // bidirectional Core Animation implementation. 83 | let addAnimationToLayer: (CABasicAnimation, CALayer) -> Void = { animation, layer in 84 | if context.direction == .backward { 85 | let swap = animation.fromValue 86 | animation.fromValue = animation.toValue 87 | animation.toValue = swap 88 | } 89 | layer.add(animation, forKey: animation.keyPath) 90 | layer.setValue(animation.toValue, forKeyPath: animation.keyPath!) 91 | } 92 | 93 | let snapshotter = TransitionViewSnapshotter(containerView: context.containerView) 94 | context.defer { 95 | snapshotter.removeAllSnapshots() 96 | } 97 | 98 | CATransaction.begin() 99 | CATransaction.setCompletionBlock { 100 | context.transitionDidEnd() 101 | } 102 | 103 | // We use a snapshot view to accomplish two things: 104 | // 1) To not affect the context view's state. 105 | // 2) To allow our context view to appear in front of the fore view controller's view. 106 | // 107 | // The provided view snapshotter will automatically hide the snapshotted view and remove the 108 | // snapshot view upon completion of the transition. 109 | let snapshotContextView = snapshotter.snapshot(of: contextView, 110 | isAppearing: context.direction == .backward) 111 | 112 | let expand = CABasicAnimation(keyPath: "transform.scale.xy") 113 | expand.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 114 | expand.fromValue = 1 115 | expand.toValue = 2 116 | addAnimationToLayer(expand, snapshotContextView.layer) 117 | 118 | let shift = CASpringAnimation(keyPath: "position") 119 | shift.damping = 500 120 | shift.stiffness = 1000 121 | shift.mass = 3 122 | shift.duration = 0.5 123 | shift.fromValue = snapshotContextView.layer.position 124 | shift.toValue = CGPoint(x: context.foreViewController.view.bounds.midX, 125 | y: context.foreViewController.view.bounds.midY) 126 | addAnimationToLayer(shift, snapshotContextView.layer) 127 | 128 | context.compose(with: FadeTransition(target: .target(snapshotContextView), 129 | style: .fadeOut)) 130 | 131 | CATransaction.commit() 132 | } 133 | } 134 | 135 | private class DestinationViewController: ExampleViewController { 136 | 137 | override func viewDidLoad() { 138 | super.viewDidLoad() 139 | 140 | view.backgroundColor = .primaryColor 141 | 142 | view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTap))) 143 | } 144 | 145 | @objc func didTap() { 146 | dismiss(animated: true) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /examples/CustomPresentationExample.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import MotionTransitioning 19 | 20 | // This example demonstrates how to make use of presentation controllers to build a flexible modal 21 | // transition that supports presenting view controllers at aribtrary frames on the screen. 22 | 23 | class CustomPresentationExampleViewController: ExampleTableViewController { 24 | 25 | override init(style: UITableView.Style) { 26 | super.init(style: style) 27 | 28 | transitions = [] 29 | 30 | // Aside: we're using a simple model pattern here to define the data for the different 31 | // transitions up separate from their presentation. Check out the `didSelectRowAt` 32 | // implementation to see how we're ultimately presenting the modal view controller. 33 | 34 | // By default, the vertical sheet transition will behave like a full screen transition... 35 | transitions.append(.init(name: "Vertical sheet", transition: VerticalSheetTransition())) 36 | 37 | // ...but we can also customize the frame of the presented view controller by providing a frame 38 | // calculation block. 39 | let modalDialog = VerticalSheetTransition() 40 | modalDialog.calculateFrameOfPresentedViewInContainerView = { info in 41 | guard let containerView = info.containerView else { 42 | assertionFailure("Missing container view during frame query.") 43 | return .zero 44 | } 45 | // Note: this block is retained for the lifetime of the view controller, so be careful not to 46 | // create a memory loop by referencing self or the presented view controller directly - use 47 | // the provided info structure to access these values instead. 48 | 49 | // Center the dialog in the container view. 50 | let size = CGSize(width: 200, height: 200) 51 | return CGRect(x: (containerView.bounds.width - size.width) / 2, 52 | y: (containerView.bounds.height - size.height) / 2, 53 | width: size.width, 54 | height: size.height) 55 | } 56 | transitions.append(.init(name: "Modal dialog", transition: modalDialog)) 57 | } 58 | 59 | override func exampleInformation() -> ExampleInfo { 60 | return .init(title: type(of: self).catalogBreadcrumbs().last!, 61 | instructions: "Tap to present a modal transition.") 62 | } 63 | 64 | required init?(coder aDecoder: NSCoder) { 65 | fatalError("init(coder:) has not been implemented") 66 | } 67 | } 68 | 69 | final class VerticalSheetTransition: NSObject, Transition { 70 | 71 | // When provided, the transition will use a presentation controller to customize the presentation 72 | // of the transition. 73 | var calculateFrameOfPresentedViewInContainerView: TransitionFrameCalculation? 74 | 75 | func start(with context: TransitionContext) { 76 | CATransaction.begin() 77 | CATransaction.setCompletionBlock { 78 | context.transitionDidEnd() 79 | } 80 | 81 | let shift = CASpringAnimation(keyPath: "position.y") 82 | 83 | // These values are extracted from UIKit's default modal presentation animation. 84 | shift.damping = 500 85 | shift.stiffness = 1000 86 | shift.mass = 3 87 | shift.duration = 0.5 88 | 89 | // Start off-screen... 90 | shift.fromValue = context.containerView.bounds.height + context.foreViewController.view.layer.bounds.height / 2 91 | // ...and shift on-screen. 92 | shift.toValue = context.foreViewController.view.layer.position.y 93 | 94 | if context.direction == .backward { 95 | let swap = shift.fromValue 96 | shift.fromValue = shift.toValue 97 | shift.toValue = swap 98 | } 99 | context.foreViewController.view.layer.add(shift, forKey: shift.keyPath) 100 | context.foreViewController.view.layer.setValue(shift.toValue, forKeyPath: shift.keyPath!) 101 | 102 | CATransaction.commit() 103 | } 104 | } 105 | 106 | extension VerticalSheetTransition: TransitionWithPresentation, TransitionWithFeasibility { 107 | 108 | // We customize the transition going forward but fall back to UIKit for dismissal. Our 109 | // presentation controller will govern both of these transitions. 110 | func canPerformTransition(with context: TransitionContext) -> Bool { 111 | return context.direction == .forward 112 | } 113 | 114 | // This method is invoked when we assign the transition to the transition controller. The result 115 | // is assigned to the view controller's modalPresentationStyle property. 116 | func defaultModalPresentationStyle() -> UIModalPresentationStyle { 117 | if calculateFrameOfPresentedViewInContainerView != nil { 118 | return .custom 119 | } 120 | return .fullScreen 121 | } 122 | 123 | func presentationController(forPresented presented: UIViewController, 124 | presenting: UIViewController, 125 | source: UIViewController?) -> UIPresentationController? { 126 | if let calculateFrameOfPresentedViewInContainerView = calculateFrameOfPresentedViewInContainerView { 127 | return TransitionPresentationController(presentedViewController: presented, 128 | presenting: presenting, 129 | calculateFrameOfPresentedView: calculateFrameOfPresentedViewInContainerView) 130 | } 131 | return nil 132 | } 133 | } 134 | 135 | // MARK: Supplemental code 136 | 137 | extension CustomPresentationExampleViewController { 138 | override func viewDidLoad() { 139 | super.viewDidLoad() 140 | 141 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") 142 | } 143 | } 144 | 145 | struct TransitionInfo { 146 | let name: String 147 | let transition: Transition 148 | } 149 | var transitions: [TransitionInfo] = [] 150 | 151 | extension CustomPresentationExampleViewController { 152 | override func numberOfSections(in tableView: UITableView) -> Int { 153 | return 1 154 | } 155 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 156 | return transitions.count 157 | } 158 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 159 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 160 | cell.textLabel?.text = transitions[indexPath.row].name 161 | return cell 162 | } 163 | } 164 | 165 | extension CustomPresentationExampleViewController { 166 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 167 | let modal = ModalViewController() 168 | modal.mdm_transitionController.transition = transitions[indexPath.row].transition 169 | showDetailViewController(modal, sender: self) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /examples/FadeExample.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | @interface FadeExampleObjcViewController : UIViewController 20 | @end 21 | -------------------------------------------------------------------------------- /examples/FadeExample.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "FadeExample.h" 18 | 19 | #import "TransitionsCatalog-Swift.h" 20 | 21 | // This example demonstrates the minimal path to using a custom transition in Objective-C. 22 | 23 | @implementation FadeExampleObjcViewController 24 | 25 | - (void)didTap { 26 | ModalViewController *viewController = [[ModalViewController alloc] init]; 27 | 28 | // The transition controller is an associated object on all UIViewController instances that 29 | // allows you to customize the way the view controller is presented. The primary API on the 30 | // controller that you'll make use of is the `transitions` property. Setting this property will 31 | // dictate how the view controller is presented. For this example we've built a custom 32 | // FadeTransition, so we'll make use of that now: 33 | viewController.mdm_transitionController.transition = [[FadeTransition alloc] init]; 34 | 35 | // Note that once we assign the transition object to the view controller, the transition will 36 | // govern all subsequent presentations and dismissals of that view controller instance. If we 37 | // want to use a different transition (e.g. to use an edge-swipe-to-dismiss transition) then we 38 | // can simply change the transition object before initiating the transition. 39 | 40 | [self presentViewController:viewController animated:true completion:nil]; 41 | } 42 | 43 | - (void)viewDidLoad { 44 | [super viewDidLoad]; 45 | 46 | self.view.backgroundColor = [UIColor whiteColor]; 47 | 48 | UILabel *label = [[UILabel alloc] initWithFrame:self.view.bounds]; 49 | label.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 50 | label.textColor = [UIColor whiteColor]; 51 | label.textAlignment = NSTextAlignmentCenter; 52 | label.text = @"Tap to start the transition"; 53 | [self.view addSubview:label]; 54 | 55 | UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self 56 | action:@selector(didTap)]; 57 | [self.view addGestureRecognizer:tap]; 58 | } 59 | 60 | + (NSArray *)catalogBreadcrumbs { 61 | return @[ @"Fade transition (objc)" ]; 62 | } 63 | 64 | @end 65 | -------------------------------------------------------------------------------- /examples/FadeExample.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import MotionTransitioning 19 | 20 | // This example demonstrates the minimal path to using a custom transition in Swift. See 21 | // FadeTransition.swift for the custom transition implementation. 22 | 23 | class FadeExampleViewController: ExampleViewController { 24 | 25 | @objc func didTap() { 26 | let modalViewController = ModalViewController() 27 | 28 | // The transition controller is an associated object on all UIViewController instances that 29 | // allows you to customize the way the view controller is presented. The primary API on the 30 | // controller that you'll make use of is the `transition` property. Setting this property will 31 | // dictate how the view controller is presented. For this example we've built a custom 32 | // FadeTransition, so we'll make use of that now: 33 | modalViewController.mdm_transitionController.transition = FadeTransition(target: .foreView) 34 | 35 | // Note that once we assign the transition object to the view controller, the transition will 36 | // govern all subsequent presentations and dismissals of that view controller instance. If we 37 | // want to use a different transition (e.g. to use an edge-swipe-to-dismiss transition) then we 38 | // can simply change the transition object before initiating the transition. 39 | 40 | present(modalViewController, animated: true) 41 | } 42 | 43 | override func viewDidLoad() { 44 | super.viewDidLoad() 45 | 46 | let label = UILabel(frame: view.bounds) 47 | label.autoresizingMask = [.flexibleWidth, .flexibleHeight] 48 | label.textColor = .white 49 | label.textAlignment = .center 50 | label.text = "Tap to start the transition" 51 | view.addSubview(label) 52 | 53 | let tap = UITapGestureRecognizer(target: self, action: #selector(didTap)) 54 | view.addGestureRecognizer(tap) 55 | } 56 | 57 | override func exampleInformation() -> ExampleInfo { 58 | return .init(title: type(of: self).catalogBreadcrumbs().last!, 59 | instructions: "Tap to present a modal transition.") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/MenuExample.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import MotionTransitioning 19 | 20 | class MenuExampleViewController: ExampleViewController { 21 | 22 | @objc func didTap() { 23 | let modalViewController = ModalViewController() 24 | modalViewController.mdm_transitionController.transition = MenuTransition() 25 | present(modalViewController, animated: true) 26 | } 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | 31 | let label = UILabel(frame: view.bounds) 32 | label.autoresizingMask = [.flexibleWidth, .flexibleHeight] 33 | label.textColor = .white 34 | label.textAlignment = .center 35 | label.text = "Tap to start the transition" 36 | view.addSubview(label) 37 | 38 | let tap = UITapGestureRecognizer(target: self, action: #selector(didTap)) 39 | view.addGestureRecognizer(tap) 40 | } 41 | 42 | override func exampleInformation() -> ExampleInfo { 43 | return .init(title: type(of: self).catalogBreadcrumbs().last!, 44 | instructions: "Tap to present a modal transition.") 45 | } 46 | } 47 | 48 | private final class MenuTransition: NSObject, TransitionWithPresentation { 49 | func presentationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController?) -> UIPresentationController? { 50 | return nil 51 | } 52 | 53 | func defaultModalPresentationStyle() -> UIModalPresentationStyle { 54 | return .overCurrentContext 55 | } 56 | 57 | func start(with context: TransitionContext) { 58 | let foreView = context.foreViewController.view! 59 | if(context.direction == .forward) { 60 | foreView.frame.origin.x = -1 * foreView.frame.width 61 | UIView.animate( 62 | withDuration: context.duration, 63 | animations: { 64 | foreView.frame.origin.x = -1 * (foreView.frame.width / 2) 65 | }, 66 | completion: { _ in 67 | context.transitionDidEnd() 68 | } 69 | ) 70 | } else { 71 | UIView.animate( 72 | withDuration: context.duration, 73 | animations: { 74 | foreView.frame.origin.x = -1 * foreView.frame.width 75 | }, 76 | completion: { _ in 77 | context.transitionDidEnd() 78 | } 79 | ) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /examples/NavControllerFadeExample.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import MotionTransitioning 19 | 20 | // This example demonstrates how to build a custom UINavigationController transition using the 21 | // Motion Transitioning APIs in Swift. The essential steps have been documented below. 22 | 23 | class NavControllerFadeExampleViewController: ExampleViewController { 24 | 25 | @objc func didTap() { 26 | let modalViewController = ModalViewController() 27 | modalViewController.title = "Example view controller" 28 | 29 | // The transition controller is an associated object on all UIViewController instances that 30 | // allows you to customize the way the view controller is presented. The primary API on the 31 | // controller that you'll make use of is the `transition` property. Setting this property will 32 | // dictate how the view controller is presented. For this example we've built a custom 33 | // FadeTransition, so we'll make use of that now: 34 | modalViewController.mdm_transitionController.transition = FadeTransition(target: .foreView) 35 | 36 | cachedNavDelegate = navigationController?.delegate 37 | 38 | // In order to customize navigation controller transitions you must implement the necessary 39 | // delegate methods. By setting the shared transition navigation controller delegate instance 40 | // we're able to customize push/pop transitions using our transitionController. 41 | 42 | navigationController?.delegate = TransitionNavigationControllerDelegate.sharedDelegate() 43 | 44 | navigationController?.pushViewController(modalViewController, animated: true) 45 | } 46 | private var cachedNavDelegate: UINavigationControllerDelegate? 47 | 48 | override func viewDidDisappear(_ animated: Bool) { 49 | super.viewDidDisappear(animated) 50 | 51 | if parent == nil { // Popped off 52 | // Restore the previous delegate, if any. 53 | navigationController?.delegate = cachedNavDelegate 54 | 55 | cachedNavDelegate = nil 56 | } 57 | } 58 | 59 | override func viewDidLoad() { 60 | super.viewDidLoad() 61 | 62 | let label = UILabel(frame: view.bounds) 63 | label.autoresizingMask = [.flexibleWidth, .flexibleHeight] 64 | label.textColor = .white 65 | label.textAlignment = .center 66 | label.text = "Tap to start the transition" 67 | view.addSubview(label) 68 | 69 | let tap = UITapGestureRecognizer(target: self, action: #selector(didTap)) 70 | view.addGestureRecognizer(tap) 71 | } 72 | 73 | override func exampleInformation() -> ExampleInfo { 74 | return .init(title: type(of: self).catalogBreadcrumbs().last!, 75 | instructions: "Tap to present a modal transition.") 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /examples/PhotoAlbumExample.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import MotionTransitioning 19 | 20 | // This example demonstrates how to build a photo album contextual transition. 21 | 22 | private let photoCellIdentifier = "photoCell" 23 | 24 | public class PhotoAlbumExampleViewController: UICollectionViewController, PhotoAlbumTransitionBackDelegate { 25 | 26 | let album = PhotoAlbum() 27 | 28 | init() { 29 | super.init(collectionViewLayout: UICollectionViewFlowLayout()) 30 | } 31 | 32 | public required init?(coder aDecoder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | override public func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | collectionView!.backgroundColor = .white 40 | collectionView!.register(PhotoCollectionViewCell.self, 41 | forCellWithReuseIdentifier: photoCellIdentifier) 42 | } 43 | 44 | public override func viewDidLayoutSubviews() { 45 | super.viewDidLayoutSubviews() 46 | 47 | updateLayout() 48 | } 49 | 50 | func updateLayout() { 51 | let layout = collectionView!.collectionViewLayout as! UICollectionViewFlowLayout 52 | layout.sectionInset = .init(top: 4, left: 4, bottom: 4, right: 4) 53 | layout.minimumInteritemSpacing = 4 54 | layout.minimumLineSpacing = 4 55 | 56 | let numberOfColumns: CGFloat = 3 57 | let squareDimension = (view.bounds.width - layout.sectionInset.left - layout.sectionInset.right - (numberOfColumns - 1) * layout.minimumInteritemSpacing) / numberOfColumns 58 | layout.itemSize = CGSize(width: squareDimension, height: squareDimension) 59 | } 60 | 61 | public override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 62 | return album.photos.count 63 | } 64 | 65 | public override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 66 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: photoCellIdentifier, 67 | for: indexPath) as! PhotoCollectionViewCell 68 | let photo = album.photos[indexPath.row] 69 | cell.imageView.image = photo.image 70 | return cell 71 | } 72 | 73 | public override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 74 | let viewController = PhotoAlbumViewController(album: album) 75 | viewController.currentPhoto = album.photos[indexPath.row] 76 | viewController.mdm_transitionController.transition = PhotoAlbumTransition(backDelegate: self, 77 | foreDelegate: viewController) 78 | viewController.modalPresentationStyle = .custom 79 | present(viewController, animated: true) 80 | } 81 | 82 | func backContextView(for transition: PhotoAlbumTransition, 83 | with foreViewController: UIViewController) -> UIImageView? { 84 | let currentPhoto = (foreViewController as! PhotoAlbumViewController).currentPhoto 85 | guard let photoIndex = album.identifierToIndex[currentPhoto.uuid] else { 86 | return nil 87 | } 88 | let photoIndexPath = IndexPath(item: photoIndex, section: 0) 89 | if collectionView?.cellForItem(at: photoIndexPath) == nil { 90 | collectionView?.scrollToItem(at: photoIndexPath, at: .top, animated: false) 91 | collectionView?.reloadItems(at: [photoIndexPath]) 92 | } 93 | guard let cell = collectionView?.cellForItem(at: photoIndexPath) as? PhotoCollectionViewCell else { 94 | return nil 95 | } 96 | return cell.imageView 97 | } 98 | } 99 | 100 | class PhotoAlbumViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, PhotoAlbumTransitionForeDelegate { 101 | 102 | var collectionView: UICollectionView! 103 | let toolbar = UIToolbar() 104 | var currentPhoto: Photo 105 | 106 | let album: PhotoAlbum 107 | init(album: PhotoAlbum) { 108 | self.album = album 109 | self.currentPhoto = self.album.photos.first! 110 | 111 | super.init(nibName: nil, bundle: nil) 112 | } 113 | 114 | required init?(coder aDecoder: NSCoder) { 115 | fatalError("init(coder:) has not been implemented") 116 | } 117 | 118 | override func viewDidLoad() { 119 | super.viewDidLoad() 120 | 121 | automaticallyAdjustsScrollViewInsets = false 122 | 123 | let layout = UICollectionViewFlowLayout() 124 | layout.itemSize = view.bounds.size 125 | layout.minimumInteritemSpacing = 0 126 | layout.minimumLineSpacing = 8 127 | layout.footerReferenceSize = CGSize(width: layout.minimumLineSpacing / 2, 128 | height: view.bounds.size.height) 129 | layout.headerReferenceSize = layout.footerReferenceSize 130 | layout.scrollDirection = .horizontal 131 | 132 | collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) 133 | collectionView.isPagingEnabled = true 134 | collectionView.backgroundColor = .backgroundColor 135 | collectionView.showsHorizontalScrollIndicator = false 136 | collectionView.dataSource = self 137 | collectionView.delegate = self 138 | 139 | collectionView.register(PhotoCollectionViewCell.self, 140 | forCellWithReuseIdentifier: photoCellIdentifier) 141 | 142 | var extendedBounds = view.bounds 143 | extendedBounds.size.width = extendedBounds.width + layout.minimumLineSpacing 144 | collectionView.bounds = extendedBounds 145 | 146 | view.addSubview(collectionView) 147 | 148 | let toolbarSize = toolbar.sizeThatFits(view.bounds.size) 149 | toolbar.frame = .init(x: 0, y: view.bounds.height - toolbarSize.height, 150 | width: toolbarSize.width, height: toolbarSize.height) 151 | view.addSubview(toolbar) 152 | } 153 | 154 | override func viewDidLayoutSubviews() { 155 | super.viewDidLayoutSubviews() 156 | 157 | collectionView.center = CGPoint(x: view.bounds.midX, y: view.bounds.midY) 158 | } 159 | 160 | override func viewWillAppear(_ animated: Bool) { 161 | super.viewWillAppear(animated) 162 | 163 | navigationController?.setNavigationBarHidden(true, animated: animated) 164 | 165 | let photoIndex = album.photos.firstIndex { $0.image == currentPhoto.image }! 166 | let indexPath = IndexPath(item: photoIndex, section: 0) 167 | collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false) 168 | } 169 | 170 | override var preferredStatusBarStyle: UIStatusBarStyle { 171 | return .lightContent 172 | } 173 | 174 | // MARK: PhotoAlbumTransitionForeDelegate 175 | 176 | func foreContextView(for transition: PhotoAlbumTransition) -> UIImageView? { 177 | return (collectionView.cellForItem(at: indexPathForCurrentPhoto()) as! PhotoCollectionViewCell).imageView 178 | } 179 | 180 | func toolbar(for transition: PhotoAlbumTransition) -> UIToolbar? { 181 | return toolbar 182 | } 183 | 184 | // MARK: UICollectionViewDataSource 185 | 186 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 187 | return album.photos.count 188 | } 189 | 190 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 191 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: photoCellIdentifier, 192 | for: indexPath) as! PhotoCollectionViewCell 193 | let photo = album.photos[indexPath.row] 194 | cell.imageView.image = photo.image 195 | cell.imageView.contentMode = .scaleAspectFit 196 | return cell 197 | } 198 | 199 | // MARK: UICollectionViewDelegate 200 | 201 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 202 | dismiss(animated: true) 203 | } 204 | 205 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 206 | currentPhoto = album.photos[indexPathForCurrentPhoto().item] 207 | } 208 | 209 | // MARK: Private 210 | 211 | private func indexPathForCurrentPhoto() -> IndexPath { 212 | return collectionView.indexPathsForVisibleItems.first! 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /examples/PhotoAlbumTransition.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import UIKit 19 | import MotionTransitioning 20 | 21 | protocol PhotoAlbumTransitionForeDelegate: class { 22 | func foreContextView(for transition: PhotoAlbumTransition) -> UIImageView? 23 | func toolbar(for transition: PhotoAlbumTransition) -> UIToolbar? 24 | } 25 | 26 | protocol PhotoAlbumTransitionBackDelegate: class { 27 | func backContextView(for transition: PhotoAlbumTransition, 28 | with foreViewController: UIViewController) -> UIImageView? 29 | } 30 | 31 | final class PhotoAlbumTransition: NSObject, Transition, TransitionWithFeasibility { 32 | weak var backDelegate: PhotoAlbumTransitionBackDelegate? 33 | weak var foreDelegate: PhotoAlbumTransitionForeDelegate? 34 | init(backDelegate: PhotoAlbumTransitionBackDelegate, 35 | foreDelegate: PhotoAlbumTransitionForeDelegate) { 36 | self.backDelegate = backDelegate 37 | self.foreDelegate = foreDelegate 38 | } 39 | 40 | func canPerformTransition(with context: TransitionContext) -> Bool { 41 | guard let backDelegate = backDelegate else { 42 | return false 43 | } 44 | return backDelegate.backContextView(for: self, with: context.foreViewController) != nil 45 | } 46 | 47 | func start(with context: TransitionContext) { 48 | guard let backDelegate = backDelegate, let foreDelegate = foreDelegate else { 49 | return 50 | } 51 | guard let contextView = backDelegate.backContextView(for: self, 52 | with: context.foreViewController) else { 53 | return 54 | } 55 | guard let foreImageView = foreDelegate.foreContextView(for: self) else { 56 | return 57 | } 58 | 59 | let snapshotter = TransitionViewSnapshotter(containerView: context.containerView) 60 | context.defer { 61 | snapshotter.removeAllSnapshots() 62 | } 63 | 64 | foreImageView.isHidden = true 65 | context.defer { 66 | foreImageView.isHidden = false 67 | } 68 | 69 | let imageSize = foreImageView.image!.size 70 | 71 | let fitScale = min(foreImageView.bounds.width / imageSize.width, 72 | foreImageView.bounds.height / imageSize.height) 73 | let fitSize = CGSize(width: fitScale * imageSize.width, height: fitScale * imageSize.height) 74 | 75 | let snapshotContextView = snapshotter.snapshot(of: contextView, 76 | isAppearing: context.direction == .backward) 77 | 78 | context.compose(with: FadeTransition(target: .foreView, style: .fadeIn)) 79 | context.compose(with: SpringFrameTransition(target: .target(snapshotContextView), 80 | size: fitSize)) 81 | 82 | if let toolbar = foreDelegate.toolbar(for: self) { 83 | context.compose(with: SlideUpTransition(target: .target(toolbar))) 84 | } 85 | 86 | // This transition doesn't directly produce any animations, so we inform the context that it is 87 | // complete here, otherwise the transition would never complete: 88 | context.transitionDidEnd() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /examples/PhotoCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import UIKit 19 | 20 | class PhotoCollectionViewCell: UICollectionViewCell { 21 | let imageView = UIImageView() 22 | 23 | override init(frame: CGRect) { 24 | super.init(frame: frame) 25 | 26 | imageView.contentMode = .scaleAspectFill 27 | imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 28 | imageView.frame = bounds 29 | imageView.clipsToBounds = true 30 | 31 | contentView.addSubview(imageView) 32 | } 33 | 34 | required init?(coder aDecoder: NSCoder) { 35 | fatalError("init(coder:) has not been implemented") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import CatalogByConvention 19 | 20 | @UIApplicationMain 21 | class AppDelegate: UIResponder, UIApplicationDelegate { 22 | 23 | var window: UIWindow? 24 | 25 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 26 | let window = UIWindow(frame: UIScreen.main.bounds) 27 | self.window = window 28 | 29 | let rootViewController = CBCNodeListViewController(node: CBCCreateNavigationTree()) 30 | rootViewController.title = "Transitioning" 31 | window.rootViewController = UINavigationController(rootViewController: rootViewController) 32 | 33 | window.makeKeyAndVisible() 34 | return true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/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 | -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/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 | 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 | -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image0.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "image0.jpg" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image0.imageset/image0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-motion/motion-transitioning-objc/b976b9271ab941bafaa6e22e008dea0445a57fe2/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image0.imageset/image0.jpg -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "image1.jpg" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image1.imageset/image1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-motion/motion-transitioning-objc/b976b9271ab941bafaa6e22e008dea0445a57fe2/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image1.imageset/image1.jpg -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "image2.jpg" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image2.imageset/image2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-motion/motion-transitioning-objc/b976b9271ab941bafaa6e22e008dea0445a57fe2/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image2.imageset/image2.jpg -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "image3.jpg" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image3.imageset/image3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-motion/motion-transitioning-objc/b976b9271ab941bafaa6e22e008dea0445a57fe2/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image3.imageset/image3.jpg -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "image4.jpg" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image4.imageset/image4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-motion/motion-transitioning-objc/b976b9271ab941bafaa6e22e008dea0445a57fe2/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image4.imageset/image4.jpg -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "image5.jpg" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image5.imageset/image5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-motion/motion-transitioning-objc/b976b9271ab941bafaa6e22e008dea0445a57fe2/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image5.imageset/image5.jpg -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image6.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "image6.jpg" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image6.imageset/image6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-motion/motion-transitioning-objc/b976b9271ab941bafaa6e22e008dea0445a57fe2/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image6.imageset/image6.jpg -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image7.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "image7.jpg" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image7.imageset/image7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-motion/motion-transitioning-objc/b976b9271ab941bafaa6e22e008dea0445a57fe2/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image7.imageset/image7.jpg -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image8.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "image8.jpg" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image8.imageset/image8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-motion/motion-transitioning-objc/b976b9271ab941bafaa6e22e008dea0445a57fe2/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image8.imageset/image8.jpg -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image9.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "image9.jpg" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image9.imageset/image9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-motion/motion-transitioning-objc/b976b9271ab941bafaa6e22e008dea0445a57fe2/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image9.imageset/image9.jpg -------------------------------------------------------------------------------- /examples/apps/Catalog/TableOfContents.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | 19 | // MARK: Catalog by convention 20 | 21 | extension ContextualExampleViewController { 22 | @objc class func catalogBreadcrumbs() -> [String] { return ["Contextual transition"] } 23 | } 24 | 25 | extension FadeExampleViewController { 26 | @objc class func catalogBreadcrumbs() -> [String] { return ["Fade transition"] } 27 | } 28 | 29 | extension NavControllerFadeExampleViewController { 30 | @objc class func catalogBreadcrumbs() -> [String] { return ["Fade transition (nav controller)"] } 31 | } 32 | 33 | extension MenuExampleViewController { 34 | @objc class func catalogBreadcrumbs() -> [String] { return ["Menu transition"] } 35 | } 36 | 37 | extension PhotoAlbumExampleViewController { 38 | @objc class func catalogBreadcrumbs() -> [String] { return ["Photo album transition"] } 39 | } 40 | 41 | extension CustomPresentationExampleViewController { 42 | @objc class func catalogBreadcrumbs() -> [String] { return ["Custom presentation transitions"] } 43 | } 44 | -------------------------------------------------------------------------------- /examples/apps/Catalog/TestHarness/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | 19 | @UIApplicationMain 20 | class AppDelegate: UIResponder, UIApplicationDelegate { 21 | 22 | var window: UIWindow? 23 | 24 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 25 | let window = UIWindow(frame: UIScreen.main.bounds) 26 | self.window = window 27 | window.rootViewController = UINavigationController(rootViewController: UIViewController()) 28 | window.makeKeyAndVisible() 29 | return true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/apps/Catalog/TestHarness/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /examples/apps/Catalog/TestHarness/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 | -------------------------------------------------------------------------------- /examples/apps/Catalog/TestHarness/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 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/apps/Catalog/TransitionsCatalog.xcodeproj/xcshareddata/xcschemes/TransitionsCatalog.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 64 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /examples/apps/Catalog/TransitionsCatalog.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 37 | 38 | 44 | 45 | 47 | 48 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/apps/Catalog/UnitTests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/supplemental/ExampleViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | 19 | struct ExampleInfo { 20 | let title: String 21 | let instructions: String 22 | } 23 | 24 | class ExampleViewController: UIViewController { 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | view.backgroundColor = .backgroundColor 29 | } 30 | 31 | func exampleInformation() -> ExampleInfo { 32 | return ExampleInfo(title: "Uninitialized", instructions: "") 33 | } 34 | 35 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 36 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 37 | 38 | self.title = exampleInformation().title 39 | } 40 | 41 | required init?(coder aDecoder: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | } 45 | 46 | class ExampleTableViewController: UITableViewController { 47 | 48 | func exampleInformation() -> ExampleInfo { 49 | return ExampleInfo(title: "Uninitialized", instructions: "") 50 | } 51 | 52 | convenience init() { 53 | self.init(style: .plain) 54 | } 55 | 56 | override init(style: UITableView.Style) { 57 | super.init(style: style) 58 | 59 | self.title = exampleInformation().title 60 | } 61 | 62 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 63 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 64 | 65 | self.title = exampleInformation().title 66 | } 67 | 68 | required init?(coder aDecoder: NSCoder) { 69 | fatalError("init(coder:) has not been implemented") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/supplemental/ExampleViews.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | 19 | func createExampleView() -> UIView { 20 | let view = UIView(frame: .init(x: 0, y: 0, width: 128, height: 128)) 21 | view.backgroundColor = .primaryColor 22 | view.layer.cornerRadius = view.bounds.width / 2 23 | return view 24 | } 25 | 26 | func createExampleSquareView() -> UIView { 27 | let view = UIView(frame: .init(x: 0, y: 0, width: 128, height: 128)) 28 | view.backgroundColor = .primaryColor 29 | return view 30 | } 31 | -------------------------------------------------------------------------------- /examples/supplemental/HexColor.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import UIKit 19 | 20 | extension UIColor { 21 | private convenience init(red: Int, green: Int, blue: Int) { 22 | assert(red >= 0 && red <= 255, "Invalid red component") 23 | assert(green >= 0 && green <= 255, "Invalid green component") 24 | assert(blue >= 0 && blue <= 255, "Invalid blue component") 25 | 26 | self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0) 27 | } 28 | 29 | convenience init(hexColor: Int) { 30 | self.init(red: (hexColor >> 16) & 0xff, green: (hexColor >> 8) & 0xff, blue: hexColor & 0xff) 31 | } 32 | 33 | static var primaryColor: UIColor { 34 | return UIColor(hexColor: 0xFF80AB) 35 | } 36 | 37 | static var secondaryColor: UIColor { 38 | return UIColor(hexColor: 0xC51162) 39 | } 40 | 41 | static var backgroundColor: UIColor { 42 | return UIColor(hexColor: 0x212121) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/supplemental/Layout.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | 19 | func center(_ view: UIView, within containerView: UIView) -> UIView { 20 | let x = (containerView.bounds.width - view.bounds.width) / 2 21 | let y = (containerView.bounds.height - view.bounds.height) / 2 22 | view.frame = .init(origin: .init(x: x, y: y), size: view.bounds.size) 23 | view.autoresizingMask = [.flexibleTopMargin, .flexibleRightMargin, .flexibleBottomMargin, .flexibleLeftMargin] 24 | return view 25 | } 26 | -------------------------------------------------------------------------------- /examples/supplemental/ModalViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import UIKit 19 | 20 | class ModalViewController: ExampleViewController { 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | 25 | view.backgroundColor = .primaryColor 26 | 27 | view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTap))) 28 | 29 | let label = UILabel(frame: view.bounds) 30 | label.numberOfLines = 0 31 | label.lineBreakMode = .byWordWrapping 32 | label.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In aliquam dolor eget orci condimentum, eu blandit metus dictum. Suspendisse vitae metus pellentesque, sagittis massa vel, sodales velit. Aliquam placerat nibh et posuere interdum. Etiam fermentum purus vel turpis lobortis auctor. Curabitur auctor maximus purus, ac iaculis mi. In ac hendrerit sapien, eget porttitor risus. Integer placerat cursus viverra. Proin mollis nulla vitae nisi posuere, eu rutrum mauris condimentum. Nullam in faucibus nulla, non tincidunt lectus. Maecenas mollis massa purus, in viverra elit molestie eu. Nunc volutpat magna eget mi vestibulum pharetra. Suspendisse nulla ligula, laoreet non ante quis, vehicula facilisis libero. Morbi faucibus, sapien a convallis sodales, leo quam scelerisque leo, ut tincidunt diam velit laoreet nulla. Proin at quam vel nibh varius ultrices porta id diam. Pellentesque pretium consequat neque volutpat tristique. Sed placerat a purus ut molestie. Nullam laoreet venenatis urna non pulvinar. Proin a vestibulum nulla, eu placerat est. Morbi molestie aliquam justo, ut aliquet neque tristique consectetur. In hac habitasse platea dictumst. Fusce vehicula justo in euismod elementum. Ut vel malesuada est. Aliquam mattis, ex vel viverra eleifend, mauris nibh faucibus nibh, in fringilla sem purus vitae elit. Donec sed dapibus orci, ut vulputate sapien. Integer eu magna efficitur est pellentesque tempor. Sed ac imperdiet ex. Maecenas congue quis lacus vel dictum. Phasellus dictum mi at sollicitudin euismod. Mauris laoreet, eros vitae euismod commodo, libero ligula pretium massa, in scelerisque eros dui eu metus. Fusce elementum mauris velit, eu tempor nulla congue ut. In at tellus id quam feugiat semper eget ut felis. Nulla quis varius quam. Nullam tincidunt laoreet risus, ut aliquet nisl gravida id. Nulla iaculis mauris velit, vitae feugiat nunc scelerisque ac. Vivamus eget ligula porta, pulvinar ex vitae, sollicitudin erat. Maecenas semper ornare suscipit. Ut et neque condimentum lectus pulvinar maximus in sit amet odio. Aliquam congue purus erat, eu rutrum risus placerat a." 33 | label.autoresizingMask = [.flexibleWidth, .flexibleHeight] 34 | view.addSubview(label) 35 | } 36 | 37 | override var preferredStatusBarStyle: UIStatusBarStyle { 38 | return .lightContent 39 | } 40 | 41 | @objc func didTap() { 42 | dismiss(animated: true) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/supplemental/Photo.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import UIKit 19 | 20 | struct Photo { 21 | let name: String 22 | let image: UIImage 23 | let uuid: String 24 | 25 | init(name: String) { 26 | self.uuid = NSUUID().uuidString 27 | self.name = name 28 | 29 | // NOTE: In a real app you should never load images from disk on the UI thread like this. 30 | // Instead, you should find some way to cache the thumbnails in memory and then asynchronously 31 | // load the full-size photos from disk/network when needed. The photo library APIs provide 32 | // exactly this sort of behavior (square thumbnails are accessible immediately on the UI thread 33 | // while the full-sized photos need to be loaded asynchronously). 34 | self.image = UIImage(named: "\(self.name).jpg")! 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/supplemental/PhotoAlbum.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import UIKit 19 | 20 | let numberOfImageAssets = 10 21 | let numberOfPhotosInAlbum = 30 22 | 23 | class PhotoAlbum { 24 | let photos: [Photo] 25 | let identifierToIndex: [String: Int] 26 | 27 | init() { 28 | var photos: [Photo] = [] 29 | var identifierToIndex: [String: Int] = [:] 30 | for index in 0.. TimeInterval { 40 | let duration = transitions.compactMap { $0 as? TransitionWithCustomDuration }.map { $0.transitionDuration(with: context) }.max { $0 < $1 } 41 | if let duration = duration { 42 | return duration 43 | } 44 | return 0.35 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /examples/transitions/FadeTransition.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import MotionTransitioning 19 | 20 | // Transitions must be NSObject types that conform to the Transition protocol. 21 | final class FadeTransition: NSObject, Transition { 22 | 23 | enum Style { 24 | case fadeIn 25 | case fadeOut 26 | } 27 | 28 | let target: TransitionTarget 29 | let style: Style 30 | init(target: TransitionTarget, style: Style = .fadeIn) { 31 | self.target = target 32 | self.style = style 33 | 34 | super.init() 35 | } 36 | 37 | convenience override init() { 38 | self.init(target: .foreView) 39 | } 40 | 41 | // The sole method we're expected to implement, start is invoked each time the view controller is 42 | // presented or dismissed. 43 | func start(with context: TransitionContext) { 44 | CATransaction.begin() 45 | 46 | CATransaction.setCompletionBlock { 47 | // Let UIKit know that the transition has come to an end. 48 | context.transitionDidEnd() 49 | } 50 | 51 | let fade = CABasicAnimation(keyPath: "opacity") 52 | 53 | fade.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 54 | 55 | switch style { 56 | case .fadeIn: 57 | fade.fromValue = 0 58 | fade.toValue = 1 59 | case .fadeOut: 60 | fade.fromValue = 1 61 | fade.toValue = 0 62 | } 63 | 64 | if context.direction == .backward { 65 | let swap = fade.fromValue 66 | fade.fromValue = fade.toValue 67 | fade.toValue = swap 68 | } 69 | 70 | let targetView = target.resolve(with: context) 71 | 72 | // Add the animation... 73 | targetView.layer.add(fade, forKey: fade.keyPath) 74 | 75 | // ...and ensure that our model layer reflects the final value. 76 | targetView.layer.setValue(fade.toValue, forKeyPath: fade.keyPath!) 77 | 78 | CATransaction.commit() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /examples/transitions/SlideUpTransition.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import MotionTransitioning 19 | 20 | // Animates the target view from off the bottom of the screen to its initial position. 21 | final class SlideUpTransition: NSObject, Transition { 22 | 23 | let target: TransitionTarget 24 | init(target: TransitionTarget) { 25 | self.target = target 26 | 27 | super.init() 28 | } 29 | 30 | func start(with context: TransitionContext) { 31 | CATransaction.begin() 32 | CATransaction.setCompletionBlock { 33 | context.transitionDidEnd() 34 | } 35 | 36 | let shift = CASpringAnimation(keyPath: "position.y") 37 | 38 | // These values are extracted from UIKit's default modal presentation animation. 39 | shift.damping = 500 40 | shift.stiffness = 1000 41 | shift.mass = 3 42 | shift.duration = 0.5 43 | 44 | let snapshotter = TransitionViewSnapshotter(containerView: context.containerView) 45 | context.defer { 46 | snapshotter.removeAllSnapshots() 47 | } 48 | 49 | let snapshotTarget = snapshotter.snapshot(of: target.resolve(with: context), 50 | isAppearing: context.direction == .forward) 51 | 52 | // Start off-screen... 53 | shift.fromValue = context.containerView.bounds.height + snapshotTarget.layer.bounds.height / 2 54 | // ...and shift on-screen. 55 | shift.toValue = snapshotTarget.layer.position.y 56 | 57 | if context.direction == .backward { 58 | let swap = shift.fromValue 59 | shift.fromValue = shift.toValue 60 | shift.toValue = swap 61 | } 62 | snapshotTarget.layer.add(shift, forKey: shift.keyPath) 63 | snapshotTarget.layer.setValue(shift.toValue, forKeyPath: shift.keyPath!) 64 | 65 | CATransaction.commit() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/transitions/SpringFrameTransition.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import MotionTransitioning 19 | 20 | // A small helper function for creating bi-directional animations. 21 | // See https://github.com/material-motion/motion-animator-objc for a more versatile 22 | // bidirectional Core Animation implementation. 23 | func addAnimationToLayer(animation: CABasicAnimation, layer: CALayer, direction: TransitionDirection) { 24 | if direction == .backward { 25 | let swap = animation.fromValue 26 | animation.fromValue = animation.toValue 27 | animation.toValue = swap 28 | } 29 | layer.add(animation, forKey: animation.keyPath) 30 | layer.setValue(animation.toValue, forKeyPath: animation.keyPath!) 31 | } 32 | 33 | final class SpringFrameTransition: NSObject, Transition { 34 | 35 | let target: TransitionTarget 36 | let size: CGSize 37 | init(target: TransitionTarget, size: CGSize) { 38 | self.target = target 39 | self.size = size 40 | 41 | super.init() 42 | } 43 | 44 | func start(with context: TransitionContext) { 45 | let contextView = target.resolve(with: context) 46 | 47 | CATransaction.begin() 48 | CATransaction.setCompletionBlock { 49 | context.transitionDidEnd() 50 | } 51 | 52 | let shift = CASpringAnimation(keyPath: "position") 53 | shift.damping = 500 54 | shift.stiffness = 1000 55 | shift.mass = 3 56 | shift.duration = 0.5 57 | shift.fromValue = contextView.layer.position 58 | shift.toValue = CGPoint(x: context.foreViewController.view.bounds.midX, 59 | y: context.foreViewController.view.bounds.midY) 60 | addAnimationToLayer(animation: shift, layer: contextView.layer, direction: context.direction) 61 | 62 | let expansion = CASpringAnimation(keyPath: "bounds.size") 63 | expansion.damping = 500 64 | expansion.stiffness = 1000 65 | expansion.mass = 3 66 | expansion.duration = 0.5 67 | expansion.fromValue = contextView.layer.bounds.size 68 | expansion.toValue = size 69 | addAnimationToLayer(animation: expansion, layer: contextView.layer, direction: context.direction) 70 | 71 | CATransaction.commit() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /examples/transitions/TransitionTarget.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import MotionTransitioning 19 | 20 | // A potential target for a transition's motion. 21 | enum TransitionTarget { 22 | case backView 23 | case foreView 24 | case target(UIView) 25 | 26 | func resolve(with context: TransitionContext) -> UIView { 27 | switch self { 28 | case .backView: 29 | return context.backViewController.view 30 | case .foreView: 31 | return context.foreViewController.view 32 | case .target(let view): 33 | return view 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/MDMTransition.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | API_DEPRECATED_BEGIN("Use standard UIViewController transitioning APIs instead.", 20 | ios(12, API_TO_BE_DEPRECATED)) 21 | 22 | @protocol MDMTransitionContext; 23 | 24 | /** 25 | A transition coordinates the animated presentation or dismissal of a view controller. 26 | 27 | By default a transition is only expected to implement startWithContext: and to eventually call the 28 | context's `transitionDidEnd` method once the transition completes. 29 | 30 | A transition can opt in to extra behavior by conforming to other TransitionWith* protocols. 31 | */ 32 | NS_SWIFT_NAME(Transition) 33 | @protocol MDMTransition 34 | 35 | /** 36 | Invoked on initiation of a view controller transition. 37 | */ 38 | - (void)startWithContext:(nonnull id)context; 39 | 40 | @end 41 | 42 | /** 43 | A transition with custom duration is able to override the default transition duration. 44 | */ 45 | NS_SWIFT_NAME(TransitionWithCustomDuration) 46 | @protocol MDMTransitionWithCustomDuration 47 | 48 | /** 49 | The desired duration of this transition in seconds. 50 | */ 51 | - (NSTimeInterval)transitionDurationWithContext:(nonnull id)context; 52 | 53 | @end 54 | 55 | /** 56 | A transition with a fallback can choose to return an alternative fallback transition instance. 57 | 58 | This is most often used in cases where a transition has certain preconditions that aren't met, such 59 | as requesting a context view where none is available. 60 | */ 61 | NS_SWIFT_NAME(TransitionWithFallback) 62 | @protocol MDMTransitionWithFallback 63 | 64 | /** 65 | Asks the receiver to return a transition instance that should be used to drive this transition. 66 | 67 | If self is returned, then the receiver will be used. 68 | 69 | If a new instance is returned and the returned instance also conforms to this protocol, the 70 | returned instance will be queried for a fallback, otherwise the returned instance will be used. 71 | */ 72 | - (nonnull id)fallbackTransitionWithContext:(nonnull id)context; 73 | 74 | @end 75 | 76 | /** 77 | A transition with feasibility can indicate whether it's capable of handling a given context. 78 | */ 79 | NS_SWIFT_NAME(TransitionWithFeasibility) 80 | @protocol MDMTransitionWithFeasibility 81 | 82 | /** 83 | Asks the receiver whether it's capable of performing the transition with the given context. 84 | 85 | If NO is returned, the receiver's startWithContext: will not be invoked. 86 | If the transition is infeasible, then a default UIKit transition will be performed instead. 87 | 88 | If YES is returned, the receiver's startWithContext: will be invoked. 89 | 90 | The context's containerView will be nil during this call. 91 | 92 | If your transition composes to other transitions then it may wish to query those transitions for 93 | feasibility as well. 94 | */ 95 | - (BOOL)canPerformTransitionWithContext:(nonnull id)context; 96 | 97 | @end 98 | 99 | /** 100 | A transition with presentation is able to customize the overall presentation of the transition, 101 | including adding temporary views and changing the destination frame of the presented view 102 | controller. 103 | */ 104 | NS_SWIFT_NAME(TransitionWithPresentation) 105 | @protocol MDMTransitionWithPresentation 106 | 107 | /** 108 | The modal presentation style this transition expects to use. 109 | 110 | This method is queried when the transition is assigned to a view controller's 111 | `transitionController` transition property. The result, if any, is assigned to the view 112 | controller's `modalPresentationStyle` property. 113 | 114 | Note: In order for a presentation controller to be used the view controller's 115 | `modalPresentationStyle` must be `.custom`. 116 | 117 | If you do not wish to use a presentation controller, return anything other than 118 | `UIModalPresentationStyleCustom`. 119 | */ 120 | - (UIModalPresentationStyle)defaultModalPresentationStyle; 121 | 122 | /** 123 | Queried when the presented view controller is first presented. 124 | 125 | The returned object is cached for the lifetime of the presented view controller. 126 | 127 | If the returned object conforms to MDMTransition then its `startWithContext:` implementation will 128 | be invoked before the transition's `startWithContext:`. 129 | 130 | If nil is returned then no presentation controller will be used. 131 | */ 132 | // clang-format off 133 | - (nullable UIPresentationController *)presentationControllerForPresentedViewController:(nonnull UIViewController *)presented 134 | presentingViewController:(nonnull UIViewController *)presenting 135 | sourceViewController:(nullable UIViewController *)source 136 | NS_SWIFT_NAME(presentationController(forPresented:presenting:source:)); 137 | // clang-format on 138 | 139 | @end 140 | 141 | API_DEPRECATED_END 142 | -------------------------------------------------------------------------------- /src/MDMTransitionContext.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | API_DEPRECATED_BEGIN("Use standard UIViewController transitioning APIs instead.", 20 | ios(12, API_TO_BE_DEPRECATED)) 21 | 22 | @protocol MDMTransition; 23 | 24 | /** The possible directions of a transition. */ 25 | typedef NS_ENUM(NSUInteger, MDMTransitionDirection) { 26 | /** 27 | The fore view controller is being presented. 28 | */ 29 | MDMTransitionDirectionForward, 30 | 31 | /** 32 | The fore view controller is being dismissed. 33 | */ 34 | MDMTransitionDirectionBackward, 35 | } NS_SWIFT_NAME(TransitionDirection); 36 | 37 | /** 38 | A presentation info instance contains objects related to a transition. 39 | */ 40 | NS_SWIFT_NAME(TransitionContext) 41 | @protocol MDMTransitionContext 42 | 43 | /** 44 | Informs the context that the transition has ended. 45 | */ 46 | - (void)transitionDidEnd; 47 | 48 | /** 49 | The direction this transition is moving in. 50 | */ 51 | @property(nonatomic, readonly) MDMTransitionDirection direction; 52 | 53 | /** 54 | The duration of this transition. 55 | */ 56 | @property(nonatomic, readonly) NSTimeInterval duration; 57 | 58 | /** 59 | The source view controller for this transition. 60 | 61 | This is the view controller that initiated the transition. 62 | */ 63 | @property(nonatomic, strong, readonly, nullable) UIViewController *sourceViewController; 64 | 65 | /** 66 | The back view controller for this transition. 67 | 68 | This is the destination when the transition's direction is backward. 69 | */ 70 | @property(nonatomic, strong, readonly, nonnull) UIViewController *backViewController; 71 | 72 | /** 73 | The fore view controller for this transition. 74 | 75 | This is the destination when the transition's direction is forward. 76 | */ 77 | @property(nonatomic, strong, readonly, nonnull) UIViewController *foreViewController; 78 | 79 | /** 80 | The container view for the transition as reported by UIKit's transition context. 81 | */ 82 | @property(nonatomic, strong, readonly, nonnull) UIView *containerView; 83 | 84 | /** 85 | The presentation view controller for this transition. 86 | */ 87 | @property(nonatomic, strong, readonly, nullable) UIPresentationController *presentationController; 88 | 89 | /** 90 | Adds the provided transition as a child of the current transition and invokes its start method. 91 | 92 | Each child transition will receive its own transition context instance to which the transition must 93 | eventually invoke transitionDidEnd. Only once both the parent transition and all of its children 94 | (and their children) have completed will the overall view controller transition be completed. 95 | */ 96 | - (void)composeWithTransition:(nonnull id)transition; 97 | 98 | /** 99 | Defers execution of the provided work until the completion of the transition. 100 | 101 | Upon completion, each block of work will be executed in the order it was provided to the context. 102 | */ 103 | - (void)deferToCompletion:(void (^ _Nonnull)(void))work; 104 | 105 | @end 106 | 107 | API_DEPRECATED_END 108 | -------------------------------------------------------------------------------- /src/MDMTransitionController.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | API_DEPRECATED_BEGIN("Use standard UIViewController transitioning APIs instead.", 20 | ios(12, API_TO_BE_DEPRECATED)) 21 | 22 | @protocol MDMTransition; 23 | 24 | /** 25 | A transition controller is a bridge between UIKit's view controller transitioning APIs and 26 | Material Motion transitions. 27 | 28 | Each view controller owns its own transition controller via the mdm_transitionController property. 29 | */ 30 | NS_SWIFT_NAME(TransitionController) 31 | @protocol MDMTransitionController 32 | 33 | /** 34 | The transition instance that will govern any presentation or dismissal of the view controller. 35 | 36 | If no transition is provided then a default UIKit transition will be used. 37 | 38 | If the transition conforms to MDMTransitionWithPresentation, then the transition's default modal 39 | presentation style will be queried and assigned to the associated view controller's 40 | `modalPresentationStyle` property. 41 | */ 42 | @property(nonatomic, strong, nullable) id transition; 43 | 44 | /** 45 | The active transition instance. 46 | 47 | This may be non-nil while a transition is active. 48 | */ 49 | @property(nonatomic, strong, nullable, readonly) id activeTransition; 50 | 51 | @end 52 | 53 | API_DEPRECATED_END 54 | -------------------------------------------------------------------------------- /src/MDMTransitionNavigationControllerDelegate.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | API_DEPRECATED_BEGIN("Use standard UIViewController transitioning APIs instead.", 20 | ios(12, API_TO_BE_DEPRECATED)) 21 | 22 | /** 23 | This class provides a singleton implementation of UINavigationControllerDelegate that makes it 24 | possible to configure view controller transitions using each view controller's transition 25 | controller. 26 | 27 | This class is not meant to be instantiated directly. 28 | 29 | The +delegate should be assigned as the delegate for any UINavigationController instance that 30 | wishes to configure transitions using the mdm_transitionController (transitionController in Swift) 31 | property on a view controller. 32 | 33 | If a navigation controller already has its own delegate, then that delegate can simply forward 34 | the two necessary methods to the +sharedInstance of this class. 35 | */ 36 | NS_SWIFT_NAME(TransitionNavigationControllerDelegate) 37 | @interface MDMTransitionNavigationControllerDelegate : NSObject 38 | 39 | /** 40 | Use when directly invoking methods. 41 | 42 | Only supported methods are exposed. 43 | */ 44 | + (nonnull instancetype)sharedInstance; 45 | 46 | /** 47 | Can be set as a navigation controller's delegate. 48 | */ 49 | + (nonnull id)sharedDelegate; 50 | 51 | #pragma mark Support 52 | 53 | - (nullable id)navigationController:(nonnull UINavigationController *)navigationController 54 | animationControllerForOperation:(UINavigationControllerOperation)operation 55 | fromViewController:(nonnull UIViewController *)fromVC 56 | toViewController:(nonnull UIViewController *)toVC; 57 | - (nullable id)navigationController:(nonnull UINavigationController *)navigationController 58 | interactionControllerForAnimationController:(nonnull id)animationController; 59 | 60 | @end 61 | 62 | API_DEPRECATED_END 63 | -------------------------------------------------------------------------------- /src/MDMTransitionNavigationControllerDelegate.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMTransitionNavigationControllerDelegate.h" 18 | 19 | #import "MDMTransitionContext.h" 20 | #import "private/MDMViewControllerTransitionController.h" 21 | 22 | @interface MDMTransitionNavigationControllerDelegate () 23 | @end 24 | 25 | @implementation MDMTransitionNavigationControllerDelegate 26 | 27 | - (instancetype)init { 28 | [self doesNotRecognizeSelector:_cmd]; 29 | return nil; 30 | } 31 | 32 | - (instancetype)initInternally { 33 | return [super init]; 34 | } 35 | 36 | + (instancetype)sharedInstance { 37 | static id sharedInstance = nil; 38 | static dispatch_once_t onceToken; 39 | dispatch_once(&onceToken, ^{ 40 | sharedInstance = [[self alloc] initInternally]; 41 | }); 42 | return sharedInstance; 43 | } 44 | 45 | + (id)sharedDelegate { 46 | return [self sharedInstance]; 47 | } 48 | 49 | #pragma mark - UINavigationControllerDelegate 50 | 51 | - (id)navigationController:(UINavigationController *)navigationController 52 | animationControllerForOperation:(UINavigationControllerOperation)operation 53 | fromViewController:(UIViewController *)fromVC 54 | toViewController:(UIViewController *)toVC { 55 | id animator = nil; 56 | 57 | if (operation == UINavigationControllerOperationPush) { 58 | animator = [toVC.transitioningDelegate animationControllerForPresentedController:toVC 59 | presentingController:fromVC 60 | sourceController:navigationController]; 61 | } else { 62 | animator = [fromVC.transitioningDelegate animationControllerForDismissedController:fromVC]; 63 | } 64 | 65 | if (!animator) { 66 | // For some reason UIKit decides to stop responding to edge swipe dismiss gestures when we 67 | // customize the navigation controller delegate's animation methods. Clearing the delegate for 68 | // the interactive pop gesture recognizer re-enables this edge-swiping behavior. 69 | navigationController.interactivePopGestureRecognizer.delegate = nil; 70 | } 71 | 72 | return animator; 73 | } 74 | 75 | - (id)navigationController:(UINavigationController *)navigationController 76 | interactionControllerForAnimationController:(id)animationController { 77 | if ([animationController conformsToProtocol:@protocol(UIViewControllerInteractiveTransitioning)]) { 78 | return (id)animationController; 79 | } 80 | return nil; 81 | } 82 | 83 | @end 84 | -------------------------------------------------------------------------------- /src/MDMTransitionPresentationController.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | #import 19 | 20 | API_DEPRECATED_BEGIN("Use standard UIViewController transitioning APIs instead.", 21 | ios(12, API_TO_BE_DEPRECATED)) 22 | 23 | @protocol MDMTransitionContext; 24 | @protocol MDMTransitionPresentationAnimationControlling; 25 | 26 | NS_SWIFT_NAME(TransitionFrameCalculation) 27 | typedef CGRect (^MDMTransitionFrameCalculation)(UIPresentationController * _Nonnull); 28 | 29 | /** 30 | A transition presentation controller implementation that supports animation delegation, a darkened 31 | overlay view, and custom presentation frames. 32 | 33 | The presentation controller will create and manage the lifecycle of the scrim view, ensuring that 34 | it is removed upon a completed dismissal of the presented view controller. 35 | */ 36 | NS_SWIFT_NAME(TransitionPresentationController) 37 | @interface MDMTransitionPresentationController : UIPresentationController 38 | 39 | /** 40 | Initializes a presentation controller with the standard values and a frame calculation block. 41 | 42 | The frame calculation block is expected to return the desired frame of the presented view 43 | controller. 44 | */ 45 | - (nonnull instancetype)initWithPresentedViewController:(nonnull UIViewController *)presentedViewController 46 | presentingViewController:(nonnull UIViewController *)presentingViewController 47 | calculateFrameOfPresentedView:(nullable MDMTransitionFrameCalculation)calculateFrameOfPresentedView 48 | NS_DESIGNATED_INITIALIZER; 49 | 50 | /** 51 | The presentation controller's scrim view. 52 | */ 53 | @property(nonatomic, strong, nullable, readonly) UIView * scrimView; 54 | 55 | /** 56 | The animation controller is able to customize animations in reaction to view controller 57 | presentation and dismissal events. 58 | 59 | The animation controller is explicitly nil'd upon completion of the dismissal transition. 60 | */ 61 | @property(nonatomic, strong, nullable) id animationController; 62 | 63 | @end 64 | 65 | /** 66 | An animation controller receives additional presentation- and dismissal-related events during a 67 | view controller transition. 68 | */ 69 | NS_SWIFT_NAME(TransitionPresentationAnimationControlling) 70 | @protocol MDMTransitionPresentationAnimationControlling 71 | @optional 72 | 73 | /** 74 | Allows the receiver to register animations for the given transition context. 75 | 76 | Invoked prior to the Transition instance's startWithContext. 77 | 78 | If not implemented, the scrim view will be faded in during presentation and out during dismissal. 79 | */ 80 | - (void)presentationController:(nonnull MDMTransitionPresentationController *)presentationController 81 | startWithContext:(nonnull NSObject *)context; 82 | 83 | /** 84 | Informs the receiver that the dismissal transition is about to begin. 85 | */ 86 | - (void)dismissalTransitionWillBeginWithPresentationController:(nonnull MDMTransitionPresentationController *)presentationController; 87 | 88 | /** 89 | Informs the receiver that the dismissal transition has completed. 90 | */ 91 | - (void)presentationController:(nonnull MDMTransitionPresentationController *)presentationController 92 | dismissalTransitionDidEnd:(BOOL)completed; 93 | 94 | @end 95 | 96 | API_DEPRECATED_END 97 | -------------------------------------------------------------------------------- /src/MDMTransitionPresentationController.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMTransitionPresentationController.h" 18 | 19 | #import "MDMTransition.h" 20 | #import "MDMTransitionContext.h" 21 | #import "MDMTransitionController.h" 22 | #import "UIViewController+TransitionController.h" 23 | 24 | @interface MDMTransitionPresentationController () 25 | @end 26 | 27 | @implementation MDMTransitionPresentationController { 28 | CGRect (^_calculateFrameOfPresentedView)(UIPresentationController *); 29 | } 30 | 31 | - (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController 32 | presentingViewController:(UIViewController *)presentingViewController 33 | calculateFrameOfPresentedView:(MDMTransitionFrameCalculation)calculateFrameOfPresentedView { 34 | self = [super initWithPresentedViewController:presentedViewController 35 | presentingViewController:presentingViewController]; 36 | if (self) { 37 | _calculateFrameOfPresentedView = [calculateFrameOfPresentedView copy]; 38 | } 39 | return self; 40 | } 41 | 42 | - (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(UIViewController *)presentingViewController { 43 | return [self initWithPresentedViewController:presentedViewController 44 | presentingViewController:presentingViewController 45 | calculateFrameOfPresentedView:nil]; 46 | } 47 | 48 | - (CGRect)frameOfPresentedViewInContainerView { 49 | if (_calculateFrameOfPresentedView) { 50 | return _calculateFrameOfPresentedView(self); 51 | } else { 52 | return self.containerView.bounds; 53 | } 54 | } 55 | 56 | - (BOOL)shouldRemovePresentersView { 57 | // We don't have access to the container view when this method is called, so we can only guess as 58 | // to whether we'll be presenting full screen by checking for the presence of a frame calculation 59 | // block. 60 | BOOL definitelyFullscreen = _calculateFrameOfPresentedView == nil; 61 | 62 | // Returning true here will cause UIKit to invoke viewWillDisappear and viewDidDisappear on the 63 | // presenting view controller, and the presenting view controller's view will be removed on 64 | // completion of the transition. 65 | return definitelyFullscreen; 66 | } 67 | 68 | - (void)dismissalTransitionWillBegin { 69 | if (!self.presentedViewController.mdm_transitionController.activeTransition) { 70 | [self.presentedViewController.transitionCoordinator animateAlongsideTransition:^(id _Nonnull context) { 71 | self.scrimView.alpha = 0; 72 | } completion:nil]; 73 | 74 | if ([self.animationController respondsToSelector:@selector(dismissalTransitionWillBeginWithPresentationController:)]) { 75 | [self.animationController dismissalTransitionWillBeginWithPresentationController:self]; 76 | } 77 | } 78 | } 79 | 80 | - (void)dismissalTransitionDidEnd:(BOOL)completed { 81 | if (completed) { 82 | [self.scrimView removeFromSuperview]; 83 | _scrimView = nil; 84 | 85 | } else { 86 | self.scrimView.alpha = 1; 87 | } 88 | 89 | if ([self.animationController respondsToSelector:@selector(presentationController:dismissalTransitionDidEnd:)]) { 90 | [self.animationController presentationController:self dismissalTransitionDidEnd:completed]; 91 | } 92 | 93 | if (completed) { 94 | // Break any potential memory cycles due to our strong ownership of the animation controller. 95 | self.animationController = nil; 96 | } 97 | } 98 | 99 | - (void)startWithContext:(NSObject *)context { 100 | if (!self.scrimView) { 101 | _scrimView = [[UIView alloc] initWithFrame:context.containerView.bounds]; 102 | self.scrimView.autoresizingMask = (UIViewAutoresizingFlexibleWidth 103 | | UIViewAutoresizingFlexibleHeight); 104 | self.scrimView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.3f]; 105 | [context.containerView insertSubview:self.scrimView 106 | belowSubview:context.foreViewController.view]; 107 | } 108 | 109 | if ([self.animationController respondsToSelector:@selector(presentationController:startWithContext:)]) { 110 | [self.animationController presentationController:self startWithContext:context]; 111 | } else { 112 | self.scrimView.alpha = context.direction == MDMTransitionDirectionForward ? 0 : 1; 113 | 114 | [UIView animateWithDuration:context.duration animations:^{ 115 | self.scrimView.alpha = context.direction == MDMTransitionDirectionForward ? 1 : 0; 116 | } completion:^(BOOL finished) { 117 | [context transitionDidEnd]; 118 | }]; 119 | } 120 | } 121 | 122 | @end 123 | -------------------------------------------------------------------------------- /src/MDMTransitionViewSnapshotter.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | #import 19 | 20 | API_DEPRECATED_BEGIN("Use standard UIViewController transitioning APIs instead.", 21 | ios(12, API_TO_BE_DEPRECATED)) 22 | 23 | /** 24 | A view snapshotter creates visual replicas of views so that they may be animated during a 25 | transition without adversely affecting the original view hierarchy. 26 | */ 27 | NS_SWIFT_NAME(TransitionViewSnapshotter) 28 | @interface MDMTransitionViewSnapshotter : NSObject 29 | 30 | /** 31 | Initializes a snapshotter with a given container view. 32 | 33 | All snapshot views will be added to the container view as a direct subview. 34 | */ 35 | - (nonnull instancetype)initWithContainerView:(nonnull UIView *)containerView NS_DESIGNATED_INITIALIZER; 36 | 37 | /** 38 | Returns a snapshot view of the provided view. 39 | 40 | The snapshotter will keep a reference to the returned view in order to facilitate its eventual 41 | removal via removeAllSnapshots once the snapshot is no longer needed. 42 | 43 | @param view The view to be snapshotted. 44 | @param isAppearing If the view is appearing for the first time, a slower form of snapshotting may 45 | be used. Otherwise, fast snapshotting may be used. 46 | @return A new UIView instance that can be used as a visual replica of the provided view. 47 | */ 48 | - (nonnull UIView *)snapshotOfView:(nonnull UIView *)view isAppearing:(BOOL)isAppearing; 49 | 50 | /** 51 | Removes all snapshots from their superview and unhide the snapshotted views. 52 | */ 53 | - (void)removeAllSnapshots; 54 | 55 | /** 56 | Unavailable. Use initWithContainerView: instead. 57 | */ 58 | - (nonnull instancetype)init NS_UNAVAILABLE; 59 | 60 | @end 61 | 62 | API_DEPRECATED_END 63 | -------------------------------------------------------------------------------- /src/MDMTransitionViewSnapshotter.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMTransitionViewSnapshotter.h" 18 | 19 | static UIView *FastSnapshotOfView(UIView *view) { 20 | return [view snapshotViewAfterScreenUpdates:NO]; 21 | } 22 | 23 | static UIView *SlowSnapshotOfView(UIView *view) { 24 | UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0); 25 | [view.layer renderInContext:UIGraphicsGetCurrentContext()]; 26 | UIImage *copied = UIGraphicsGetImageFromCurrentImageContext(); 27 | UIView *copiedView = [[UIImageView alloc] initWithImage:copied]; 28 | UIGraphicsEndImageContext(); 29 | return copiedView; 30 | } 31 | 32 | @implementation MDMTransitionViewSnapshotter { 33 | UIView *_containerView; 34 | NSMutableArray *_snapshotViews; 35 | NSMutableArray *_hiddenViews; 36 | } 37 | 38 | - (void)dealloc { 39 | for (UIView *view in _snapshotViews) { 40 | [view removeFromSuperview]; 41 | } 42 | for (UIView *view in _hiddenViews) { 43 | view.hidden = NO; 44 | } 45 | } 46 | 47 | - (instancetype)initWithContainerView:(UIView *)containerView { 48 | self = [super init]; 49 | if (self) { 50 | _containerView = containerView; 51 | 52 | _snapshotViews = [NSMutableArray array]; 53 | _hiddenViews = [NSMutableArray array]; 54 | } 55 | return self; 56 | } 57 | 58 | - (UIView *)snapshotOfView:(UIView *)view isAppearing:(BOOL)isAppearing { 59 | UIView *snapshotView; 60 | if ([view isKindOfClass:[UIImageView class]]) { 61 | snapshotView = [self richReplicaOfImageView:(UIImageView *)view]; 62 | 63 | } else { 64 | snapshotView = isAppearing ? SlowSnapshotOfView(view) : FastSnapshotOfView(view); 65 | } 66 | 67 | snapshotView.layer.borderColor = view.layer.borderColor; 68 | snapshotView.layer.borderWidth = view.layer.borderWidth; 69 | snapshotView.layer.cornerRadius = view.layer.cornerRadius; 70 | snapshotView.layer.shadowColor = view.layer.shadowColor; 71 | snapshotView.layer.shadowOffset = view.layer.shadowOffset; 72 | snapshotView.layer.shadowOpacity = view.layer.shadowOpacity; 73 | snapshotView.layer.shadowPath = view.layer.shadowPath; 74 | snapshotView.layer.shadowRadius = view.layer.shadowRadius; 75 | 76 | snapshotView.layer.position = [_containerView convertPoint:view.layer.position fromView:view.superview]; 77 | snapshotView.layer.bounds = view.layer.bounds; 78 | snapshotView.layer.transform = view.layer.transform; 79 | 80 | [_containerView addSubview:snapshotView]; 81 | [_snapshotViews addObject:snapshotView]; 82 | 83 | [_hiddenViews addObject:view]; 84 | view.hidden = YES; 85 | 86 | return snapshotView; 87 | } 88 | 89 | - (void)removeAllSnapshots { 90 | for (UIView *view in _snapshotViews) { 91 | [view removeFromSuperview]; 92 | } 93 | for (UIView *view in _hiddenViews) { 94 | view.hidden = NO; 95 | } 96 | 97 | [_snapshotViews removeAllObjects]; 98 | [_hiddenViews removeAllObjects]; 99 | } 100 | 101 | #pragma mark - Private 102 | 103 | - (UIView *)richReplicaOfImageView:(UIImageView *)imageView { 104 | UIImageView *copiedImageView = [[UIImageView alloc] init]; 105 | 106 | copiedImageView.image = imageView.image; 107 | copiedImageView.highlightedImage = imageView.highlightedImage; 108 | 109 | copiedImageView.animationImages = imageView.animationImages; 110 | copiedImageView.highlightedAnimationImages = imageView.highlightedAnimationImages; 111 | copiedImageView.animationDuration = imageView.animationDuration; 112 | copiedImageView.animationRepeatCount = imageView.animationRepeatCount; 113 | 114 | [self copyPropertiesFrom:imageView toView:copiedImageView]; 115 | 116 | return copiedImageView; 117 | } 118 | 119 | - (void)copyPropertiesFrom:(UIView *)view toView:(UIView *)copiedView { 120 | copiedView.clipsToBounds = view.clipsToBounds; 121 | copiedView.backgroundColor = view.backgroundColor; 122 | copiedView.alpha = view.alpha; 123 | copiedView.opaque = view.isOpaque; 124 | copiedView.clearsContextBeforeDrawing = view.clearsContextBeforeDrawing; 125 | copiedView.hidden = view.isHidden; 126 | copiedView.contentMode = view.contentMode; 127 | copiedView.maskView = view.maskView; 128 | copiedView.tintColor = view.tintColor; 129 | copiedView.userInteractionEnabled = view.isUserInteractionEnabled; 130 | } 131 | 132 | @end 133 | -------------------------------------------------------------------------------- /src/MotionTransitioning.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMTransition.h" 18 | #import "MDMTransitionContext.h" 19 | #import "MDMTransitionController.h" 20 | #import "MDMTransitionNavigationControllerDelegate.h" 21 | #import "MDMTransitionPresentationController.h" 22 | #import "MDMTransitionViewSnapshotter.h" 23 | #import "UIViewController+TransitionController.h" 24 | -------------------------------------------------------------------------------- /src/UIViewController+TransitionController.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | #import 19 | 20 | API_DEPRECATED_BEGIN("Use standard UIViewController transitioning APIs instead.", 21 | ios(12, API_TO_BE_DEPRECATED)) 22 | 23 | @protocol MDMTransitionController; 24 | 25 | @interface UIViewController (MDMTransitionController) 26 | 27 | /** 28 | A transition controller may be used to implement custom transitions. 29 | 30 | The transition controller is lazily created upon access. 31 | 32 | Side effects: If the view controller's transitioningDelegate is nil when the controller is created, 33 | then the controller will also be set to the transitioningDelegate property. 34 | */ 35 | @property(nonatomic, strong, readonly, nonnull) id mdm_transitionController; 36 | 37 | @end 38 | 39 | API_DEPRECATED_END 40 | -------------------------------------------------------------------------------- /src/UIViewController+TransitionController.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "UIViewController+TransitionController.h" 18 | 19 | #import "private/MDMViewControllerTransitionController.h" 20 | 21 | #import 22 | 23 | @implementation UIViewController (MDMTransitionController) 24 | 25 | #pragma mark - Public 26 | 27 | - (id)mdm_transitionController { 28 | const void *key = [self mdm_transitionControllerKey]; 29 | 30 | MDMViewControllerTransitionController *controller = objc_getAssociatedObject(self, key); 31 | if (!controller) { 32 | controller = [[MDMViewControllerTransitionController alloc] initWithViewController:self]; 33 | [self mdm_setTransitionController:controller]; 34 | } 35 | return controller; 36 | } 37 | 38 | #pragma mark - Private 39 | 40 | - (void)mdm_setTransitionController:(MDMViewControllerTransitionController *)controller { 41 | const void *key = [self mdm_transitionControllerKey]; 42 | 43 | // Clear the previous delegate if we'd previously set one. 44 | MDMViewControllerTransitionController *existingController = objc_getAssociatedObject(self, key); 45 | id delegate = self.transitioningDelegate; 46 | if (existingController == delegate) { 47 | self.transitioningDelegate = nil; 48 | } 49 | 50 | objc_setAssociatedObject(self, key, controller, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 51 | 52 | if (!delegate) { 53 | self.transitioningDelegate = controller; 54 | } 55 | } 56 | 57 | - (const void *)mdm_transitionControllerKey { 58 | return @selector(mdm_transitionController); 59 | } 60 | 61 | @end 62 | -------------------------------------------------------------------------------- /src/private/MDMViewControllerTransitionController.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | #import 19 | 20 | #import "MDMTransitionController.h" 21 | 22 | API_DEPRECATED_BEGIN("Use standard UIViewController transitioning APIs instead.", 23 | ios(12, API_TO_BE_DEPRECATED)) 24 | 25 | @interface MDMViewControllerTransitionController : NSObject 26 | 27 | - (nonnull instancetype)initWithViewController:(nonnull UIViewController *)viewController 28 | NS_DESIGNATED_INITIALIZER; 29 | 30 | - (nonnull instancetype)init NS_UNAVAILABLE; 31 | 32 | @end 33 | 34 | API_DEPRECATED_END 35 | -------------------------------------------------------------------------------- /src/private/MDMViewControllerTransitionController.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMViewControllerTransitionController.h" 18 | 19 | #import "MDMTransition.h" 20 | #import "MDMViewControllerTransitionCoordinator.h" 21 | 22 | @interface MDMViewControllerTransitionController () 23 | @end 24 | 25 | @implementation MDMViewControllerTransitionController { 26 | // We expect the view controller to hold a strong reference to its transition controller, so keep 27 | // a weak reference to the view controller here. 28 | __weak UIViewController *_associatedViewController; 29 | 30 | __weak UIPresentationController *_presentationController; 31 | 32 | MDMViewControllerTransitionCoordinator *_coordinator; 33 | __weak UIViewController *_source; 34 | } 35 | 36 | @synthesize transition = _transition; 37 | 38 | - (nonnull instancetype)initWithViewController:(nonnull UIViewController *)viewController { 39 | self = [super init]; 40 | if (self) { 41 | _associatedViewController = viewController; 42 | } 43 | return self; 44 | } 45 | 46 | #pragma mark - Public 47 | 48 | - (void)setTransition:(id)transition { 49 | _transition = transition; 50 | 51 | // Set the default modal presentation style. 52 | id withPresentation = [self presentationTransition]; 53 | if (withPresentation != nil) { 54 | UIModalPresentationStyle style = [withPresentation defaultModalPresentationStyle]; 55 | _associatedViewController.modalPresentationStyle = style; 56 | } 57 | } 58 | 59 | - (id)activeTransition { 60 | return [self.activeTransitions firstObject]; 61 | } 62 | 63 | - (NSArray> *)activeTransitions { 64 | return [_coordinator activeTransitions]; 65 | } 66 | 67 | - (id)presentationTransition { 68 | if ([self.transition respondsToSelector:@selector(defaultModalPresentationStyle)]) { 69 | return (id)self.transition; 70 | } 71 | return nil; 72 | } 73 | 74 | #pragma mark - UIViewControllerTransitioningDelegate 75 | 76 | // Animated transitions 77 | 78 | - (id)animationControllerForPresentedController:(UIViewController *)presented 79 | presentingController:(UIViewController *)presenting 80 | sourceController:(UIViewController *)source { 81 | _source = source; 82 | 83 | [self prepareForTransitionWithSourceViewController:source 84 | backViewController:presenting 85 | foreViewController:presented 86 | direction:MDMTransitionDirectionForward]; 87 | return _coordinator; 88 | } 89 | 90 | - (id)animationControllerForDismissedController:(UIViewController *)dismissed { 91 | [self prepareForTransitionWithSourceViewController:_source 92 | backViewController:dismissed.presentingViewController 93 | foreViewController:dismissed 94 | direction:MDMTransitionDirectionBackward]; 95 | return _coordinator; 96 | } 97 | 98 | // Presentation 99 | 100 | - (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented 101 | presentingViewController:(UIViewController *)presenting 102 | sourceViewController:(UIViewController *)source { 103 | id withPresentation = [self presentationTransition]; 104 | if (withPresentation == nil) { 105 | return nil; 106 | } 107 | UIPresentationController *presentationController = 108 | [withPresentation presentationControllerForPresentedViewController:presented 109 | presentingViewController:presenting 110 | sourceViewController:source]; 111 | // _presentationController is weakly-held, so we have to do this local var dance to keep it 112 | // from being nil'd on assignment. 113 | _presentationController = presentationController; 114 | return presentationController; 115 | } 116 | 117 | #pragma mark - MDMViewControllerTransitionCoordinatorDelegate 118 | 119 | - (void)transitionDidCompleteWithCoordinator:(MDMViewControllerTransitionCoordinator *)coordinator { 120 | if (_coordinator == coordinator) { 121 | _coordinator = nil; 122 | } 123 | } 124 | 125 | #pragma mark - Private 126 | 127 | - (void)prepareForTransitionWithSourceViewController:(nullable UIViewController *)source 128 | backViewController:(nonnull UIViewController *)back 129 | foreViewController:(nonnull UIViewController *)fore 130 | direction:(MDMTransitionDirection)direction { 131 | if (direction == MDMTransitionDirectionBackward) { 132 | _coordinator = nil; 133 | } 134 | NSAssert(!_coordinator, @"A transition is already active."); 135 | 136 | _coordinator = [[MDMViewControllerTransitionCoordinator alloc] initWithTransition:self.transition 137 | direction:direction 138 | sourceViewController:source 139 | backViewController:back 140 | foreViewController:fore 141 | presentationController:_presentationController]; 142 | _coordinator.delegate = self; 143 | } 144 | 145 | @end 146 | -------------------------------------------------------------------------------- /src/private/MDMViewControllerTransitionCoordinator.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMTransitionContext.h" 18 | 19 | API_DEPRECATED_BEGIN("Use standard UIViewController transitioning APIs instead.", 20 | ios(12, API_TO_BE_DEPRECATED)) 21 | 22 | @protocol MDMTransition; 23 | @protocol MDMViewControllerTransitionCoordinatorDelegate; 24 | 25 | @interface MDMViewControllerTransitionCoordinator : NSObject 26 | 27 | - (nonnull instancetype)initWithTransition:(nonnull NSObject *)transition 28 | direction:(MDMTransitionDirection)direction 29 | sourceViewController:(nullable UIViewController *)sourceViewController 30 | backViewController:(nonnull UIViewController *)backViewController 31 | foreViewController:(nonnull UIViewController *)foreViewController 32 | presentationController:(nullable UIPresentationController *)presentationController; 33 | - (nonnull instancetype)init NS_UNAVAILABLE; 34 | 35 | - (nonnull NSArray *> *)activeTransitions; 36 | 37 | @property(nonatomic, weak, nullable) id delegate; 38 | 39 | @end 40 | 41 | @protocol MDMViewControllerTransitionCoordinatorDelegate 42 | 43 | - (void)transitionDidCompleteWithCoordinator:(nonnull MDMViewControllerTransitionCoordinator *)coordinator; 44 | 45 | @end 46 | 47 | API_DEPRECATED_END 48 | -------------------------------------------------------------------------------- /src/private/MDMViewControllerTransitionCoordinator.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMViewControllerTransitionCoordinator.h" 18 | 19 | #import 20 | 21 | #import "MDMTransition.h" 22 | 23 | @class MDMViewControllerTransitionContextNode; 24 | 25 | @protocol MDMViewControllerTransitionContextNodeParent 26 | - (void)childNodeTransitionDidEnd:(MDMViewControllerTransitionContextNode *)childNode; 27 | @end 28 | 29 | @interface MDMViewControllerTransitionContextNode : NSObject 30 | @property(nonatomic, strong) id transitionContext; 31 | @property(nonatomic, strong, readonly) id transition; 32 | @property(nonatomic, copy, readonly) NSMutableArray *children; 33 | @end 34 | 35 | @implementation MDMViewControllerTransitionContextNode { 36 | // Every node points to the same array in memory. 37 | NSMutableArray *_sharedCompletionBlocks; 38 | 39 | BOOL _hasStarted; 40 | BOOL _didEnd; 41 | __weak id _parent; 42 | } 43 | 44 | @synthesize duration = _duration; 45 | @synthesize direction = _direction; 46 | @synthesize sourceViewController = _sourceViewController; 47 | @synthesize backViewController = _backViewController; 48 | @synthesize foreViewController = _foreViewController; 49 | @synthesize presentationController = _presentationController; 50 | 51 | - (instancetype)initWithTransition:(id)transition 52 | direction:(MDMTransitionDirection)direction 53 | sourceViewController:(UIViewController *)sourceViewController 54 | backViewController:(UIViewController *)backViewController 55 | foreViewController:(UIViewController *)foreViewController 56 | presentationController:(UIPresentationController *)presentationController 57 | sharedCompletionBlocks:(NSMutableArray *)sharedCompletionBlocks 58 | parent:(id)parent { 59 | self = [super init]; 60 | if (self) { 61 | _children = [NSMutableArray array]; 62 | _transition = transition; 63 | _direction = direction; 64 | _sourceViewController = sourceViewController; 65 | _backViewController = backViewController; 66 | _foreViewController = foreViewController; 67 | _presentationController = presentationController; 68 | _sharedCompletionBlocks = sharedCompletionBlocks; 69 | _parent = parent; 70 | } 71 | return self; 72 | } 73 | 74 | #pragma mark - Private 75 | 76 | - (MDMViewControllerTransitionContextNode *)spawnChildWithTransition:(id)transition { 77 | MDMViewControllerTransitionContextNode *node = 78 | [[MDMViewControllerTransitionContextNode alloc] initWithTransition:transition 79 | direction:_direction 80 | sourceViewController:_sourceViewController 81 | backViewController:_backViewController 82 | foreViewController:_foreViewController 83 | presentationController:_presentationController 84 | sharedCompletionBlocks:_sharedCompletionBlocks 85 | parent:self]; 86 | node.transitionContext = _transitionContext; 87 | return node; 88 | } 89 | 90 | - (void)checkAndNotifyOfCompletion { 91 | BOOL anyChildActive = NO; 92 | for (MDMViewControllerTransitionContextNode *child in _children) { 93 | if (!child->_didEnd) { 94 | anyChildActive = YES; 95 | break; 96 | } 97 | } 98 | 99 | if (!anyChildActive && _didEnd) { // Inform our parent of completion. 100 | [_parent childNodeTransitionDidEnd:self]; 101 | } 102 | } 103 | 104 | #pragma mark - Public 105 | 106 | - (void)start { 107 | if (_hasStarted) { 108 | return; 109 | } 110 | 111 | _hasStarted = YES; 112 | 113 | for (MDMViewControllerTransitionContextNode *child in _children) { 114 | [child attemptFallback]; 115 | 116 | [child start]; 117 | } 118 | 119 | if ([_transition respondsToSelector:@selector(startWithContext:)]) { 120 | [_transition startWithContext:self]; 121 | } else { 122 | _didEnd = YES; 123 | 124 | [self checkAndNotifyOfCompletion]; 125 | } 126 | } 127 | 128 | - (NSArray *)activeTransitions { 129 | NSMutableArray *activeTransitions = [NSMutableArray array]; 130 | if (!_didEnd) { 131 | [activeTransitions addObject:self]; 132 | } 133 | for (MDMViewControllerTransitionContextNode *child in _children) { 134 | [activeTransitions addObjectsFromArray:[child activeTransitions]]; 135 | } 136 | return activeTransitions; 137 | } 138 | 139 | - (void)setTransitionContext:(id)transitionContext { 140 | _transitionContext = transitionContext; 141 | 142 | for (MDMViewControllerTransitionContextNode *child in _children) { 143 | child.transitionContext = transitionContext; 144 | } 145 | } 146 | 147 | - (void)setDuration:(NSTimeInterval)duration { 148 | _duration = duration; 149 | 150 | for (MDMViewControllerTransitionContextNode *child in _children) { 151 | child.duration = duration; 152 | } 153 | } 154 | 155 | - (void)attemptFallback { 156 | id transition = _transition; 157 | while ([transition respondsToSelector:@selector(fallbackTransitionWithContext:)]) { 158 | id withFallback = (id)transition; 159 | 160 | id fallback = [withFallback fallbackTransitionWithContext:self]; 161 | if (fallback == transition) { 162 | break; 163 | } 164 | transition = fallback; 165 | } 166 | _transition = transition; 167 | } 168 | 169 | #pragma mark - MDMViewControllerTransitionContextNodeDelegate 170 | 171 | - (void)childNodeTransitionDidEnd:(MDMViewControllerTransitionContextNode *)contextNode { 172 | [self checkAndNotifyOfCompletion]; 173 | } 174 | 175 | #pragma mark - MDMTransitionContext 176 | 177 | - (void)composeWithTransition:(id)transition { 178 | MDMViewControllerTransitionContextNode *child = [self spawnChildWithTransition:transition]; 179 | 180 | [_children addObject:child]; 181 | 182 | if (_hasStarted) { 183 | [child start]; 184 | } 185 | } 186 | 187 | - (UIView *)containerView { 188 | return _transitionContext.containerView; 189 | } 190 | 191 | - (void)deferToCompletion:(void (^)(void))work { 192 | [_sharedCompletionBlocks addObject:[work copy]]; 193 | } 194 | 195 | - (void)transitionDidEnd { 196 | if (_didEnd) { 197 | return; // No use in re-notifying. 198 | } 199 | _didEnd = YES; 200 | 201 | [self checkAndNotifyOfCompletion]; 202 | } 203 | 204 | @end 205 | 206 | @interface MDMViewControllerTransitionCoordinator() 207 | @end 208 | 209 | @implementation MDMViewControllerTransitionCoordinator { 210 | MDMTransitionDirection _direction; 211 | UIPresentationController *_presentationController; 212 | 213 | MDMViewControllerTransitionContextNode *_root; 214 | NSMutableArray *_completionBlocks; 215 | 216 | id _transitionContext; 217 | } 218 | 219 | - (instancetype)initWithTransition:(NSObject *)transition 220 | direction:(MDMTransitionDirection)direction 221 | sourceViewController:(UIViewController *)sourceViewController 222 | backViewController:(UIViewController *)backViewController 223 | foreViewController:(UIViewController *)foreViewController 224 | presentationController:(UIPresentationController *)presentationController { 225 | self = [super init]; 226 | if (self) { 227 | _direction = direction; 228 | _presentationController = presentationController; 229 | 230 | _completionBlocks = [NSMutableArray array]; 231 | 232 | // Build our contexts: 233 | 234 | _root = [[MDMViewControllerTransitionContextNode alloc] initWithTransition:transition 235 | direction:direction 236 | sourceViewController:sourceViewController 237 | backViewController:backViewController 238 | foreViewController:foreViewController 239 | presentationController:presentationController 240 | sharedCompletionBlocks:_completionBlocks 241 | parent:self]; 242 | 243 | if (_presentationController 244 | && [_presentationController respondsToSelector:@selector(startWithContext:)]) { 245 | MDMViewControllerTransitionContextNode *presentationNode = 246 | [[MDMViewControllerTransitionContextNode alloc] initWithTransition:(id)_presentationController 247 | direction:direction 248 | sourceViewController:sourceViewController 249 | backViewController:backViewController 250 | foreViewController:foreViewController 251 | presentationController:presentationController 252 | sharedCompletionBlocks:_completionBlocks 253 | parent:_root]; 254 | [_root.children addObject:presentationNode]; 255 | } 256 | 257 | if ([transition respondsToSelector:@selector(canPerformTransitionWithContext:)]) { 258 | id withFeasibility = (id)transition; 259 | if (![withFeasibility canPerformTransitionWithContext:_root]) { 260 | self = nil; 261 | return nil; // No active transitions means no need for a coordinator. 262 | } 263 | } 264 | } 265 | return self; 266 | } 267 | 268 | #pragma mark - MDMViewControllerTransitionContextNodeDelegate 269 | 270 | - (void)childNodeTransitionDidEnd:(MDMViewControllerTransitionContextNode *)node { 271 | if (_root != nil && _root == node) { 272 | _root = nil; 273 | 274 | for (void (^work)(void) in _completionBlocks) { 275 | work(); 276 | } 277 | [_completionBlocks removeAllObjects]; 278 | 279 | [_transitionContext completeTransition:true]; 280 | _transitionContext = nil; 281 | 282 | [_delegate transitionDidCompleteWithCoordinator:self]; 283 | } 284 | } 285 | 286 | #pragma mark - UIViewControllerAnimatedTransitioning 287 | 288 | - (NSTimeInterval)transitionDuration:(id)transitionContext { 289 | NSTimeInterval duration = 0.35; 290 | if ([_root.transition respondsToSelector:@selector(transitionDurationWithContext:)]) { 291 | id withCustomDuration = (id)_root.transition; 292 | duration = [withCustomDuration transitionDurationWithContext:_root]; 293 | } 294 | _root.duration = duration; 295 | return duration; 296 | } 297 | 298 | - (void)animateTransition:(id)transitionContext { 299 | _transitionContext = transitionContext; 300 | 301 | [self initiateTransition]; 302 | } 303 | 304 | // TODO(featherless): Implement interactive transitioning. Need to implement 305 | // UIViewControllerInteractiveTransitioning here and isInteractive and interactionController* in 306 | // MDMViewControllerTransitionController. 307 | 308 | - (NSArray *> *)activeTransitions { 309 | return [_root activeTransitions]; 310 | } 311 | 312 | #pragma mark - Private 313 | 314 | - (void)initiateTransition { 315 | _root.transitionContext = _transitionContext; 316 | 317 | UIViewController *from = [_transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; 318 | UIView *fromView = [_transitionContext viewForKey:UITransitionContextFromViewKey]; 319 | if (fromView == nil) { 320 | fromView = from.view; 321 | } 322 | if (fromView != nil && fromView == from.view) { 323 | CGRect finalFrame = [_transitionContext finalFrameForViewController:from]; 324 | if (!CGRectIsEmpty(finalFrame)) { 325 | fromView.frame = finalFrame; 326 | } 327 | } 328 | 329 | UIViewController *to = [_transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; 330 | UIView *toView = [_transitionContext viewForKey:UITransitionContextToViewKey]; 331 | if (toView == nil) { 332 | toView = to.view; 333 | } 334 | if (toView != nil && toView == to.view) { 335 | CGRect finalFrame = [_transitionContext finalFrameForViewController:to]; 336 | if (!CGRectIsEmpty(finalFrame)) { 337 | toView.frame = finalFrame; 338 | } 339 | } 340 | 341 | if (toView.superview == nil) { 342 | switch (_direction) { 343 | case MDMTransitionDirectionForward: 344 | [_transitionContext.containerView addSubview:toView]; 345 | break; 346 | 347 | case MDMTransitionDirectionBackward: 348 | [_transitionContext.containerView insertSubview:toView atIndex:0]; 349 | break; 350 | } 351 | } 352 | 353 | [toView layoutIfNeeded]; 354 | 355 | [_root attemptFallback]; 356 | [self anticipateOnlyExplicitAnimations]; 357 | 358 | [CATransaction begin]; 359 | [CATransaction setAnimationDuration:[self transitionDuration:_transitionContext]]; 360 | 361 | [_root start]; 362 | 363 | [CATransaction commit]; 364 | } 365 | 366 | // UIKit transitions will not animate any of the system animations (status bar changes, notably) 367 | // unless we have at least one implicit UIView animation. Material Motion doesn't use implicit 368 | // animations out of the box, so to ensure that system animations still occur we create an 369 | // invisible throwaway view and apply an animation to it. 370 | - (void)anticipateOnlyExplicitAnimations { 371 | UIView *throwawayView = [[UIView alloc] init]; 372 | [_transitionContext.containerView addSubview:throwawayView]; 373 | 374 | [UIView animateWithDuration:[self transitionDuration:_transitionContext] 375 | animations:^{ 376 | throwawayView.frame = CGRectOffset(throwawayView.frame, 1, 0); 377 | 378 | } 379 | completion:^(BOOL finished) { 380 | [throwawayView removeFromSuperview]; 381 | }]; 382 | } 383 | 384 | @end 385 | -------------------------------------------------------------------------------- /tests/unit/TransitionTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import XCTest 18 | import MotionTransitioning 19 | 20 | class TransitionTests: XCTestCase { 21 | 22 | private var window: UIWindow! 23 | override func setUp() { 24 | window = UIWindow() 25 | window.rootViewController = UIViewController() 26 | window.makeKeyAndVisible() 27 | } 28 | 29 | override func tearDown() { 30 | window = nil 31 | } 32 | 33 | func testTransitionDidEndDoesComplete() { 34 | let presentedViewController = UIViewController() 35 | presentedViewController.mdm_transitionController.transition = InstantCompletionTransition() 36 | 37 | let didComplete = expectation(description: "Did complete") 38 | window.rootViewController!.present(presentedViewController, animated: true) { 39 | didComplete.fulfill() 40 | } 41 | 42 | waitForExpectations(timeout: 0.1) 43 | 44 | XCTAssertEqual(window.rootViewController!.presentedViewController, presentedViewController) 45 | } 46 | 47 | func testTransitionCompositionDoesComplete() { 48 | let presentedViewController = UIViewController() 49 | presentedViewController.mdm_transitionController.transition = CompositeTransition(transitions: [ 50 | InstantCompletionTransition(), 51 | InstantCompletionTransition() 52 | ]) 53 | 54 | let didComplete = expectation(description: "Did complete") 55 | window.rootViewController!.present(presentedViewController, animated: true) { 56 | didComplete.fulfill() 57 | } 58 | 59 | waitForExpectations(timeout: 0.1) 60 | 61 | XCTAssertEqual(window.rootViewController!.presentedViewController, presentedViewController) 62 | } 63 | 64 | func testTransitionFallbackToOtherTransitionDoesComplete() { 65 | let presentedViewController = UIViewController() 66 | let transition = FallbackTransition(to: InstantCompletionTransition()) 67 | presentedViewController.mdm_transitionController.transition = transition 68 | 69 | let didComplete = expectation(description: "Did complete") 70 | window.rootViewController!.present(presentedViewController, animated: true) { 71 | didComplete.fulfill() 72 | } 73 | 74 | waitForExpectations(timeout: 0.1) 75 | 76 | XCTAssertFalse(transition.startWasInvoked) 77 | XCTAssertEqual(window.rootViewController!.presentedViewController, presentedViewController) 78 | } 79 | 80 | func testTransitionFallbackToSelfDoesComplete() { 81 | let presentedViewController = UIViewController() 82 | let transition = FallbackTransition() 83 | presentedViewController.mdm_transitionController.transition = transition 84 | 85 | let didComplete = expectation(description: "Did complete") 86 | window.rootViewController!.present(presentedViewController, animated: true) { 87 | didComplete.fulfill() 88 | } 89 | 90 | waitForExpectations(timeout: 0.1) 91 | 92 | XCTAssertTrue(transition.startWasInvoked) 93 | XCTAssertEqual(window.rootViewController!.presentedViewController, presentedViewController) 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /tests/unit/TransitionWithCustomDurationTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import XCTest 18 | import MotionTransitioning 19 | 20 | final class DurationMemoryTransition: NSObject, Transition { 21 | var recordedDuration: TimeInterval? 22 | func start(with context: TransitionContext) { 23 | recordedDuration = context.duration 24 | 25 | context.transitionDidEnd() 26 | } 27 | } 28 | 29 | final class CustomDurationMemoryTransition: NSObject, TransitionWithCustomDuration { 30 | let duration: TimeInterval 31 | init(with duration: TimeInterval) { 32 | self.duration = duration 33 | } 34 | 35 | func transitionDuration(with context: TransitionContext) -> TimeInterval { 36 | return duration 37 | } 38 | 39 | var recordedDuration: TimeInterval? 40 | func start(with context: TransitionContext) { 41 | recordedDuration = context.duration 42 | 43 | context.transitionDidEnd() 44 | } 45 | } 46 | 47 | class TransitionWithCustomDurationTests: XCTestCase { 48 | 49 | private var window: UIWindow! 50 | override func setUp() { 51 | window = UIWindow() 52 | window.rootViewController = UIViewController() 53 | window.makeKeyAndVisible() 54 | } 55 | 56 | override func tearDown() { 57 | window = nil 58 | } 59 | 60 | func testDefaultDurationIsProvidedViaContext() { 61 | let presentedViewController = UIViewController() 62 | let transition = DurationMemoryTransition() 63 | presentedViewController.mdm_transitionController.transition = transition 64 | 65 | let didComplete = expectation(description: "Did complete") 66 | window.rootViewController!.present(presentedViewController, animated: true) { 67 | didComplete.fulfill() 68 | } 69 | 70 | waitForExpectations(timeout: 0.1) 71 | 72 | // TODO: This should be an extern const in the library. 73 | XCTAssertEqual(transition.recordedDuration, 0.35) 74 | 75 | XCTAssertEqual(window.rootViewController!.presentedViewController, presentedViewController) 76 | } 77 | 78 | func testCustomDurationIsProvidedViaContext() { 79 | let presentedViewController = UIViewController() 80 | let customDuration: TimeInterval = 0.1 81 | let transition = CustomDurationMemoryTransition(with: customDuration) 82 | presentedViewController.mdm_transitionController.transition = transition 83 | 84 | let didComplete = expectation(description: "Did complete") 85 | window.rootViewController!.present(presentedViewController, animated: true) { 86 | didComplete.fulfill() 87 | } 88 | 89 | waitForExpectations(timeout: 0.1) 90 | 91 | XCTAssertEqual(transition.recordedDuration, customDuration) 92 | 93 | XCTAssertEqual(window.rootViewController!.presentedViewController, presentedViewController) 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /tests/unit/TransitionWithPresentationTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import XCTest 18 | import MotionTransitioning 19 | 20 | class TransitionWithPresentationTests: XCTestCase { 21 | 22 | private var window: UIWindow! 23 | override func setUp() { 24 | window = UIWindow() 25 | window.rootViewController = UIViewController() 26 | window.makeKeyAndVisible() 27 | } 28 | 29 | override func tearDown() { 30 | window = nil 31 | } 32 | 33 | func testPresentationControllerIsQueriedAndCompletesWithoutAnimation() { 34 | let presentedViewController = UIViewController() 35 | presentedViewController.mdm_transitionController.transition = 36 | PresentationTransition(presentationControllerType: TestingPresentationController.self) 37 | 38 | let didComplete = expectation(description: "Did complete") 39 | window.rootViewController!.present(presentedViewController, animated: true) { 40 | didComplete.fulfill() 41 | } 42 | 43 | waitForExpectations(timeout: 0.5) 44 | 45 | XCTAssert(presentedViewController.presentationController is TestingPresentationController) 46 | } 47 | 48 | func testPresentationControllerIsQueriedAndCompletesWithAnimation() { 49 | let presentedViewController = UIViewController() 50 | presentedViewController.mdm_transitionController.transition = 51 | PresentationTransition(presentationControllerType: TransitionPresentationController.self) 52 | 53 | let didComplete = expectation(description: "Did complete") 54 | window.rootViewController!.present(presentedViewController, animated: true) { 55 | didComplete.fulfill() 56 | } 57 | 58 | waitForExpectations(timeout: 0.5) 59 | 60 | XCTAssert(presentedViewController.presentationController is TransitionPresentationController) 61 | } 62 | } 63 | 64 | final class TestingPresentationController: UIPresentationController { 65 | } 66 | 67 | final class PresentationTransition: NSObject, TransitionWithPresentation { 68 | let presentationControllerType: UIPresentationController.Type 69 | init(presentationControllerType: UIPresentationController.Type) { 70 | self.presentationControllerType = presentationControllerType 71 | 72 | super.init() 73 | } 74 | 75 | func defaultModalPresentationStyle() -> UIModalPresentationStyle { 76 | return .custom 77 | } 78 | 79 | func presentationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController?) -> UIPresentationController? { 80 | return presentationControllerType.init(presentedViewController: presented, presenting: presenting) 81 | } 82 | 83 | func start(with context: TransitionContext) { 84 | context.transitionDidEnd() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/unit/Transitions/CompositeTransition.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import MotionTransitioning 19 | 20 | final class CompositeTransition: NSObject, Transition, TransitionWithCustomDuration { 21 | 22 | let transitions: [Transition] 23 | init(transitions: [Transition]) { 24 | self.transitions = transitions 25 | 26 | super.init() 27 | } 28 | 29 | // The sole method we're expected to implement, start is invoked each time the view controller is 30 | // presented or dismissed. 31 | func start(with context: TransitionContext) { 32 | transitions.forEach { context.compose(with: $0) } 33 | 34 | context.transitionDidEnd() 35 | } 36 | 37 | // MARK: TransitionWithCustomDuration 38 | 39 | func transitionDuration(with context: TransitionContext) -> TimeInterval { 40 | let duration = transitions.flatMap { $0 as? TransitionWithCustomDuration }.map { $0.transitionDuration(with: context) }.max { $0 < $1 } 41 | if let duration = duration { 42 | return duration 43 | } 44 | return 0.35 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /tests/unit/Transitions/FallbackTransition.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import MotionTransitioning 18 | 19 | final class FallbackTransition: NSObject, Transition, TransitionWithFallback { 20 | 21 | let fallbackTo: Transition? 22 | 23 | init(to: Transition) { 24 | self.fallbackTo = to 25 | } 26 | 27 | override init() { 28 | self.fallbackTo = nil 29 | } 30 | 31 | func fallbackTransition(with context: TransitionContext) -> Transition { 32 | if let fallbackTo = fallbackTo { 33 | return fallbackTo 34 | } 35 | return self 36 | } 37 | 38 | var startWasInvoked = false 39 | func start(with context: TransitionContext) { 40 | startWasInvoked = true 41 | context.transitionDidEnd() 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /tests/unit/Transitions/InstantCompletionTransition.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import MotionTransitioning 18 | 19 | final class InstantCompletionTransition: NSObject, Transition { 20 | func start(with context: TransitionContext) { 21 | context.transitionDidEnd() 22 | } 23 | } 24 | --------------------------------------------------------------------------------