├── .gitignore ├── .swift-version ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── Motion.podspec ├── Motion.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── Motion.xcscmblueprint └── xcshareddata │ └── xcschemes │ └── Motion.xcscheme ├── Package.swift ├── README.md └── Sources ├── Animator ├── MotionAnimator.swift ├── MotionAnimatorViewContext.swift ├── MotionCoreAnimationViewContext.swift ├── MotionCoreAnimator.swift ├── MotionViewPropertyViewContext.swift └── MotionViewTransitionAnimator.swift ├── Application.swift ├── Extensions ├── Motion+Array.swift ├── Motion+CALayer.swift ├── Motion+CAMediaTimingFunction.swift ├── Motion+CG.swift ├── Motion+Obj-C.swift ├── Motion+UIKit.swift ├── Motion+UIView.swift └── Motion+UIViewController.swift ├── Info.plist ├── LICENSE ├── Motion.h ├── MotionAnimation.swift ├── MotionAnimationState.swift ├── MotionCAAnimation.swift ├── MotionContext.swift ├── MotionCoordinateSpace.swift ├── MotionModifier.swift ├── MotionPlugin.swift ├── MotionSnapshotType.swift ├── MotionTargetState.swift ├── MotionTransitionObserver.swift ├── MotionViewOrderStrategy.swift ├── MotionViewTransition.swift ├── Preprocessors ├── CascadePreprocessor.swift ├── ConditionalPreprocessor.swift ├── IgnoreSubviewModifiersPreprocessor.swift ├── MatchPreprocessor.swift ├── MotionCorePreprocessor.swift ├── MotionPreprocessor.swift ├── SourcePreprocessor.swift └── TransitionPreprocessor.swift └── Transition ├── MotionProgressRunner.swift ├── MotionTransition+Animate.swift ├── MotionTransition+Complete.swift ├── MotionTransition+CustomTransition.swift ├── MotionTransition+Interactive.swift ├── MotionTransition+Start.swift ├── MotionTransition+UINavigationControllerDelegate.swift ├── MotionTransition+UITabBarControllerDelegate.swift ├── MotionTransition+UIViewControllerTransitioningDelegate.swift └── MotionTransition.swift /.gitignore: -------------------------------------------------------------------------------- 1 | #OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/* 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | profile 16 | *.moved-aside 17 | *.playground 18 | *.framework 19 | DerivedData 20 | Index 21 | Build 22 | 23 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.1.3 2 | 3 | - [pr-76](https://github.com/CosmicMind/Motion/pull/76): Added missing module for Swift Package Manager. 4 | * Updated Xcode 11 settings. 5 | 6 | ## 3.1.2 7 | 8 | * Updated layout version in Xcode project. 9 | 10 | ## 3.1.1 11 | 12 | * Added installation instructions to README. 13 | 14 | ## 3.1.0 15 | 16 | - Updated to swift 4.2. 17 | - [pr-45](https://github.com/CosmicMind/Motion/pull/45): Fixed issue-44, view is hidden below status bar during call. 18 | - [issue-44](https://github.com/CosmicMind/Motion/issues/44): View is hidden below status bar when a call is active. 19 | - [pr-50](https://github.com/CosmicMind/Motion/pull/50): Allow framework to be linked from extensions. 20 | - [pr-53](https://github.com/CosmicMind/Motion/pull/53): Bug fixes and `MotionViewTransition`. 21 | - Made `MotionTransition.{start|update|finish|cancel}` methods public. 22 | - Changed to updating model before firing animations for `CALayer` animations. 23 | - Added reverting `XXForNextTransition` options back to default. 24 | - Fixed issue when interactive transition was cancelled it was calling finish instead of cancel. 25 | - Fixed appearance transition callbacks for transition cancellation. 26 | - Fixed resume time was calculated incorrectly. 27 | - Fixed ~0.001 seconds precision issue by storing current time. 28 | - Added `MotionViewTransition` to make transitions of views possible. 29 | - [pr-54](https://github.com/CosmicMind/Motion/pull/54): Added setCompletionCallbackForNextTransition. 30 | - [pr-55](https://github.com/CosmicMind/Motion/pull/55): Fixed UITabBarController had userinteraction enabled during transition. 31 | 32 | ## 1.5.0 33 | 34 | * Updated for Swift 4.2. 35 | 36 | ## 1.4.3 37 | 38 | * [pr-42](https://github.com/CosmicMind/Motion/pull/42): Fixed unbalanced calls to begin/end appearance transitions. 39 | * [issue-29](https://github.com/CosmicMind/Motion/issues/29): Unbalanced calls to begin/end appearance. 40 | 41 | ## 1.4.2 42 | 43 | * [pr-40](https://github.com/CosmicMind/Motion/pull/40): Fixed delegation issue, where UINavigationController and UITabBarController were not correctly calling their delegate methods. 44 | 45 | ## 1.4.1 46 | 47 | * Minor cleanup. 48 | 49 | ## 1.4.0 50 | 51 | * Updated for Xcode 9.3. 52 | 53 | ## 1.3.5 54 | 55 | * [issue-26](https://github.com/CosmicMind/Motion/issues/26): Fixed typo. 56 | 57 | ## 1.3.4 58 | 59 | * [issue-1022](https://github.com/CosmicMind/Material/issues/1022): Fixed alpha issue, where alpha was being set to 1 and not respecting the initial set alpha value. 60 | 61 | ## 1.3.3 62 | 63 | * [issue-24](https://github.com/CosmicMind/Motion/issues/24): Fixed regression where view lifecycle functions were not being called. 64 | 65 | ## 1.3.2 66 | 67 | * Fixed unbalanced calls in Motion transitions. 68 | 69 | ## 1.3.1 70 | 71 | * Updated isMotionEnabled check, as it was determined incorrectly. 72 | 73 | ## 1.3.0 74 | 75 | * Reworked Motion internals. 76 | 77 | ## 1.2.5 78 | 79 | * `UIViewController.motionModalTransitionType` renamed to `UIViewController.motionTransitionType`. 80 | * Updated logic steps for `TabsController.motionTransitionType` and child view controller `motionTransitionType` values. 81 | 82 | ## 1.2.4 83 | 84 | * Added begin / end transition methods for from / to view controllers. 85 | 86 | ## 1.2.3 87 | 88 | * Replaced DispatchQueue.main.async calls to Motion.async. 89 | 90 | ## 1.2.2 91 | 92 | * Updated Motion for iOS 11, where snapshot would no longer include a container view. 93 | 94 | ## 1.2.1 95 | 96 | * Submodule access rights update for [Material](https://github.com/CosmicMind/Material). 97 | 98 | ## 1.2.0 99 | 100 | * Updated to `Swift 4`. 101 | * Fixed a couple memory leaks. 102 | 103 | ## 1.1.2 104 | 105 | * Minor internal updates. 106 | 107 | ## 1.1.1 108 | 109 | * Added Motion logo to README. 110 | 111 | ## 1.1.0 112 | 113 | * [issue-5](https://github.com/CosmicMind/Motion/issues/5): Added the ability to add custom timing functions. 114 | * [issue-4](https://github.com/CosmicMind/Motion/issues/4): Fixed an issue where a white flash occurred when pushing/popping a view controller. 115 | * [issue-8](https://github.com/CosmicMind/Motion/issues/8): Added the ability to add animation immediately. 116 | * [issue-6](https://github.com/CosmicMind/Motion/issues/6): Added the ability to animate views that are not paired. 117 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | This document contains information and guidelines about contributing to this project. 4 | Please read it before you start participating. 5 | 6 | **Topics** 7 | 8 | * [Pull Request Submissions](#pull-request-submissions) 9 | * [Asking Questions](#asking-questions) 10 | * [Reporting Security Issues](#reporting-security-issues) 11 | * [Reporting Issues](#reporting-other-issues) 12 | * [Developers Certificate of Origin](#developers-certificate-of-origin) 13 | * [Code of Conduct](#code-of-conduct) 14 | 15 | 16 | ## Pull Request Submissions. 17 | 18 | All pull requests should be made to the development branch. Details should be available describing your fix. Before submitting a pull request, please confirm that merge issues are resolved. 19 | 20 | 21 | ## Asking Questions 22 | 23 | We don't use GitHub as a support forum. 24 | For any usage questions that are not specific to the project itself, 25 | please ask on [Stack Overflow](http://stackoverflow.com/questions/tagged/cosmicmind) instead. 26 | By doing so, you'll be more likely to quickly solve your problem, 27 | and you'll allow anyone else with the same question to find the answer. 28 | This also allows maintainers to focus on improving the project for others. 29 | 30 | 31 | ## Reporting Security Issues 32 | 33 | CosmicMind takes security seriously. 34 | If you discover a security issue, please bring it to our attention right away! 35 | 36 | Please **DO NOT** file a public issue, 37 | instead send your report privately to . 38 | This will help ensure that any vulnerabilities that _are_ found 39 | can be [disclosed responsibly](http://en.wikipedia.org/wiki/Responsible_disclosure) 40 | to any affected parties. 41 | 42 | 43 | ## Reporting Other Issues 44 | 45 | A great way to contribute to the project 46 | is to send a detailed issue when you encounter an problem. 47 | We always appreciate a well-written, thorough bug report. 48 | 49 | Check that the project issues database 50 | doesn't already include that problem or suggestion before submitting an issue. 51 | If you find a match, add a quick "+1" or "I have this problem too." 52 | Doing this helps prioritize the most common problems and requests. 53 | 54 | When reporting issues, please include the following: 55 | 56 | * The version of Xcode you're using 57 | * The version of iOS you're targeting 58 | * The full output of any stack trace or compiler error 59 | * A code snippet that reproduces the described behavior, if applicable 60 | * Any other details that would be useful in understanding the problem 61 | 62 | This information will help us review and fix your issue faster. 63 | 64 | 65 | ## Developer's Certificate of Origin 1.1 66 | 67 | By making a contribution to this project, I certify that: 68 | 69 | - (a) The contribution was created in whole or in part by me and I 70 | have the right to submit it under the open source license 71 | indicated in the file; or 72 | 73 | - (b) The contribution is based upon previous work that, to the best 74 | of my knowledge, is covered under an appropriate open source 75 | license and I have the right under that license to submit that 76 | work with modifications, whether created in whole or in part 77 | by me, under the same open source license (unless I am 78 | permitted to submit under a different license), as indicated 79 | in the file; or 80 | 81 | - (c) The contribution was provided directly to me by some other 82 | person who certified (a), (b) or (c) and I have not modified 83 | it. 84 | 85 | - (d) I understand and agree that this project and the contribution 86 | are public and that a record of the contribution (including all 87 | personal information I submit with it, including my sign-off) is 88 | maintained indefinitely and may be redistributed consistent with 89 | this project or the open source license(s) involved. 90 | 91 | --- 92 | 93 | *Some of the ideas and wording for the statements above were based on work by the [Alamofire](https://github.com/Alamofire/Alamofire/blob/master/CONTRIBUTING.md), [Docker](https://github.com/docker/docker/blob/master/CONTRIBUTING.md) and [Linux](http://elinux.org/Developer_Certificate_Of_Origin) communities. We commend them for their efforts to facilitate collaboration in their projects.* 94 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2019, CosmicMind, Inc. . 4 | All rights reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Motion.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'Motion' 3 | s.version = '3.1.3' 4 | s.swift_version = '5.0' 5 | s.license = 'MIT' 6 | s.summary = 'A library used to create beautiful animations and transitions for iOS.' 7 | s.homepage = 'http://cosmicmind.com' 8 | s.social_media_url = 'https://www.facebook.com/cosmicmindcom' 9 | s.authors = { 'CosmicMind, Inc.' => 'support@cosmicmind.com' } 10 | s.source = { :git => 'https://github.com/CosmicMind/Motion.git', :tag => s.version } 11 | s.platform = :ios, '8.0' 12 | 13 | s.default_subspec = 'Core' 14 | 15 | s.subspec 'Core' do |s| 16 | s.ios.deployment_target = '8.0' 17 | s.ios.source_files = 'Sources/**/*.swift' 18 | s.requires_arc = true 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /Motion.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Motion.xcodeproj/project.xcworkspace/xcshareddata/Motion.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "0F3E254D46E5A5B90D1542854F510B7D145A9F31", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "0F3E254D46E5A5B90D1542854F510B7D145A9F31" : 9223372036854775807, 8 | "9CF35BC641478FBD765F7442D4034ED362261E0A" : 9223372036854775807 9 | }, 10 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "6AC70DB0-50D8-45F1-A69E-812AE5A97A0A", 11 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 12 | "0F3E254D46E5A5B90D1542854F510B7D145A9F31" : "Motion\/", 13 | "9CF35BC641478FBD765F7442D4034ED362261E0A" : ".." 14 | }, 15 | "DVTSourceControlWorkspaceBlueprintNameKey" : "Motion", 16 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 17 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "Motion.xcodeproj", 18 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 19 | { 20 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/CosmicMind\/Motion.git", 21 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "0F3E254D46E5A5B90D1542854F510B7D145A9F31" 23 | }, 24 | { 25 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/CosmicMind\/Material.git", 26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 27 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "9CF35BC641478FBD765F7442D4034ED362261E0A" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /Motion.xcodeproj/xcshareddata/xcschemes/Motion.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Motion", 6 | // platforms: [.iOS("8.0")], 7 | products: [ 8 | .library(name: "Motion", targets: ["Motion"]) 9 | ], 10 | targets: [ 11 | .target( 12 | name: "Motion", 13 | path: "Sources" 14 | ) 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Motion Logo](http://www.cosmicmind.com/motion/logo/motion_logo.png) 2 | 3 | # Motion 4 | 5 | Welcome to **Motion,** a library used to create beautiful animations and transitions for views, layers, and view controllers. 6 | 7 | ## Photos Sample 8 | 9 | Take a look at a sample [Photos](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/Photos) project to get started. 10 | 11 | ![Photos](http://www.cosmicmind.com/motion/projects/photos.gif) 12 | 13 | * [Photos Sample](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/Photos) 14 | 15 | ### Who is Motion for? 16 | 17 | Motion is designed for beginner to expert developers. For beginners, you will be exposed to very powerful APIs that would take time and experience to develop on your own, and experts will appreciate the time saved by using Motion. 18 | 19 | ### What you will learn 20 | 21 | You will learn how to use Motion with a general introduction to fundamental concepts and easy to use code snippets. 22 | 23 | # Transitions 24 | 25 | Motion transitions a source view to a destination view using a linking identifier property named `motionIdentifier`. 26 | 27 | | Match | Translate | Rotate | Arc | Scale | 28 | |:---:|:---:|:---:|:---:|:---:| 29 | | ![Match](http://www.cosmicmind.com/motion/transitions_identifier/match.gif) | ![Translate](http://www.cosmicmind.com/motion/transitions_identifier/translate.gif) | ![Rotate](http://www.cosmicmind.com/motion/transitions_identifier/rotate.gif) | ![Arc](http://www.cosmicmind.com/motion/transitions_identifier/arc.gif) | ![Scale](http://www.cosmicmind.com/motion/transitions_identifier/scale.gif) | 30 | 31 | ### Example Usage 32 | 33 | An example of transitioning from one view controller to another with transitions: 34 | 35 | #### View Controller 1 36 | 37 | ```swift 38 | greyView.motionIdentifier = "foo" 39 | orangeView.motionIdentifier = "bar" 40 | ``` 41 | 42 | #### View Controller 2 43 | 44 | ```swift 45 | isMotionEnabled = true 46 | greyView.motionIdentifier = "foo" 47 | orangeView.motionIdentifier = "bar" 48 | orangeView.transition(.translate(x: -100)) 49 | ``` 50 | 51 | The above code snippet tells the source views in `view controller 1` to link to the destination views in `view controller 2` using the `motionIdentifier`. Animations may be added to views during a transition using the **transition** method. The *transition* method accepts MotionTransition structs that configure the view's animation. 52 | 53 | * [MotionTransition API](https://cosmicmind.gitbooks.io/motion/content/motion_transition_api.html) 54 | * [Code Samples](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/TransitionsWithIdentifier) 55 | 56 | ## UINavigationController, UITabBarController, and UIViewController Transitions 57 | 58 | Motion offers default transitions that may be used by UINavigationControllers, UITabBarControllers, and presenting UIViewControllers. 59 | 60 | | Push | Slide | ZoomSlide | Cover | Page | Fade | Zoom | 61 | |:---:|:---:|:---:|:---:|:---:|:---:|:---:| 62 | | ![Push](http://www.cosmicmind.com/motion/transitions/push.gif) | ![Slide](http://www.cosmicmind.com/motion/transitions/slide.gif)| ![Zoom Slide](http://www.cosmicmind.com/motion/transitions/zoom_slide.gif) | ![Cover](http://www.cosmicmind.com/motion/transitions/cover.gif) | ![Page](http://www.cosmicmind.com/motion/transitions/page_in.gif) | ![Fade](http://www.cosmicmind.com/motion/transitions/fade.gif) | ![Zoom](http://www.cosmicmind.com/motion/transitions/zoom.gif)| 63 | 64 | ### Example Usage 65 | 66 | An example of transitioning from one view controller to another using a UINavigationController with a zoom transition: 67 | 68 | #### UINavigationController 69 | 70 | ```swift 71 | class AppNavigationController: UINavigationController { 72 | open override func viewDidLoad() { 73 | super.viewDidLoad() 74 | isMotionEnabled = true 75 | motionNavigationTransitionType = .zoom 76 | } 77 | } 78 | ``` 79 | 80 | To add an automatic reverse transition, use `autoReverse`. 81 | 82 | ```swift 83 | motionNavigationTransitionType = .autoReverse(presenting: .zoom) 84 | ``` 85 | 86 | * [Code Samples](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/Transitions) 87 | 88 | # Animations 89 | 90 | Motion provides the building blocks necessary to create stunning animations without much effort. Motion's animation API will make maintenance a breeze and changes even easier. To create an animation, use the **animate** method of a view or layer and pass in a list of MotionAnimation structs. MotionAnimation structs are configurable values that describe how to animate a property or group of properties. 91 | 92 | | Background Color | Corner Radius | Fade | Rotate | Size | Spring | 93 | |:---:|:---:|:---:|:---:|:---:|:---:| 94 | | ![Background Color](http://www.cosmicmind.com/motion/animations/background_color.gif) | ![Corner Radius](http://www.cosmicmind.com/motion/animations/corner_radius.gif) | ![Fade](http://www.cosmicmind.com/motion/animations/fade.gif) | ![Rotate](http://www.cosmicmind.com/motion/animations/rotate.gif) | ![Size](http://www.cosmicmind.com/motion/animations/size.gif) | ![Spring](http://www.cosmicmind.com/motion/animations/spring.gif) | 95 | 96 | | Border Color & Border Width | Depth | Position | Scale | Spin | Translate | 97 | |:---:|:---:|:---:|:---:|:---:|:---:| 98 | |![Border Color & Border Width](http://www.cosmicmind.com/motion/animations/border_color.gif) | ![Depth](http://www.cosmicmind.com/motion/animations/depth.gif) | ![Position](http://www.cosmicmind.com/motion/animations/position.gif) | ![Scale](http://www.cosmicmind.com/motion/animations/scale.gif) | ![Spin](http://www.cosmicmind.com/motion/animations/spin.gif) | ![Translate](http://www.cosmicmind.com/motion/animations/translate.gif) | 99 | 100 | ### Example Usage 101 | 102 | ```swift 103 | let box = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 104 | box.backgroundColor = .blue 105 | 106 | box.animate(.background(color: .red), .rotate(180), .delay(1)) 107 | ``` 108 | 109 | In the above code example, a box view is created with a width of 100, height of 100, and an initial background color of blue. Following the general creation of the view, the _Motion animate_ method is passed _MotionAnimation structs_ that tell the view to animate to a red background color and rotate 180 degrees after a delay of 1 second. That's pretty much the general idea of creating animations. 110 | 111 | * [MotionAnimation API](https://cosmicmind.gitbooks.io/motion/content/motion_animation_api.html) 112 | * [Code Samples](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/Animations) 113 | 114 | ## Requirements 115 | 116 | * iOS 8.0+ 117 | * Xcode 8.0+ 118 | 119 | ## Communication 120 | 121 | - If you **need help**, use [Stack Overflow](http://stackoverflow.com/questions/tagged/cosmicmind). (Tag 'cosmicmind') 122 | - If you'd like to **ask a general question**, use [Stack Overflow](http://stackoverflow.com/questions/tagged/cosmicmind). 123 | - If you **found a bug**, _and can provide steps to reliably reproduce it_, open an issue. 124 | - If you **have a feature request**, open an issue. 125 | - If you **want to contribute**, submit a pull request. 126 | 127 | ## Installation 128 | 129 | > **Embedded frameworks require a minimum deployment target of iOS 8.** 130 | > - [Download Motion](https://github.com/CosmicMind/Motion/archive/master.zip) 131 | 132 | ## CocoaPods 133 | 134 | [CocoaPods](http://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command: 135 | 136 | ```bash 137 | $ gem install cocoapods 138 | ``` 139 | 140 | To integrate Motion's core features into your Xcode project using CocoaPods, specify it in your `Podfile`: 141 | 142 | ```ruby 143 | source 'https://github.com/CocoaPods/Specs.git' 144 | platform :ios, '8.0' 145 | use_frameworks! 146 | 147 | pod 'Motion', '~> 3.1.0' 148 | ``` 149 | 150 | Then, run the following command: 151 | 152 | ```bash 153 | $ pod install 154 | ``` 155 | 156 | ## Carthage 157 | 158 | Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. 159 | 160 | You can install Carthage with Homebrew using the following command: 161 | 162 | ```bash 163 | $ brew update 164 | $ brew install carthage 165 | ``` 166 | To integrate Motion into your Xcode project using Carthage, specify it in your Cartfile: 167 | 168 | ```bash 169 | github "CosmicMind/Motion" 170 | ``` 171 | 172 | Run `carthage update` to build the framework and drag the built `Motion.framework` into your Xcode project. 173 | 174 | ## Change Log 175 | 176 | Motion is a growing project and will encounter changes throughout its development. It is recommended that the [Change Log](https://github.com/CosmicMind/Motion/blob/master/CHANGELOG.md) be reviewed prior to updating versions. 177 | 178 | ## License 179 | 180 | The MIT License (MIT) 181 | 182 | Copyright (C) 2019, CosmicMind, Inc. . 183 | All rights reserved. 184 | 185 | Permission is hereby granted, free of charge, to any person obtaining a copy 186 | of this software and associated documentation files (the "Software"), to deal 187 | in the Software without restriction, including without limitation the rights 188 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 189 | copies of the Software, and to permit persons to whom the Software is 190 | furnished to do so, subject to the following conditions: 191 | 192 | The above copyright notice and this permission notice shall be included in 193 | all copies or substantial portions of the Software. 194 | 195 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 196 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 197 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 198 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 199 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 200 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 201 | THE SOFTWARE. 202 | -------------------------------------------------------------------------------- /Sources/Animator/MotionAnimator.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | public protocol MotionAnimator: class { 29 | /// A reference to Motion. 30 | var motion: MotionTransition! { get set } 31 | 32 | /// Cleans the contexts. 33 | func clean() 34 | 35 | /** 36 | A function that determines if a view can be animated. 37 | - Parameter view: A UIView. 38 | - Parameter isAppearing: A boolean that determines whether the 39 | view is appearing. 40 | */ 41 | func canAnimate(view: UIView, isAppearing: Bool) -> Bool 42 | 43 | /** 44 | Animates the fromViews to the toViews. 45 | - Parameter fromViews: An Array of UIViews. 46 | - Parameter toViews: An Array of UIViews. 47 | - Returns: A TimeInterval. 48 | */ 49 | func animate(fromViews: [UIView], toViews: [UIView]) -> TimeInterval 50 | 51 | /** 52 | Moves the view's animation to the given elapsed time. 53 | - Parameter to progress: A TimeInterval. 54 | */ 55 | func seek(to progress: TimeInterval) 56 | 57 | /** 58 | Resumes the animation with a given elapsed time and 59 | optional reversed boolean. 60 | - Parameter at progress: A TimeInterval. 61 | - Parameter isReversed: A boolean to reverse the animation 62 | or not. 63 | */ 64 | func resume(at progress: TimeInterval, isReversed: Bool) -> TimeInterval 65 | 66 | /** 67 | Applies the given state to the given view. 68 | - Parameter state: A MotionModifier. 69 | - Parameter to view: A UIView. 70 | */ 71 | func apply(state: MotionTargetState, to view: UIView) 72 | } 73 | -------------------------------------------------------------------------------- /Sources/Animator/MotionAnimatorViewContext.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | internal class MotionAnimatorViewContext: NSObject { 29 | /// An optional reference to a MotionAnimator. 30 | var animator: MotionAnimator? 31 | 32 | /// A reference to the snapshot UIView. 33 | var snapshot: UIView 34 | 35 | /// The animation target state. 36 | var targetState: MotionTargetState 37 | 38 | /// A boolean indicating if the view is appearing. 39 | var isAppearing: Bool 40 | 41 | /// Animation duration time. 42 | var duration: TimeInterval = 0 43 | 44 | /// A container view for the transition. 45 | var container: UIView? { 46 | return animator?.motion.context.container 47 | } 48 | 49 | /** 50 | An initializer. 51 | - Parameter animator: A MotionAnimator. 52 | - Parameter snapshot: A UIView. 53 | - Parameter targetState: A MotionModifier. 54 | - Parameter isAppearing: A Boolean. 55 | */ 56 | required init(animator: MotionAnimator, snapshot: UIView, targetState: MotionTargetState, isAppearing: Bool) { 57 | self.animator = animator 58 | self.snapshot = snapshot 59 | self.targetState = targetState 60 | self.isAppearing = isAppearing 61 | } 62 | 63 | /// Cleans the context. 64 | func clean() { 65 | animator = nil 66 | } 67 | 68 | /** 69 | A class function that determines if a view can be animated 70 | to a given state. 71 | - Parameter view: A UIView. 72 | - Parameter state: A MotionModifier. 73 | - Parameter isAppearing: A boolean that determines whether the 74 | view is appearing. 75 | */ 76 | class func canAnimate(view: UIView, state: MotionTargetState, isAppearing: Bool) -> Bool { 77 | return false 78 | } 79 | 80 | /** 81 | Resumes the animation with a given elapsed time and 82 | optional reversed boolean. 83 | - Parameter at progress: A TimeInterval. 84 | - Parameter isReversed: A boolean to reverse the animation 85 | or not. 86 | - Returns: A TimeInterval. 87 | */ 88 | @discardableResult 89 | func resume(at progress: TimeInterval, isReversed: Bool) -> TimeInterval { 90 | return 0 91 | } 92 | 93 | /** 94 | Moves the animation to the given elapsed time. 95 | - Parameter to progress: A TimeInterval. 96 | */ 97 | func seek(to progress: TimeInterval) {} 98 | 99 | /** 100 | Applies the given state to the target state. 101 | - Parameter state: A MotionModifier. 102 | */ 103 | func apply(state: MotionTargetState) {} 104 | 105 | /** 106 | Starts the animations with an appearing boolean flag. 107 | - Parameter isAppearing: A boolean value whether the view 108 | is appearing or not. 109 | */ 110 | @discardableResult 111 | func startAnimations() -> TimeInterval { 112 | return 0 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Sources/Animator/MotionCoreAnimator.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | internal class MotionCoreAnimator: MotionAnimator { 29 | /** 30 | Backing field for storing CACurrentMediaTime to ensure all 31 | animations begin at the exact same time. 32 | 33 | Should be invalidated using invalidateCurrentTime method 34 | after firing all animations. 35 | */ 36 | private var storedCurrentTime: TimeInterval? 37 | 38 | /// Current time for the animator. 39 | var currentTime: TimeInterval { 40 | if nil == storedCurrentTime { 41 | storedCurrentTime = CACurrentMediaTime() 42 | } 43 | 44 | return storedCurrentTime! 45 | } 46 | 47 | /// Invalidates stored current time. 48 | func invalidateCurrentTime() { 49 | storedCurrentTime = nil 50 | } 51 | 52 | weak public var motion: MotionTransition! 53 | 54 | /// A reference to the MotionContext. 55 | public var context: MotionContext! { 56 | return motion.context 57 | } 58 | 59 | /// An index of views to their corresponding animator context. 60 | var viewToContexts = [UIView: T]() 61 | 62 | /// Cleans the contexts. 63 | func clean() { 64 | for v in viewToContexts.values { 65 | v.clean() 66 | } 67 | 68 | viewToContexts.removeAll() 69 | invalidateCurrentTime() 70 | } 71 | 72 | /** 73 | A function that determines if a view can be animated. 74 | - Parameter view: A UIView. 75 | - Parameter isAppearing: A boolean that determines whether the 76 | view is appearing. 77 | */ 78 | func canAnimate(view: UIView, isAppearing: Bool) -> Bool { 79 | guard let state = targetState(for: view) else { 80 | return false 81 | } 82 | 83 | return T.canAnimate(view: view, state: state, isAppearing: isAppearing) 84 | } 85 | 86 | /** 87 | Animates the fromViews to the toViews. 88 | - Parameter fromViews: An Array of UIViews. 89 | - Parameter toViews: An Array of UIViews. 90 | - Returns: A TimeInterval. 91 | */ 92 | func animate(fromViews: [UIView], toViews: [UIView]) -> TimeInterval { 93 | var d: TimeInterval = 0 94 | 95 | for v in fromViews { 96 | createViewContext(view: v, isAppearing: false) 97 | } 98 | 99 | for v in toViews { 100 | createViewContext(view: v, isAppearing: true) 101 | } 102 | 103 | for v in viewToContexts.values { 104 | if let duration = v.targetState.duration, .infinity != duration { 105 | v.duration = duration 106 | d = max(d, duration) 107 | 108 | } else { 109 | let duration = v.snapshot.optimizedDuration(targetState: v.targetState) 110 | 111 | if nil == v.targetState.duration { 112 | v.duration = duration 113 | } 114 | 115 | d = max(d, duration) 116 | } 117 | } 118 | 119 | for v in viewToContexts.values { 120 | if .infinity == v.targetState.duration { 121 | v.duration = d 122 | } 123 | 124 | d = max(d, v.startAnimations()) 125 | } 126 | 127 | invalidateCurrentTime() 128 | 129 | return d 130 | } 131 | 132 | /** 133 | Moves the view's animation to the given elapsed time. 134 | - Parameter to progress: A TimeInterval. 135 | */ 136 | func seek(to progress: TimeInterval) { 137 | for v in viewToContexts.values { 138 | v.seek(to: progress) 139 | } 140 | 141 | invalidateCurrentTime() 142 | } 143 | 144 | /** 145 | Resumes the animation with a given elapsed time and 146 | optional reversed boolean. 147 | - Parameter at progress: A TimeInterval. 148 | - Parameter isReversed: A boolean to reverse the animation 149 | or not. 150 | */ 151 | func resume(at progress: TimeInterval, isReversed: Bool) -> TimeInterval { 152 | var duration: TimeInterval = 0 153 | 154 | for (_, v) in viewToContexts { 155 | duration = max(duration, v.resume(at: progress, isReversed: isReversed)) 156 | } 157 | 158 | invalidateCurrentTime() 159 | return duration 160 | } 161 | 162 | /** 163 | Applies the given state to the given view. 164 | - Parameter state: A MotionModifier. 165 | - Parameter to view: A UIView. 166 | */ 167 | func apply(state: MotionTargetState, to view: UIView) { 168 | guard let v = viewToContexts[view] else { 169 | return 170 | } 171 | 172 | v.apply(state: state) 173 | 174 | invalidateCurrentTime() 175 | } 176 | 177 | /** 178 | Returns MotionTargetState for the given view. 179 | - Parameter for view: A UIView. 180 | - Returns: A MotionTargetState. 181 | */ 182 | func targetState(for view: UIView) -> MotionTargetState? { 183 | return context[view] 184 | } 185 | 186 | /** 187 | Returns snapshot view for the given view. 188 | - Parameter for view: A UIView. 189 | - Returns: A snapshot UIView. 190 | */ 191 | func snapshotView(for view: UIView) -> UIView { 192 | return context.snapshotView(for: view) 193 | } 194 | } 195 | 196 | fileprivate extension MotionCoreAnimator { 197 | /** 198 | Creates a view context for a given view. 199 | - Parameter view: A UIView. 200 | - Parameter isAppearing: A boolean that determines whether the 201 | view is appearing. 202 | */ 203 | func createViewContext(view: UIView, isAppearing: Bool) { 204 | viewToContexts[view] = T(animator: self, snapshot: snapshotView(for: view), targetState: targetState(for: view)!, isAppearing: isAppearing) 205 | } 206 | } 207 | 208 | 209 | -------------------------------------------------------------------------------- /Sources/Animator/MotionViewPropertyViewContext.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | @available(iOS 10, tvOS 10, *) 29 | internal class MotionViewPropertyViewContext: MotionAnimatorViewContext { 30 | /// A reference to the UIViewPropertyAnimator. 31 | fileprivate var viewPropertyAnimator: UIViewPropertyAnimator! 32 | 33 | /// Ending effect. 34 | fileprivate var endEffect: UIVisualEffect? 35 | 36 | /// Starting effect. 37 | fileprivate var startEffect: UIVisualEffect? 38 | 39 | override class func canAnimate(view: UIView, state: MotionTargetState, isAppearing: Bool) -> Bool { 40 | return view is UIVisualEffectView && nil != state.opacity 41 | } 42 | 43 | override func resume(at progress: TimeInterval, isReversed: Bool) -> TimeInterval { 44 | guard let visualEffectView = snapshot as? UIVisualEffectView else { 45 | return 0 46 | } 47 | 48 | if isReversed { 49 | viewPropertyAnimator?.stopAnimation(false) 50 | viewPropertyAnimator?.finishAnimation(at: .current) 51 | viewPropertyAnimator = UIViewPropertyAnimator(duration: duration, curve: .linear) { [weak self] in 52 | guard let `self` = self else { 53 | return 54 | } 55 | 56 | visualEffectView.effect = isReversed ? self.startEffect : self.endEffect 57 | } 58 | } 59 | 60 | viewPropertyAnimator.startAnimation() 61 | 62 | return duration 63 | } 64 | 65 | override func seek(to progress: TimeInterval) { 66 | viewPropertyAnimator?.pauseAnimation() 67 | viewPropertyAnimator?.fractionComplete = CGFloat(progress / duration) 68 | } 69 | 70 | override func clean() { 71 | super.clean() 72 | viewPropertyAnimator?.stopAnimation(false) 73 | viewPropertyAnimator?.finishAnimation(at: .current) 74 | viewPropertyAnimator = nil 75 | } 76 | 77 | override func startAnimations() -> TimeInterval { 78 | guard let visualEffectView = snapshot as? UIVisualEffectView else { 79 | return 0 80 | } 81 | 82 | let appearedEffect = visualEffectView.effect 83 | let disappearedEffect = 0 == targetState.opacity ? nil : visualEffectView.effect 84 | 85 | startEffect = isAppearing ? disappearedEffect : appearedEffect 86 | endEffect = isAppearing ? appearedEffect : disappearedEffect 87 | 88 | visualEffectView.effect = startEffect 89 | 90 | viewPropertyAnimator = UIViewPropertyAnimator(duration: duration, curve: .linear) { [weak self] in 91 | guard let `self` = self else { 92 | return 93 | } 94 | 95 | visualEffectView.effect = self.endEffect 96 | } 97 | 98 | viewPropertyAnimator.startAnimation() 99 | 100 | return duration 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/Animator/MotionViewTransitionAnimator.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | internal class MotionViewTransitionAnimator: MotionCoreAnimator { 29 | /** 30 | Returns MotionTargetState for the given view. 31 | - Parameter for view: A UIView. 32 | - Returns: A MotionTargetState. 33 | */ 34 | override func targetState(for view: UIView) -> MotionTargetState? { 35 | guard let modifiers = view.motionModifiers else { 36 | return nil 37 | } 38 | 39 | return MotionTargetState(modifiers: modifiers) 40 | } 41 | 42 | /** 43 | Returns snapshot view for the given view. 44 | - Parameter for view: A UIView. 45 | - Returns: A snapshot UIView. 46 | */ 47 | override func snapshotView(for view: UIView) -> UIView { 48 | return view 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Application.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | internal class Application { 29 | static var shared: UIApplication { 30 | let sharedSelector = NSSelectorFromString("sharedApplication") 31 | guard UIApplication.responds(to: sharedSelector) else { 32 | fatalError("[Motion: Extensions cannot access Application]") 33 | } 34 | 35 | let shared = UIApplication.perform(sharedSelector) 36 | return shared?.takeUnretainedValue() as! UIApplication 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Extensions/Motion+Array.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | internal extension Array { 29 | /** 30 | Retrieves the element at the given index if it exists. 31 | - Parameter _ index: An Int. 32 | - Returns: An optional Element value. 33 | */ 34 | func get(_ index: Int) -> Element? { 35 | if index < count { 36 | return self[index] 37 | } 38 | return nil 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /Sources/Extensions/Motion+CALayer.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | internal extension CALayer { 29 | /// Swizzle the `add(_:forKey:) selector. 30 | static var motionAddedAnimations: [(CALayer, String, CAAnimation)]? = { 31 | let swizzling: (AnyClass, Selector, Selector) -> Void = { forClass, originalSelector, swizzledSelector in 32 | if let originalMethod = class_getInstanceMethod(forClass, originalSelector), let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) { 33 | method_exchangeImplementations(originalMethod, swizzledMethod) 34 | } 35 | } 36 | 37 | swizzling(CALayer.self, #selector(add(_:forKey:)), #selector(motionAdd(anim:forKey:))) 38 | 39 | return nil 40 | }() 41 | 42 | @objc 43 | dynamic func motionAdd(anim: CAAnimation, forKey: String?) { 44 | if nil == CALayer.motionAddedAnimations { 45 | motionAdd(anim: anim, forKey: forKey) 46 | } else { 47 | let copiedAnim = anim.copy() as! CAAnimation 48 | copiedAnim.delegate = nil // having delegate resulted some weird animation behavior 49 | CALayer.motionAddedAnimations?.append((self, forKey!, copiedAnim)) 50 | } 51 | } 52 | 53 | /// Retrieves all currently running animations for the layer. 54 | var animations: [(String, CAAnimation)] { 55 | guard let keys = animationKeys() else { 56 | return [] 57 | } 58 | 59 | return keys.map { 60 | return ($0, self.animation(forKey: $0)!.copy() as! CAAnimation) 61 | } 62 | } 63 | 64 | /** 65 | Concats transforms and returns the result. 66 | - Parameters layer: A CALayer. 67 | - Returns: A CATransform3D. 68 | */ 69 | func flatTransformTo(layer: CALayer) -> CATransform3D { 70 | var l = layer 71 | var t = l.transform 72 | 73 | while let sl = l.superlayer, self != sl { 74 | t = CATransform3DConcat(sl.transform, t) 75 | l = sl 76 | } 77 | 78 | return t 79 | } 80 | 81 | /// Removes all Motion animations. 82 | func removeAllMotionAnimations() { 83 | guard let keys = animationKeys() else { 84 | return 85 | } 86 | 87 | for animationKey in keys where animationKey.hasPrefix("motion.") { 88 | removeAnimation(forKey: animationKey) 89 | } 90 | } 91 | } 92 | 93 | public extension CALayer { 94 | /** 95 | A function that accepts CAAnimation objects and executes them on the 96 | view's backing layer. 97 | - Parameter animation: A CAAnimation instance. 98 | */ 99 | func animate(_ animations: CAAnimation...) { 100 | animate(animations) 101 | } 102 | 103 | /** 104 | A function that accepts CAAnimation objects and executes them on the 105 | view's backing layer. 106 | - Parameter animation: A CAAnimation instance. 107 | */ 108 | func animate(_ animations: [CAAnimation]) { 109 | for animation in animations { 110 | if let a = animation as? CABasicAnimation { 111 | a.fromValue = (presentation() ?? self).value(forKeyPath: a.keyPath!) 112 | } 113 | 114 | updateModel(animation) 115 | if let a = animation as? CAPropertyAnimation { 116 | add(a, forKey: a.keyPath!) 117 | } else if let a = animation as? CAAnimationGroup { 118 | add(a, forKey: nil) 119 | } else if let a = animation as? CATransition { 120 | add(a, forKey: kCATransition) 121 | } 122 | } 123 | } 124 | 125 | /** 126 | A function that accepts a list of MotionAnimation values and executes them. 127 | - Parameter animations: A list of MotionAnimation values. 128 | */ 129 | func animate(_ animations: MotionAnimation...) { 130 | animate(animations) 131 | } 132 | 133 | /** 134 | A function that accepts an Array of MotionAnimation values and executes them. 135 | - Parameter animations: An Array of MotionAnimation values. 136 | - Parameter completion: An optional completion block. 137 | */ 138 | func animate(_ animations: [MotionAnimation], completion: (() -> Void)? = nil) { 139 | startAnimations(animations, completion: completion) 140 | } 141 | } 142 | 143 | fileprivate extension CALayer { 144 | /** 145 | A function that executes an Array of MotionAnimation values. 146 | - Parameter _ animations: An Array of MotionAnimations. 147 | - Parameter completion: An optional completion block. 148 | */ 149 | func startAnimations(_ animations: [MotionAnimation], completion: (() -> Void)? = nil) { 150 | let ts = MotionAnimationState(animations: animations) 151 | 152 | Motion.delay(ts.delay) { [weak self, 153 | ts = ts, 154 | completion = completion] in 155 | 156 | guard let `self` = self else { 157 | return 158 | } 159 | 160 | var anims = [CABasicAnimation]() 161 | var duration = 0 == ts.duration ? 0.01 : ts.duration 162 | 163 | if let v = ts.backgroundColor { 164 | let a = MotionCAAnimation.background(color: UIColor(cgColor: v)) 165 | a.fromValue = self.backgroundColor 166 | anims.append(a) 167 | } 168 | 169 | if let v = ts.borderColor { 170 | let a = MotionCAAnimation.border(color: UIColor(cgColor: v)) 171 | a.fromValue = self.borderColor 172 | anims.append(a) 173 | } 174 | 175 | if let v = ts.borderWidth { 176 | let a = MotionCAAnimation.border(width: v) 177 | a.fromValue = NSNumber(floatLiteral: Double(self.borderWidth)) 178 | anims.append(a) 179 | } 180 | 181 | if let v = ts.cornerRadius { 182 | let a = MotionCAAnimation.corner(radius: v) 183 | a.fromValue = NSNumber(floatLiteral: Double(self.cornerRadius)) 184 | anims.append(a) 185 | } 186 | 187 | if let v = ts.transform { 188 | let a = MotionCAAnimation.transform(v) 189 | a.fromValue = NSValue(caTransform3D: self.transform) 190 | anims.append(a) 191 | } 192 | 193 | if let v = ts.spin { 194 | var a = MotionCAAnimation.spin(x: v.x) 195 | a.fromValue = NSNumber(floatLiteral: 0) 196 | anims.append(a) 197 | 198 | a = MotionCAAnimation.spin(y: v.y) 199 | a.fromValue = NSNumber(floatLiteral: 0) 200 | anims.append(a) 201 | 202 | a = MotionCAAnimation.spin(z: v.z) 203 | a.fromValue = NSNumber(floatLiteral: 0) 204 | anims.append(a) 205 | } 206 | 207 | if let v = ts.position { 208 | let a = MotionCAAnimation.position(v) 209 | a.fromValue = NSValue(cgPoint: self.position) 210 | anims.append(a) 211 | } 212 | 213 | if let v = ts.opacity { 214 | let a = MotionCAAnimation.fade(v) 215 | a.fromValue = self.value(forKeyPath: MotionAnimationKeyPath.opacity.rawValue) ?? NSNumber(floatLiteral: 1) 216 | anims.append(a) 217 | } 218 | 219 | if let v = ts.zPosition { 220 | let a = MotionCAAnimation.zPosition(v) 221 | a.fromValue = self.value(forKeyPath: MotionAnimationKeyPath.zPosition.rawValue) ?? NSNumber(floatLiteral: 0) 222 | anims.append(a) 223 | } 224 | 225 | if let v = ts.size { 226 | let a = MotionCAAnimation.size(v) 227 | a.fromValue = NSValue(cgSize: self.bounds.size) 228 | anims.append(a) 229 | } 230 | 231 | if let v = ts.shadowPath { 232 | let a = MotionCAAnimation.shadow(path: v) 233 | a.fromValue = self.shadowPath 234 | anims.append(a) 235 | } 236 | 237 | if let v = ts.shadowColor { 238 | let a = MotionCAAnimation.shadow(color: UIColor(cgColor: v)) 239 | a.fromValue = self.shadowColor 240 | anims.append(a) 241 | } 242 | 243 | if let v = ts.shadowOffset { 244 | let a = MotionCAAnimation.shadow(offset: v) 245 | a.fromValue = NSValue(cgSize: self.shadowOffset) 246 | anims.append(a) 247 | } 248 | 249 | if let v = ts.shadowOpacity { 250 | let a = MotionCAAnimation.shadow(opacity: v) 251 | a.fromValue = NSNumber(floatLiteral: Double(self.shadowOpacity)) 252 | anims.append(a) 253 | } 254 | 255 | if let v = ts.shadowRadius { 256 | let a = MotionCAAnimation.shadow(radius: v) 257 | a.fromValue = NSNumber(floatLiteral: Double(self.shadowRadius)) 258 | anims.append(a) 259 | } 260 | 261 | if #available(iOS 9.0, *), let (stiffness, damping) = ts.spring { 262 | for i in 0.. duration { 273 | duration = a.settlingDuration 274 | } 275 | } 276 | } 277 | 278 | let g = Motion.animate(group: anims, timingFunction: ts.timingFunction, duration: duration) 279 | self.animate(g) 280 | 281 | if let v = ts.completion { 282 | Motion.delay(duration, execute: v) 283 | } 284 | 285 | if let v = completion { 286 | Motion.delay(duration, execute: v) 287 | } 288 | } 289 | } 290 | } 291 | 292 | private extension CALayer { 293 | /** 294 | Updates the model with values provided in animation. 295 | - Parameter animation: A CAAnimation. 296 | */ 297 | func updateModel(_ animation: CAAnimation) { 298 | if let a = animation as? CABasicAnimation { 299 | setValue(a.toValue, forKeyPath: a.keyPath!) 300 | } else if let a = animation as? CAAnimationGroup { 301 | a.animations?.forEach { 302 | updateModel($0) 303 | } 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /Sources/Extensions/Motion+CAMediaTimingFunction.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | public extension CAMediaTimingFunction { 29 | // Default 30 | static let linear = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) 31 | static let easeIn = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn) 32 | static let easeOut = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) 33 | static let easeInOut = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 34 | 35 | // Material 36 | static let standard = CAMediaTimingFunction(controlPoints: 0.4, 0.0, 0.2, 1.0) 37 | static let deceleration = CAMediaTimingFunction(controlPoints: 0.0, 0.0, 0.2, 1) 38 | static let acceleration = CAMediaTimingFunction(controlPoints: 0.4, 0.0, 1, 1) 39 | static let sharp = CAMediaTimingFunction(controlPoints: 0.4, 0.0, 0.6, 1) 40 | 41 | // Easing.net 42 | static let easeOutBack = CAMediaTimingFunction(controlPoints: 0.175, 0.885, 0.32, 1.75) 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Extensions/Motion+CG.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import MetalKit 27 | 28 | public extension CGSize { 29 | /// THe center point based on width and height. 30 | var center: CGPoint { 31 | return CGPoint(x: width / 2, y: height / 2) 32 | } 33 | 34 | /// Top left point based on the size. 35 | var topLeft: CGPoint { 36 | return .zero 37 | } 38 | 39 | /// Top right point based on the size. 40 | var topRight: CGPoint { 41 | return CGPoint(x: width, y: 0) 42 | } 43 | 44 | /// Bottom left point based on the size. 45 | var bottomLeftPoint: CGPoint { 46 | return CGPoint(x: 0, y: height) 47 | } 48 | 49 | /// Bottom right point based on the size. 50 | var bottomRight: CGPoint { 51 | return CGPoint(x: width, y: height) 52 | } 53 | 54 | /** 55 | Retrieves the size based on a given CGAffineTransform. 56 | - Parameter _ t: A CGAffineTransform. 57 | - Returns: A CGSize. 58 | */ 59 | func transform(_ t: CGAffineTransform) -> CGSize { 60 | return applying(t) 61 | } 62 | 63 | /** 64 | Retrieves the size based on a given CATransform3D. 65 | - Parameter _ t: A CGAffineTransform. 66 | - Returns: A CGSize. 67 | */ 68 | func transform(_ t: CATransform3D) -> CGSize { 69 | return applying(CATransform3DGetAffineTransform(t)) 70 | } 71 | } 72 | 73 | public extension CGRect { 74 | /// A center point based on the origin and size values. 75 | var center: CGPoint { 76 | return CGPoint(x: origin.x + width / 2, y: origin.y + height / 2) 77 | } 78 | 79 | /// The bounding box size based from from the frame's rect. 80 | var bounds: CGRect { 81 | return CGRect(origin: .zero, size: size) 82 | } 83 | 84 | /** 85 | An initializer with a given point and size. 86 | - Parameter center: A CGPoint. 87 | - Parameter size: A CGSize. 88 | */ 89 | init(center: CGPoint, size: CGSize) { 90 | self.init(x: center.x - size.width / 2, y: center.y - size.height / 2, width: size.width, height: size.height) 91 | } 92 | } 93 | 94 | public extension CGFloat { 95 | /** 96 | Calculates the limiting position to an area. 97 | - Parameter _ a: A CGFloat. 98 | - Parameter _ b: A CGFloat. 99 | - Returns: A CGFloat. 100 | */ 101 | func clamp(_ a: CGFloat, _ b: CGFloat) -> CGFloat { 102 | return self < a ? a : self > b ? b : self 103 | } 104 | } 105 | 106 | public extension CGPoint { 107 | /** 108 | Calculates a translation point based on the origin value. 109 | - Parameter _ dx: A CGFloat. 110 | - Parameter _ dy: A CGFloat. 111 | - Returns: A CGPoint. 112 | */ 113 | func translate(_ dx: CGFloat, dy: CGFloat) -> CGPoint { 114 | return CGPoint(x: x + dx, y: y + dy) 115 | } 116 | 117 | /** 118 | Calculates a transform point based on a given CGAffineTransform. 119 | - Parameter _ t: CGAffineTransform. 120 | - Returns: A CGPoint. 121 | */ 122 | func transform(_ t: CGAffineTransform) -> CGPoint { 123 | return applying(t) 124 | } 125 | 126 | /** 127 | Calculates a transform point based on a given CATransform3D. 128 | - Parameter _ t: CATransform3D. 129 | - Returns: A CGPoint. 130 | */ 131 | func transform(_ t: CATransform3D) -> CGPoint { 132 | return applying(CATransform3DGetAffineTransform(t)) 133 | } 134 | 135 | /** 136 | Calculates the distance between the CGPoint and given CGPoint. 137 | - Parameter _ b: A CGPoint. 138 | - Returns: A CGFloat. 139 | */ 140 | func distance(_ b: CGPoint) -> CGFloat { 141 | return sqrt(pow(x - b.x, 2) + pow(y - b.y, 2)) 142 | } 143 | } 144 | 145 | /** 146 | A handler for the (+) operator. 147 | - Parameter left: A CGPoint. 148 | - Parameter right: A CGPoint. 149 | - Returns: A CGPoint. 150 | */ 151 | public func + (left: CGPoint, right: CGPoint) -> CGPoint { 152 | return CGPoint(x: left.x + right.x, y: left.y + right.y) 153 | } 154 | 155 | /** 156 | A handler for the (-) operator. 157 | - Parameter left: A CGPoint. 158 | - Parameter right: A CGPoint. 159 | - Returns: A CGPoint. 160 | */ 161 | public func - (left: CGPoint, right: CGPoint) -> CGPoint { 162 | return CGPoint(x: left.x - right.x, y: left.y - right.y) 163 | } 164 | 165 | /** 166 | A handler for the (/) operator. 167 | - Parameter left: A CGPoint. 168 | - Parameter right: A CGFloat. 169 | - Returns: A CGPoint. 170 | */ 171 | public func / (left: CGPoint, right: CGFloat) -> CGPoint { 172 | return CGPoint(x: left.x / right, y: left.y / right) 173 | } 174 | 175 | /** 176 | A handler for the (/) operator. 177 | - Parameter left: A CGPoint. 178 | - Parameter right: A CGPoint. 179 | - Returns: A CGPoint. 180 | */ 181 | public func / (left: CGPoint, right: CGPoint) -> CGPoint { 182 | return CGPoint(x: left.x / right.x, y: left.y / right.y) 183 | } 184 | 185 | /** 186 | A handler for the (/) operator. 187 | - Parameter left: A CGPoint. 188 | - Parameter right: A CGSize. 189 | - Returns: A CGPoint. 190 | */ 191 | public func / (left: CGPoint, right: CGSize) -> CGPoint { 192 | return CGPoint(x: left.x / right.width, y: left.y / right.height) 193 | } 194 | 195 | /** 196 | A handler for the (/) operator. 197 | - Parameter left: A CGSize. 198 | - Parameter right: A CGSize. 199 | - Returns: A CGSize. 200 | */ 201 | public func / (left: CGSize, right: CGSize) -> CGSize { 202 | return CGSize(width: left.width / right.width, height: left.height / right.height) 203 | } 204 | 205 | /** 206 | A handler for the (*) operator. 207 | - Parameter left: A CGPoint. 208 | - Parameter right: A CGFloat. 209 | - Returns: A CGPoint. 210 | */ 211 | public func * (left: CGPoint, right: CGFloat) -> CGPoint { 212 | return CGPoint(x: left.x * right, y: left.y * right) 213 | } 214 | 215 | /** 216 | A handler for the (*) operator. 217 | - Parameter left: A CGPoint. 218 | - Parameter right: A CGSize. 219 | - Returns: A CGPoint. 220 | */ 221 | public func * (left: CGPoint, right: CGSize) -> CGPoint { 222 | return CGPoint(x: left.x * right.width, y: left.y * right.width) 223 | } 224 | 225 | /** 226 | A handler for the (*) operator. 227 | - Parameter left: A CGFloat. 228 | - Parameter right: A CGPoint. 229 | - Returns: A CGPoint. 230 | */ 231 | public func * (left: CGFloat, right: CGPoint) -> CGPoint { 232 | return right * left 233 | } 234 | 235 | /** 236 | A handler for the (*) operator. 237 | - Parameter left: A CGPoint. 238 | - Parameter right: A CGPoint. 239 | - Returns: A CGPoint. 240 | */ 241 | public func * (left: CGPoint, right: CGPoint) -> CGPoint { 242 | return CGPoint(x: left.x * right.x, y: left.y * right.y) 243 | } 244 | 245 | /** 246 | A handler for the (*) prefix. 247 | - Parameter left: A CGSize. 248 | - Parameter right: A CGFloat. 249 | - Returns: A CGSize. 250 | */ 251 | public func * (left: CGSize, right: CGFloat) -> CGSize { 252 | return CGSize(width: left.width * right, height: left.height * right) 253 | } 254 | 255 | /** 256 | A handler for the (*) prefix. 257 | - Parameter left: A CGSize. 258 | - Parameter right: A CGSize. 259 | - Returns: A CGSize. 260 | */ 261 | public func * (left: CGSize, right: CGSize) -> CGSize { 262 | return CGSize(width: left.width * right.width, height: left.height * right.height) 263 | } 264 | 265 | /** 266 | A handler for the (==) operator. 267 | - Parameter lhs: A CATransform3D. 268 | - Parameter rhs: A CATransform3D. 269 | - Returns: A Bool. 270 | */ 271 | public func == (lhs: CATransform3D, rhs: CATransform3D) -> Bool { 272 | var lhs = lhs 273 | var rhs = rhs 274 | return 0 == memcmp(&lhs, &rhs, MemoryLayout.size) 275 | } 276 | 277 | /** 278 | A handler for the (!=) operator. 279 | - Parameter lhs: A CATransform3D. 280 | - Parameter rhs: A CATransform3D. 281 | - Returns: A Bool. 282 | */ 283 | public func != (lhs: CATransform3D, rhs: CATransform3D) -> Bool { 284 | return !(lhs == rhs) 285 | } 286 | 287 | /** 288 | A handler for the (-) prefix. 289 | - Parameter point: A CGPoint. 290 | - Returns: A CGPoint. 291 | */ 292 | public prefix func - (point: CGPoint) -> CGPoint { 293 | return .zero - point 294 | } 295 | 296 | /** 297 | A handler for the (abs) function. 298 | - Parameter _ p: A CGPoint. 299 | - Returns: A CGPoint. 300 | */ 301 | public func abs(_ p: CGPoint) -> CGPoint { 302 | return CGPoint(x: abs(p.x), y: abs(p.y)) 303 | } 304 | -------------------------------------------------------------------------------- /Sources/Extensions/Motion+Obj-C.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import ObjectiveC 27 | 28 | public struct AssociatedObject { 29 | /** 30 | Gets the Obj-C reference for the instance object within the UIView extension. 31 | - Parameter base: Base object. 32 | - Parameter key: Memory key pointer. 33 | - Parameter initializer: Object initializer. 34 | - Returns: The associated reference for the initializer object. 35 | */ 36 | public static func get(base: Any, key: UnsafePointer, initializer: () -> T) -> T { 37 | if let v = objc_getAssociatedObject(base, key) as? T { 38 | return v 39 | } 40 | 41 | let v = initializer() 42 | objc_setAssociatedObject(base, key, v, .OBJC_ASSOCIATION_RETAIN) 43 | return v 44 | } 45 | 46 | /** 47 | Sets the Obj-C reference for the instance object within the UIView extension. 48 | - Parameter base: Base object. 49 | - Parameter key: Memory key pointer. 50 | - Parameter value: The object instance to set for the associated object. 51 | - Returns: The associated reference for the initializer object. 52 | */ 53 | public static func set(base: Any, key: UnsafePointer, value: T) { 54 | objc_setAssociatedObject(base, key, value, .OBJC_ASSOCIATION_RETAIN) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Extensions/Motion+UIKit.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | fileprivate let parameterRegex = "(?:\\-?\\d+(\\.?\\d+)?)|\\w+" 29 | fileprivate let transitionsRegex = "(\\w+)(?:\\(([^\\)]*)\\))?" 30 | 31 | internal extension NSObject { 32 | /// Copies an object using NSKeyedArchiver. 33 | func copyWithArchiver() -> Any? { 34 | return NSKeyedUnarchiver.unarchiveObject(with: NSKeyedArchiver.archivedData(withRootObject: self))! 35 | } 36 | } 37 | 38 | internal extension UIColor { 39 | /// A tuple of the rgba components. 40 | var components: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) { 41 | var r: CGFloat = 0 42 | var g: CGFloat = 0 43 | var b: CGFloat = 0 44 | var a: CGFloat = 0 45 | 46 | getRed(&r, green: &g, blue: &b, alpha: &a) 47 | 48 | return (r, g, b, a) 49 | } 50 | 51 | /// The alpha component value. 52 | var alphaComponent: CGFloat { 53 | return components.a 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Extensions/Motion+UIView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | fileprivate var AssociatedInstanceKey: UInt8 = 0 29 | 30 | fileprivate struct AssociatedInstance { 31 | /// A boolean indicating whether Motion is enabled. 32 | fileprivate var isEnabled: Bool 33 | 34 | /// A boolean indicating whether Motion is enabled for subviews. 35 | fileprivate var isEnabledForSubviews: Bool 36 | 37 | /// An optional reference to the motion identifier. 38 | fileprivate var identifier: String? 39 | 40 | /// An optional reference to the motion animations. 41 | fileprivate var animations: [MotionAnimation]? 42 | 43 | /// An optional reference to the motion animation modifiers. 44 | fileprivate var modifiers: [MotionModifier]? 45 | 46 | /// An alpha value. 47 | fileprivate var alpha: CGFloat? 48 | } 49 | 50 | fileprivate extension UIView { 51 | /// AssociatedInstance reference. 52 | var associatedInstance: AssociatedInstance { 53 | get { 54 | return AssociatedObject.get(base: self, key: &AssociatedInstanceKey) { 55 | return AssociatedInstance(isEnabled: true, isEnabledForSubviews: true, identifier: nil, animations: nil, modifiers: nil, alpha: nil) 56 | } 57 | } 58 | set(value) { 59 | AssociatedObject.set(base: self, key: &AssociatedInstanceKey, value: value) 60 | } 61 | } 62 | } 63 | 64 | public extension UIView { 65 | /// A boolean that indicates whether motion is enabled. 66 | @IBInspectable 67 | var isMotionEnabled: Bool { 68 | get { 69 | return associatedInstance.isEnabled 70 | } 71 | set(value) { 72 | associatedInstance.isEnabled = value 73 | } 74 | } 75 | 76 | /// A boolean that indicates whether motion is enabled. 77 | @IBInspectable 78 | var isMotionEnabledForSubviews: Bool { 79 | get { 80 | return associatedInstance.isEnabledForSubviews 81 | } 82 | set(value) { 83 | associatedInstance.isEnabledForSubviews = value 84 | } 85 | } 86 | 87 | /// An identifier value used to connect views across UIViewControllers. 88 | @IBInspectable 89 | var motionIdentifier: String? { 90 | get { 91 | return associatedInstance.identifier 92 | } 93 | set(value) { 94 | associatedInstance.identifier = value 95 | } 96 | } 97 | 98 | /** 99 | A function that accepts CAAnimation objects and executes them on the 100 | view's backing layer. 101 | - Parameter animations: A list of CAAnimations. 102 | */ 103 | func animate(_ animations: CAAnimation...) { 104 | layer.animate(animations) 105 | } 106 | 107 | /** 108 | A function that accepts an Array of CAAnimation objects and executes 109 | them on the view's backing layer. 110 | - Parameter animations: An Array of CAAnimations. 111 | */ 112 | func animate(_ animations: [CAAnimation]) { 113 | layer.animate(animations) 114 | } 115 | 116 | /** 117 | A function that accepts a list of MotionAnimation values and executes 118 | them on the view's backing layer. 119 | - Parameter animations: A list of MotionAnimation values. 120 | */ 121 | func animate(_ animations: MotionAnimation...) { 122 | layer.animate(animations) 123 | } 124 | 125 | /** 126 | A function that accepts an Array of MotionAnimation values and executes 127 | them on the view's backing layer. 128 | - Parameter animations: An Array of MotionAnimation values. 129 | - Parameter completion: An optional completion block. 130 | */ 131 | func animate(_ animations: [MotionAnimation], completion: (() -> Void)? = nil) { 132 | layer.animate(animations, completion: completion) 133 | } 134 | 135 | /** 136 | A function that accepts a list of MotionTargetState values. 137 | - Parameter transitions: A list of MotionTargetState values. 138 | */ 139 | func transition(_ modifiers: MotionModifier...) { 140 | transition(modifiers) 141 | } 142 | 143 | /** 144 | A function that accepts an Array of MotionTargetState values. 145 | - Parameter transitions: An Array of MotionTargetState values. 146 | */ 147 | func transition(_ modifiers: [MotionModifier]) { 148 | motionModifiers = modifiers 149 | } 150 | } 151 | 152 | internal extension UIView { 153 | /// The animations to run while in transition. 154 | var motionModifiers: [MotionModifier]? { 155 | get { 156 | return associatedInstance.modifiers 157 | } 158 | set(value) { 159 | associatedInstance.modifiers = value 160 | } 161 | } 162 | 163 | /// The animations to run while in transition. 164 | var motionAlpha: CGFloat? { 165 | get { 166 | return associatedInstance.alpha 167 | } 168 | set(value) { 169 | associatedInstance.alpha = value 170 | } 171 | } 172 | 173 | /// Computes the rotate of the view. 174 | var motionRotationAngle: CGFloat { 175 | get { 176 | return CGFloat(atan2f(Float(transform.b), Float(transform.a))) * 180 / CGFloat(Double.pi) 177 | } 178 | set(value) { 179 | transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi) * value / 180) 180 | } 181 | } 182 | 183 | /// The global position of a view. 184 | var motionPosition: CGPoint { 185 | return superview?.convert(layer.position, to: nil) ?? layer.position 186 | } 187 | 188 | /// The layer.transform of a view. 189 | var motionTransform: CATransform3D { 190 | get { 191 | return layer.transform 192 | } 193 | set(value) { 194 | layer.transform = value 195 | } 196 | } 197 | 198 | /// Computes the scale X axis value of the view. 199 | var motionScaleX: CGFloat { 200 | return transform.a 201 | } 202 | 203 | /// Computes the scale Y axis value of the view. 204 | var motionScaleY: CGFloat { 205 | return transform.b 206 | } 207 | } 208 | 209 | internal class SnapshotWrapperView: UIView { 210 | /// A reference to the contentView. 211 | let contentView: UIView 212 | 213 | /** 214 | An initializer that takes in a contentView. 215 | - Parameter contentView: A UIView. 216 | */ 217 | init(contentView: UIView) { 218 | self.contentView = contentView 219 | super.init(frame: contentView.frame) 220 | contentView.frame = bounds 221 | addSubview(contentView) 222 | } 223 | 224 | required init?(coder aDecoder: NSCoder) { 225 | fatalError("init(coder:) has not been implemented") 226 | } 227 | 228 | override func layoutSubviews() { 229 | super.layoutSubviews() 230 | contentView.frame = bounds 231 | } 232 | } 233 | 234 | internal extension UIView { 235 | /// Retrieves a single Array of UIViews that are in the view hierarchy. 236 | var flattenedViewHierarchy: [UIView] { 237 | guard isMotionEnabled else { 238 | return [] 239 | } 240 | 241 | if #available(iOS 9.0, *), isHidden && (superview is UICollectionView || superview is UIStackView || self is UITableViewCell) { 242 | return [] 243 | 244 | } else if isHidden && (superview is UICollectionView || self is UITableViewCell) { 245 | return [] 246 | 247 | } else if isMotionEnabledForSubviews { 248 | return [self] + subviews.flatMap { 249 | $0.flattenedViewHierarchy 250 | } 251 | } 252 | 253 | return [self] 254 | } 255 | 256 | /** 257 | Retrieves the optimized duration based on the given parameters. 258 | - Parameter fromPosition: A CGPoint. 259 | - Parameter toPosition: An optional CGPoint. 260 | - Parameter size: An optional CGSize. 261 | - Parameter transform: An optional CATransform3D. 262 | - Returns: A TimeInterval. 263 | */ 264 | func optimizedDuration(position: CGPoint?, size: CGSize?, transform: CATransform3D?) -> TimeInterval { 265 | let fromPos = (layer.presentation() ?? layer).position 266 | let toPos = position ?? fromPos 267 | let fromSize = (layer.presentation() ?? layer).bounds.size 268 | let toSize = size ?? fromSize 269 | let fromTransform = (layer.presentation() ?? layer).transform 270 | let toTransform = transform ?? fromTransform 271 | 272 | let realFromPos = CGPoint.zero.transform(fromTransform) + fromPos 273 | let realToPos = CGPoint.zero.transform(toTransform) + toPos 274 | 275 | let realFromSize = fromSize.transform(fromTransform) 276 | let realToSize = toSize.transform(toTransform) 277 | 278 | let movePoints = (realFromPos.distance(realToPos) + realFromSize.bottomRight.distance(realToSize.bottomRight)) 279 | 280 | // duration is 0.2 @ 0 to 0.375 @ 500 281 | return 0.208 + Double(movePoints.clamp(0, 500)) / 3000 282 | } 283 | 284 | /** 285 | Calculates the optimized duration for a view. 286 | - Parameter targetState: A MotionTargetState. 287 | - Returns: A TimeInterval. 288 | */ 289 | func optimizedDuration(targetState: MotionTargetState) -> TimeInterval { 290 | return optimizedDuration(position: targetState.position, 291 | size: targetState.size, 292 | transform: targetState.transform) 293 | } 294 | 295 | /** 296 | Takes a snapshot of a view usinag the UI graphics context. 297 | - Returns: A UIView with an embedded UIImageView. 298 | */ 299 | func slowSnapshotView() -> UIView { 300 | UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0) 301 | 302 | guard let currentContext = UIGraphicsGetCurrentContext() else { 303 | UIGraphicsEndImageContext() 304 | return UIView() 305 | } 306 | 307 | layer.render(in: currentContext) 308 | 309 | let image = UIGraphicsGetImageFromCurrentImageContext() 310 | UIGraphicsEndImageContext() 311 | 312 | let imageView = UIImageView(image: image) 313 | imageView.frame = bounds 314 | 315 | return SnapshotWrapperView(contentView: imageView) 316 | } 317 | 318 | /** 319 | Returns a snapshot of the view itself. 320 | - Returns: An optional UIView. 321 | */ 322 | func snapshotView() -> UIView? { 323 | let snapshot = snapshotView(afterScreenUpdates: true) 324 | 325 | if #available(iOS 11.0, *), let oldSnapshot = snapshot { 326 | // iOS 11 no longer contains a container view. 327 | return SnapshotWrapperView(contentView: oldSnapshot) 328 | } 329 | 330 | return snapshot 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /Sources/Extensions/Motion+UIViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | fileprivate var AssociatedInstanceKey: UInt8 = 0 29 | 30 | fileprivate struct AssociatedInstance { 31 | /// A reference to the modal animation. 32 | var modalTransitionType: MotionTransitionAnimationType 33 | 34 | /// A reference to the navigation animation. 35 | var navigationTransitionType: MotionTransitionAnimationType 36 | 37 | /// A reference to the tabBar animation. 38 | var tabBarTransitionType: MotionTransitionAnimationType 39 | 40 | /// A reference to the stored snapshot. 41 | var storedSnapshot: UIView? 42 | 43 | /** 44 | A reference to the previous navigation controller delegate 45 | before Motion was enabled. 46 | */ 47 | weak var previousNavigationDelegate: UINavigationControllerDelegate? 48 | 49 | /** 50 | A reference to the previous tab bar controller delegate 51 | before Motion was enabled. 52 | */ 53 | weak var previousTabBarDelegate: UITabBarControllerDelegate? 54 | } 55 | 56 | extension UIViewController { 57 | /// AssociatedInstance reference. 58 | fileprivate var associatedInstance: AssociatedInstance { 59 | get { 60 | return AssociatedObject.get(base: self, key: &AssociatedInstanceKey) { 61 | return AssociatedInstance(modalTransitionType: .auto, 62 | navigationTransitionType: .auto, 63 | tabBarTransitionType: .auto, 64 | storedSnapshot: nil, 65 | previousNavigationDelegate: nil, 66 | previousTabBarDelegate: nil) 67 | } 68 | } 69 | set(value) { 70 | AssociatedObject.set(base: self, key: &AssociatedInstanceKey, value: value) 71 | } 72 | } 73 | 74 | /// Default motion animation type for presenting & dismissing modally. 75 | public var motionTransitionType: MotionTransitionAnimationType { 76 | get { 77 | return associatedInstance.modalTransitionType 78 | } 79 | set(value) { 80 | associatedInstance.modalTransitionType = value 81 | } 82 | } 83 | 84 | /// Used for .overFullScreen presentation. 85 | internal var motionStoredSnapshot: UIView? { 86 | get { 87 | return associatedInstance.storedSnapshot 88 | } 89 | set(value) { 90 | associatedInstance.storedSnapshot = value 91 | } 92 | } 93 | 94 | /** 95 | A reference to the previous navigation controller delegate 96 | before Motion was enabled. 97 | */ 98 | internal var previousNavigationDelegate: UINavigationControllerDelegate? { 99 | get { 100 | return associatedInstance.previousNavigationDelegate 101 | } 102 | set(value) { 103 | associatedInstance.previousNavigationDelegate = value 104 | } 105 | } 106 | 107 | /** 108 | A reference to the previous tab bar controller delegate 109 | before Motion was enabled. 110 | */ 111 | internal var previousTabBarDelegate: UITabBarControllerDelegate? { 112 | get { 113 | return associatedInstance.previousTabBarDelegate 114 | } 115 | set(value) { 116 | associatedInstance.previousTabBarDelegate = value 117 | } 118 | } 119 | 120 | ///A boolean that indicates whether Motion is enabled or disabled. 121 | @IBInspectable 122 | public var isMotionEnabled: Bool { 123 | get { 124 | return transitioningDelegate is MotionTransition 125 | } 126 | set(value) { 127 | guard value != isMotionEnabled else { 128 | return 129 | } 130 | 131 | if value { 132 | transitioningDelegate = MotionTransition.shared 133 | 134 | if let v = self as? UINavigationController { 135 | previousNavigationDelegate = v.delegate 136 | v.delegate = MotionTransition.shared 137 | } 138 | 139 | if let v = self as? UITabBarController { 140 | previousTabBarDelegate = v.delegate 141 | v.delegate = MotionTransition.shared 142 | } 143 | 144 | } else { 145 | transitioningDelegate = nil 146 | 147 | if let v = self as? UINavigationController, v.delegate is MotionTransition { 148 | v.delegate = previousNavigationDelegate 149 | } 150 | 151 | if let v = self as? UITabBarController, v.delegate is MotionTransition { 152 | v.delegate = previousTabBarDelegate 153 | } 154 | } 155 | } 156 | } 157 | } 158 | 159 | extension UINavigationController { 160 | /// Default motion animation type for push and pop within the navigation controller. 161 | public var motionNavigationTransitionType: MotionTransitionAnimationType { 162 | get { 163 | return associatedInstance.navigationTransitionType 164 | } 165 | set(value) { 166 | associatedInstance.navigationTransitionType = value 167 | } 168 | } 169 | } 170 | 171 | extension UITabBarController { 172 | /// Default motion animation type for switching tabs within the tab bar controller. 173 | public var motionTabBarTransitionType: MotionTransitionAnimationType { 174 | get { 175 | return associatedInstance.tabBarTransitionType 176 | } 177 | set(value) { 178 | associatedInstance.tabBarTransitionType = value 179 | } 180 | } 181 | } 182 | 183 | extension UIViewController { 184 | /** 185 | Dismiss the current view controller with animation. Will perform a 186 | navigationController.popViewController if the current view controller 187 | is contained inside a navigationController 188 | */ 189 | @IBAction 190 | public func motionDismissViewController() { 191 | if let v = navigationController, self != v.viewControllers.first { 192 | v.popViewController(animated: true) 193 | } else { 194 | dismiss(animated: true) 195 | } 196 | } 197 | 198 | /// Unwind to the root view controller using Motion. 199 | @IBAction 200 | public func motionUnwindToRootViewController() { 201 | motionUnwindToViewController { 202 | nil == $0.presentingViewController 203 | } 204 | } 205 | 206 | /// Unwind to a specific view controller using Motion. 207 | public func motionUnwindToViewController(_ toViewController: UIViewController) { 208 | motionUnwindToViewController { 209 | $0 == toViewController 210 | } 211 | } 212 | 213 | /// Unwind to a view controller that responds to the given selector using Motion. 214 | public func motionUnwindToViewController(withSelector: Selector) { 215 | motionUnwindToViewController { 216 | $0.responds(to: withSelector) 217 | } 218 | } 219 | 220 | /// Unwind to a view controller with given class using Motion 221 | public func motionUnwindToViewController(withClass: AnyClass) { 222 | motionUnwindToViewController { 223 | $0.isKind(of: withClass) 224 | } 225 | } 226 | 227 | /// Unwind to a view controller that the matchBlock returns true on. 228 | public func motionUnwindToViewController(withMatchBlock: (UIViewController) -> Bool) { 229 | var target: UIViewController? = nil 230 | var current: UIViewController? = self 231 | 232 | while nil == target && nil != current { 233 | if let childViewControllers = (current as? UINavigationController)?.children ?? current!.navigationController?.children { 234 | 235 | for vc in childViewControllers.reversed() { 236 | if self != vc, withMatchBlock(vc) { 237 | target = vc 238 | break 239 | } 240 | } 241 | } 242 | 243 | if nil == target { 244 | current = current!.presentingViewController 245 | 246 | if let vc = current, true == withMatchBlock(vc) { 247 | target = vc 248 | } 249 | } 250 | } 251 | 252 | guard let t = target else { 253 | return 254 | } 255 | 256 | guard nil != t.presentedViewController else { 257 | _ = t.navigationController?.popToViewController(t, animated: true) 258 | return 259 | } 260 | 261 | _ = t.navigationController?.popToViewController(t, animated: false) 262 | 263 | let fvc = navigationController ?? self 264 | let tvc = t.navigationController ?? t 265 | 266 | if t.presentedViewController != fvc { 267 | // UIKit's UIViewController.dismiss will jump to target.presentedViewController then perform the dismiss. 268 | // We overcome this behavior by inserting a snapshot into target.presentedViewController 269 | // And also force Hero to use the current VC as the fromViewController 270 | MotionTransition.shared.fromViewController = fvc 271 | 272 | let snapshotView = fvc.view.snapshotView(afterScreenUpdates: true)! 273 | let targetSuperview = tvc.presentedViewController!.view! 274 | 275 | if let visualEffectView = targetSuperview as? UIVisualEffectView { 276 | visualEffectView.contentView.addSubview(snapshotView) 277 | } else { 278 | targetSuperview.addSubview(snapshotView) 279 | } 280 | } 281 | 282 | tvc.dismiss(animated: true, completion: nil) 283 | } 284 | 285 | /** 286 | Replace the current view controller with another view controller within the 287 | navigation/modal stack. 288 | - Parameter with next: A UIViewController. 289 | */ 290 | public func motionReplaceViewController(with next: UIViewController) { 291 | let motion = next.transitioningDelegate as? MotionTransition ?? MotionTransition.shared 292 | 293 | guard !motion.isTransitioning else { 294 | print("motionReplaceViewController cancelled because Motion was doing a transition. Use Motion.shared.cancel(animated: false) or Motion.shared.end(animated: false) to stop the transition first before calling motionReplaceViewController.") 295 | return 296 | } 297 | 298 | if let nc = navigationController { 299 | var v = nc.children 300 | 301 | if !v.isEmpty { 302 | v.removeLast() 303 | v.append(next) 304 | } 305 | 306 | if nc.isMotionEnabled { 307 | motion.forceNonInteractive = true 308 | } 309 | 310 | nc.setViewControllers(v, animated: true) 311 | } else if let container = view.superview { 312 | let presentingVC = presentingViewController 313 | 314 | motion.transition(from: self, to: next, in: container) { [weak self] (isFinishing) in 315 | guard isFinishing else { 316 | return 317 | } 318 | 319 | next.view.window?.addSubview(next.view) 320 | 321 | guard let pvc = presentingVC else { 322 | Application.shared.keyWindow?.rootViewController = next 323 | return 324 | } 325 | 326 | self?.dismiss(animated: false) { 327 | pvc.present(next, animated: false) 328 | } 329 | } 330 | } 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /Sources/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 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2019, CosmicMind, Inc. . 4 | All rights reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Sources/Motion.h: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | #import 27 | 28 | //! Project version number for Motion. 29 | FOUNDATION_EXPORT double MotionVersionNumber; 30 | 31 | //! Project version string for Motion. 32 | FOUNDATION_EXPORT const unsigned char MotionVersionString[]; 33 | 34 | // In this header, you should import all the public headers of your framework using statements like #import 35 | -------------------------------------------------------------------------------- /Sources/MotionAnimation.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | public class MotionAnimation { 29 | /// A reference to the callback that applies the MotionAnimationState. 30 | internal let apply: (inout MotionAnimationState) -> Void 31 | 32 | /** 33 | An initializer that accepts a given callback. 34 | - Parameter applyFunction: A given callback. 35 | */ 36 | init(applyFunction: @escaping (inout MotionAnimationState) -> Void) { 37 | apply = applyFunction 38 | } 39 | } 40 | 41 | public extension MotionAnimation { 42 | /** 43 | Animates a view's current background color to the 44 | given color. 45 | - Parameter color: A UIColor. 46 | - Returns: A MotionAnimation. 47 | */ 48 | static func background(color: UIColor) -> MotionAnimation { 49 | return MotionAnimation { 50 | $0.backgroundColor = color.cgColor 51 | } 52 | } 53 | 54 | /** 55 | Animates a view's current border color to the 56 | given color. 57 | - Parameter color: A UIColor. 58 | - Returns: A MotionAnimation. 59 | */ 60 | static func border(color: UIColor) -> MotionAnimation { 61 | return MotionAnimation { 62 | $0.borderColor = color.cgColor 63 | } 64 | } 65 | 66 | /** 67 | Animates a view's current border width to the 68 | given width. 69 | - Parameter width: A CGFloat. 70 | - Returns: A MotionAnimation. 71 | */ 72 | static func border(width: CGFloat) -> MotionAnimation { 73 | return MotionAnimation { 74 | $0.borderWidth = width 75 | } 76 | } 77 | 78 | /** 79 | Animates a view's current corner radius to the 80 | given radius. 81 | - Parameter radius: A CGFloat. 82 | - Returns: A MotionAnimation. 83 | */ 84 | static func corner(radius: CGFloat) -> MotionAnimation { 85 | return MotionAnimation { 86 | $0.cornerRadius = radius 87 | } 88 | } 89 | 90 | /** 91 | Animates a view's current transform (perspective, scale, rotate) 92 | to the given one. 93 | - Parameter _ transform: A CATransform3D. 94 | - Returns: A MotionAnimation. 95 | */ 96 | static func transform(_ transform: CATransform3D) -> MotionAnimation { 97 | return MotionAnimation { 98 | $0.transform = transform 99 | } 100 | } 101 | 102 | /** 103 | Animates a view's current rotation to the given x, y, 104 | and z values. 105 | - Parameter x: A CGFloat. 106 | - Parameter y: A CGFloat. 107 | - Parameter z: A CGFloat. 108 | - Returns: A MotionAnimation. 109 | */ 110 | static func rotate(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) -> MotionAnimation { 111 | return MotionAnimation { 112 | var t = $0.transform ?? CATransform3DIdentity 113 | t = CATransform3DRotate(t, CGFloat(Double.pi) * x / 180, 1, 0, 0) 114 | t = CATransform3DRotate(t, CGFloat(Double.pi) * y / 180, 0, 1, 0) 115 | $0.transform = CATransform3DRotate(t, CGFloat(Double.pi) * z / 180, 0, 0, 1) 116 | } 117 | } 118 | 119 | /** 120 | Animates a view's current rotation to the given point. 121 | - Parameter _ point: A CGPoint. 122 | - Parameter z: A CGFloat. 123 | - Returns: A MotionAnimation. 124 | */ 125 | static func rotate(_ point: CGPoint, z: CGFloat = 0) -> MotionAnimation { 126 | return .rotate(x: point.x, y: point.y, z: z) 127 | } 128 | 129 | /** 130 | Rotate 2d. 131 | - Parameter _ z: A CGFloat. 132 | - Returns: A MotionAnimation. 133 | */ 134 | static func rotate(_ z: CGFloat) -> MotionAnimation { 135 | return .rotate(z: z) 136 | } 137 | 138 | /** 139 | Animates a view's current spin to the given x, y, 140 | and z values. 141 | - Parameter x: A CGFloat. 142 | - Parameter y: A CGFloat. 143 | - Parameter z: A CGFloat. 144 | - Returns: A MotionAnimation. 145 | */ 146 | static func spin(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) -> MotionAnimation { 147 | return MotionAnimation { 148 | $0.spin = (x, y, z) 149 | } 150 | } 151 | 152 | /** 153 | Animates a view's current spin to the given point. 154 | - Parameter _ point: A CGPoint. 155 | - Parameter z: A CGFloat. 156 | - Returns: A MotionAnimation. 157 | */ 158 | static func spin(_ point: CGPoint, z: CGFloat = 0) -> MotionAnimation { 159 | return .spin(x: point.x, y: point.y, z: z) 160 | } 161 | 162 | /** 163 | Spin 2d. 164 | - Parameter _ z: A CGFloat. 165 | - Returns: A MotionAnimation. 166 | */ 167 | static func spin(_ z: CGFloat) -> MotionAnimation { 168 | return .spin(z: z) 169 | } 170 | 171 | /** 172 | Animates the view's current scale to the given x, y, and z scale values. 173 | - Parameter x: A CGFloat. 174 | - Parameter y: A CGFloat. 175 | - Parameter z: A CGFloat. 176 | - Returns: A MotionAnimation. 177 | */ 178 | static func scale(x: CGFloat = 1, y: CGFloat = 1, z: CGFloat = 1) -> MotionAnimation { 179 | return MotionAnimation { 180 | $0.transform = CATransform3DScale($0.transform ?? CATransform3DIdentity, x, y, z) 181 | } 182 | } 183 | 184 | /** 185 | Animates the view's current x & y scale to the given scale value. 186 | - Parameter _ xy: A CGFloat. 187 | - Returns: A MotionAnimation. 188 | */ 189 | static func scale(_ xy: CGFloat) -> MotionAnimation { 190 | return .scale(x: xy, y: xy) 191 | } 192 | 193 | /** 194 | Animates a view equal to the distance given by the x, y, and z values. 195 | - Parameter x: A CGFloat. 196 | - Parameter y: A CGFloat. 197 | - Parameter z: A CGFloat. 198 | - Returns: A MotionAnimation. 199 | */ 200 | static func translate(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) -> MotionAnimation { 201 | return MotionAnimation { 202 | $0.transform = CATransform3DTranslate($0.transform ?? CATransform3DIdentity, x, y, z) 203 | } 204 | } 205 | 206 | /** 207 | Animates a view equal to the distance given by a point and z value. 208 | - Parameter _ point: A CGPoint. 209 | - Parameter z: A CGFloat. 210 | - Returns: A MotionAnimation. 211 | */ 212 | static func translate(_ point: CGPoint, z: CGFloat = 0) -> MotionAnimation { 213 | return .translate(x: point.x, y: point.y, z: z) 214 | } 215 | 216 | /** 217 | Animates a view's current position to the given point. 218 | - Parameter _ point: A CGPoint. 219 | - Returns: A MotionAnimation. 220 | */ 221 | static func position(_ point: CGPoint) -> MotionAnimation { 222 | return MotionAnimation { 223 | $0.position = point 224 | } 225 | } 226 | 227 | /** 228 | Animates a view's current position to the given x and y values. 229 | - Parameter x: A CGloat. 230 | - Parameter y: A CGloat. 231 | - Returns: A MotionAnimation. 232 | */ 233 | static func position(x: CGFloat, y: CGFloat) -> MotionAnimation { 234 | return .position(CGPoint(x: x, y: y)) 235 | } 236 | 237 | /// Fades the view in during an animation. 238 | static var fadeIn = MotionAnimation.fade(1) 239 | 240 | /// Fades the view out during an animation. 241 | static var fadeOut = MotionAnimation.fade(0) 242 | 243 | /** 244 | Animates a view's current opacity to the given one. 245 | - Parameter _ opacity: A Double. 246 | - Returns: A MotionAnimation. 247 | */ 248 | static func fade(_ opacity: Double) -> MotionAnimation { 249 | return MotionAnimation { 250 | $0.opacity = opacity 251 | } 252 | } 253 | 254 | /** 255 | Animates a view's current zPosition to the given position. 256 | - Parameter _ position: An Int. 257 | - Returns: A MotionAnimation. 258 | */ 259 | static func zPosition(_ position: CGFloat) -> MotionAnimation { 260 | return MotionAnimation { 261 | $0.zPosition = position 262 | } 263 | } 264 | 265 | /** 266 | Animates a view's current size to the given one. 267 | - Parameter _ size: A CGSize. 268 | - Returns: A MotionAnimation. 269 | */ 270 | static func size(_ size: CGSize) -> MotionAnimation { 271 | return MotionAnimation { 272 | $0.size = size 273 | } 274 | } 275 | 276 | /** 277 | Animates the view's current size to the given width and height. 278 | - Parameter width: A CGFloat. 279 | - Parameter height: A CGFloat. 280 | - Returns: A MotionAnimation. 281 | */ 282 | static func size(width: CGFloat, height: CGFloat) -> MotionAnimation { 283 | return .size(CGSize(width: width, height: height)) 284 | } 285 | 286 | /** 287 | Animates a view's current shadow path to the given one. 288 | - Parameter path: A CGPath. 289 | - Returns: A MotionAnimation. 290 | */ 291 | static func shadow(path: CGPath) -> MotionAnimation { 292 | return MotionAnimation { 293 | $0.shadowPath = path 294 | } 295 | } 296 | 297 | /** 298 | Animates a view's current shadow color to the given one. 299 | - Parameter color: A UIColor. 300 | - Returns: A MotionAnimation. 301 | */ 302 | static func shadow(color: UIColor) -> MotionAnimation { 303 | return MotionAnimation { 304 | $0.shadowColor = color.cgColor 305 | } 306 | } 307 | 308 | /** 309 | Animates a view's current shadow offset to the given one. 310 | - Parameter offset: A CGSize. 311 | - Returns: A MotionAnimation. 312 | */ 313 | static func shadow(offset: CGSize) -> MotionAnimation { 314 | return MotionAnimation { 315 | $0.shadowOffset = offset 316 | } 317 | } 318 | 319 | /** 320 | Animates a view's current shadow opacity to the given one. 321 | - Parameter opacity: A Float. 322 | - Returns: A MotionAnimation. 323 | */ 324 | static func shadow(opacity: Float) -> MotionAnimation { 325 | return MotionAnimation { 326 | $0.shadowOpacity = opacity 327 | } 328 | } 329 | 330 | /** 331 | Animates a view's current shadow radius to the given one. 332 | - Parameter radius: A CGFloat. 333 | - Returns: A MotionAnimation. 334 | */ 335 | static func shadow(radius: CGFloat) -> MotionAnimation { 336 | return MotionAnimation { 337 | $0.shadowRadius = radius 338 | } 339 | } 340 | 341 | /** 342 | Animates the views shadow offset, opacity, and radius. 343 | - Parameter offset: A CGSize. 344 | - Parameter opacity: A Float. 345 | - Parameter radius: A CGFloat. 346 | */ 347 | static func depth(offset: CGSize, opacity: Float, radius: CGFloat) -> MotionAnimation { 348 | return MotionAnimation { 349 | $0.shadowOffset = offset 350 | $0.shadowOpacity = opacity 351 | $0.shadowRadius = radius 352 | } 353 | } 354 | 355 | /** 356 | Animates the views shadow offset, opacity, and radius. 357 | - Parameter _ depth: A tuple (CGSize, FLoat, CGFloat). 358 | */ 359 | static func depth(_ depth: (CGSize, Float, CGFloat)) -> MotionAnimation { 360 | return .depth(offset: depth.0, opacity: depth.1, radius: depth.2) 361 | } 362 | 363 | /** 364 | Available in iOS 9+, animates a view using the spring API, 365 | given a stiffness and damping. 366 | - Parameter stiffness: A CGFlloat. 367 | - Parameter damping: A CGFloat. 368 | - Returns: A MotionAnimation. 369 | */ 370 | @available(iOS 9, *) 371 | static func spring(stiffness: CGFloat, damping: CGFloat) -> MotionAnimation { 372 | return MotionAnimation { 373 | $0.spring = (stiffness, damping) 374 | } 375 | } 376 | 377 | /** 378 | The duration of the view's animation. If a duration of 0 is used, 379 | the value will be converted to 0.01, to give a close to zero value. 380 | - Parameter _ duration: A TimeInterval. 381 | - Returns: A MotionAnimation. 382 | */ 383 | static func duration(_ duration: TimeInterval) -> MotionAnimation { 384 | return MotionAnimation { 385 | $0.duration = duration 386 | } 387 | } 388 | 389 | /** 390 | Delays the animation of a given view. 391 | - Parameter _ time: TimeInterval. 392 | - Returns: A MotionAnimation. 393 | */ 394 | static func delay(_ time: TimeInterval) -> MotionAnimation { 395 | return MotionAnimation { 396 | $0.delay = time 397 | } 398 | } 399 | 400 | /** 401 | Sets the view's timing function for the animation. 402 | - Parameter _ timingFunction: A CAMediaTimingFunction. 403 | - Returns: A MotionAnimation. 404 | */ 405 | static func timingFunction(_ timingFunction: CAMediaTimingFunction) -> MotionAnimation { 406 | return MotionAnimation { 407 | $0.timingFunction = timingFunction 408 | } 409 | } 410 | 411 | /** 412 | Creates a completion block handler that is executed once the animation 413 | is done. 414 | - Parameter _ execute: A callback to execute once completed. 415 | */ 416 | static func completion(_ execute: @escaping () -> Void) -> MotionAnimation { 417 | return MotionAnimation { 418 | $0.completion = execute 419 | } 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /Sources/MotionAnimationState.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | public struct MotionAnimationState { 29 | /// A reference to the position. 30 | public var position: CGPoint? 31 | 32 | /// A reference to the size. 33 | public var size: CGSize? 34 | 35 | /// A reference to the transform. 36 | public var transform: CATransform3D? 37 | 38 | /// A reference to the spin tuple. 39 | public var spin: (x: CGFloat, y: CGFloat, z: CGFloat)? 40 | 41 | /// A reference to the opacity. 42 | public var opacity: Double? 43 | 44 | /// A reference to the cornerRadius. 45 | public var cornerRadius: CGFloat? 46 | 47 | /// A reference to the backgroundColor. 48 | public var backgroundColor: CGColor? 49 | 50 | /// A reference to the zPosition. 51 | public var zPosition: CGFloat? 52 | 53 | /// A reference to the borderWidth. 54 | public var borderWidth: CGFloat? 55 | 56 | /// A reference to the borderColor. 57 | public var borderColor: CGColor? 58 | 59 | /// A reference to the shadowColor. 60 | public var shadowColor: CGColor? 61 | 62 | /// A reference to the shadowOpacity. 63 | public var shadowOpacity: Float? 64 | 65 | /// A reference to the shadowOffset. 66 | public var shadowOffset: CGSize? 67 | 68 | /// A reference to the shadowRadius. 69 | public var shadowRadius: CGFloat? 70 | 71 | /// A reference to the shadowPath. 72 | public var shadowPath: CGPath? 73 | 74 | /// A reference to the spring animation settings. 75 | public var spring: (CGFloat, CGFloat)? 76 | 77 | /// A time delay on starting the animation. 78 | public var delay: TimeInterval = 0 79 | 80 | /// The duration of the animation. 81 | public var duration: TimeInterval = 0.35 82 | 83 | /// The timing function value of the animation. 84 | public var timingFunction = CAMediaTimingFunction.easeInOut 85 | 86 | /// Custom target states. 87 | public var custom: [String: Any]? 88 | 89 | /// Completion block. 90 | public var completion: (() -> Void)? 91 | 92 | /** 93 | An initializer that accepts an Array of MotionAnimations. 94 | - Parameter animations: An Array of MotionAnimations. 95 | */ 96 | init(animations: [MotionAnimation]) { 97 | append(contentsOf: animations) 98 | } 99 | } 100 | 101 | extension MotionAnimationState { 102 | /** 103 | Adds a MotionAnimation to the current state. 104 | - Parameter _ element: A MotionAnimation. 105 | */ 106 | public mutating func append(_ element: MotionAnimation) { 107 | element.apply(&self) 108 | } 109 | 110 | /** 111 | Adds an Array of MotionAnimations to the current state. 112 | - Parameter contentsOf elements: An Array of MotionAnimations. 113 | */ 114 | public mutating func append(contentsOf elements: [MotionAnimation]) { 115 | for v in elements { 116 | v.apply(&self) 117 | } 118 | } 119 | 120 | /** 121 | A subscript that returns a custom value for a specified key. 122 | - Parameter key: A String. 123 | - Returns: An optional Any value. 124 | */ 125 | public subscript(key: String) -> Any? { 126 | get { 127 | return custom?[key] 128 | } 129 | set(value) { 130 | if nil == custom { 131 | custom = [:] 132 | } 133 | 134 | custom![key] = value 135 | } 136 | } 137 | } 138 | 139 | extension MotionAnimationState: ExpressibleByArrayLiteral { 140 | /** 141 | An initializer implementing the ExpressibleByArrayLiteral protocol. 142 | - Parameter arrayLiteral elements: A list of MotionAnimations. 143 | */ 144 | public init(arrayLiteral elements: MotionAnimation...) { 145 | append(contentsOf: elements) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Sources/MotionCAAnimation.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | public enum MotionAnimationKeyPath: String { 29 | case backgroundColor 30 | case borderColor 31 | case borderWidth 32 | case cornerRadius 33 | case transform 34 | case rotate = "transform.rotation" 35 | case rotateX = "transform.rotation.x" 36 | case rotateY = "transform.rotation.y" 37 | case rotateZ = "transform.rotation.z" 38 | case scale = "transform.scale" 39 | case scaleX = "transform.scale.x" 40 | case scaleY = "transform.scale.y" 41 | case scaleZ = "transform.scale.z" 42 | case translation = "transform.translation" 43 | case translationX = "transform.translation.x" 44 | case translationY = "transform.translation.y" 45 | case translationZ = "transform.translation.z" 46 | case position 47 | case opacity 48 | case zPosition 49 | case width = "bounds.size.width" 50 | case height = "bounds.size.height" 51 | case size = "bounds.size" 52 | case shadowPath 53 | case shadowColor 54 | case shadowOffset 55 | case shadowOpacity 56 | case shadowRadius 57 | } 58 | 59 | extension CABasicAnimation { 60 | /** 61 | A convenience initializer that takes a given MotionAnimationKeyPath. 62 | - Parameter keyPath: An MotionAnimationKeyPath. 63 | */ 64 | public convenience init(keyPath: MotionAnimationKeyPath) { 65 | self.init(keyPath: keyPath.rawValue) 66 | } 67 | } 68 | 69 | public struct MotionCAAnimation {} 70 | 71 | fileprivate extension MotionCAAnimation { 72 | /** 73 | Creates a CABasicAnimation. 74 | - Parameter keyPath: A MotionAnimationKeyPath. 75 | - Parameter toValue: An Any value that is the end state of the animation. 76 | */ 77 | static func createAnimation(keyPath: MotionAnimationKeyPath, toValue: Any) -> CABasicAnimation { 78 | let a = CABasicAnimation(keyPath: keyPath) 79 | a.toValue = toValue 80 | return a 81 | } 82 | } 83 | 84 | @available(iOS 9.0, *) 85 | internal extension MotionCAAnimation { 86 | /** 87 | Converts a CABasicAnimation to a CASpringAnimation. 88 | - Parameter animation: A CABasicAnimation. 89 | - Parameter stiffness: A CGFloat. 90 | - Parameter damping: A CGFloat. 91 | */ 92 | static func convert(animation: CABasicAnimation, stiffness: CGFloat, damping: CGFloat) -> CASpringAnimation { 93 | let a = CASpringAnimation(keyPath: animation.keyPath) 94 | a.fromValue = animation.fromValue 95 | a.toValue = animation.toValue 96 | a.stiffness = stiffness 97 | a.damping = damping 98 | return a 99 | } 100 | } 101 | 102 | public extension MotionCAAnimation { 103 | /** 104 | Creates a CABasicAnimation for the backgroundColor key path. 105 | - Parameter color: A UIColor. 106 | - Returns: A CABasicAnimation. 107 | */ 108 | static func background(color: UIColor) -> CABasicAnimation { 109 | return createAnimation(keyPath: .backgroundColor, toValue: color.cgColor) 110 | } 111 | 112 | /** 113 | Creates a CABasicAnimation for the borderColor key path. 114 | - Parameter color: A UIColor. 115 | - Returns: A CABasicAnimation. 116 | */ 117 | static func border(color: UIColor) -> CABasicAnimation { 118 | return createAnimation(keyPath: .borderColor, toValue: color.cgColor) 119 | } 120 | 121 | /** 122 | Creates a CABasicAnimation for the borderWidth key path. 123 | - Parameter width: A CGFloat. 124 | - Returns: A CABasicAnimation. 125 | */ 126 | static func border(width: CGFloat) -> CABasicAnimation { 127 | return createAnimation(keyPath: .borderWidth, toValue: NSNumber(floatLiteral: Double(width))) 128 | } 129 | 130 | /** 131 | Creates a CABasicAnimation for the cornerRadius key path. 132 | - Parameter radius: A CGFloat. 133 | - Returns: A CABasicAnimation. 134 | */ 135 | static func corner(radius: CGFloat) -> CABasicAnimation { 136 | return createAnimation(keyPath: .cornerRadius, toValue: NSNumber(floatLiteral: Double(radius))) 137 | } 138 | 139 | /** 140 | Creates a CABasicAnimation for the transform key path. 141 | - Parameter _ t: A CATransform3D object. 142 | - Returns: A CABasicAnimation. 143 | */ 144 | static func transform(_ t: CATransform3D) -> CABasicAnimation { 145 | return createAnimation(keyPath: .transform, toValue: NSValue(caTransform3D: t)) 146 | } 147 | 148 | /** 149 | Creates a CABasicAnimation for the transform.scale key path. 150 | - Parameter xyz: A CGFloat. 151 | - Returns: A CABasicAnimation. 152 | */ 153 | static func scale(_ xyz: CGFloat) -> CABasicAnimation { 154 | let a = CABasicAnimation(keyPath: .scale) 155 | a.toValue = NSNumber(value: Double(xyz)) 156 | return a 157 | } 158 | 159 | /** 160 | Creates a CABasicAnimation for the transform.scale.x key path. 161 | - Parameter x: A CGFloat. 162 | - Returns: A CABasicAnimation. 163 | */ 164 | static func scale(x: CGFloat) -> CABasicAnimation { 165 | let a = CABasicAnimation(keyPath: .scaleX) 166 | a.toValue = NSNumber(value: Double(x)) 167 | return a 168 | } 169 | 170 | /** 171 | Creates a CABasicAnimation for the transform.scale.y key path. 172 | - Parameter y: A CGFloat. 173 | - Returns: A CABasicAnimation. 174 | */ 175 | static func scale(y: CGFloat) -> CABasicAnimation { 176 | let a = CABasicAnimation(keyPath: .scaleY) 177 | a.toValue = NSNumber(value: Double(y)) 178 | return a 179 | } 180 | 181 | /** 182 | Creates a CABasicAnimation for the transform.scale.z key path. 183 | - Parameter z: A CGFloat. 184 | - Returns: A CABasicAnimation. 185 | */ 186 | static func scale(z: CGFloat) -> CABasicAnimation { 187 | let a = CABasicAnimation(keyPath: .scaleZ) 188 | a.toValue = NSNumber(value: Double(z)) 189 | return a 190 | } 191 | 192 | /** 193 | Creates a CABasicAnimation for the transform.rotate.x key path. 194 | - Parameter x: An optional CGFloat. 195 | - Returns: A CABasicAnimation. 196 | */ 197 | static func spin(x: CGFloat) -> CABasicAnimation { 198 | return createAnimation(keyPath: .rotateX, toValue: NSNumber(value: Double(CGFloat(Double.pi) * 2 * x))) 199 | } 200 | 201 | /** 202 | Creates a CABasicAnimation for the transform.rotate.y key path. 203 | - Parameter y: An optional CGFloat. 204 | - Returns: A CABasicAnimation. 205 | */ 206 | static func spin(y: CGFloat) -> CABasicAnimation { 207 | return createAnimation(keyPath: .rotateY, toValue: NSNumber(value: Double(CGFloat(Double.pi) * 2 * y))) 208 | } 209 | 210 | /** 211 | Creates a CABasicAnimation for the transform.rotate.z key path. 212 | - Parameter z: An optional CGFloat. 213 | - Returns: A CABasicAnimation. 214 | */ 215 | static func spin(z: CGFloat) -> CABasicAnimation { 216 | return createAnimation(keyPath: .rotateZ, toValue: NSNumber(value: Double(CGFloat(Double.pi) * 2 * z))) 217 | } 218 | 219 | /** 220 | Creates a CABasicAnimation for the position key path. 221 | - Parameter _ point: A CGPoint. 222 | - Returns: A CABasicAnimation. 223 | */ 224 | static func position(_ point: CGPoint) -> CABasicAnimation { 225 | return createAnimation(keyPath: .position, toValue: NSValue(cgPoint: point)) 226 | } 227 | 228 | /** 229 | Creates a CABasicAnimation for the opacity key path. 230 | - Parameter _ opacity: A Double. 231 | - Returns: A CABasicAnimation. 232 | */ 233 | static func fade(_ opacity: Double) -> CABasicAnimation { 234 | return createAnimation(keyPath: .opacity, toValue: NSNumber(floatLiteral: opacity)) 235 | } 236 | 237 | /** 238 | Creates a CABasicaAnimation for the zPosition key path. 239 | - Parameter _ position: A CGFloat. 240 | - Returns: A CABasicAnimation. 241 | */ 242 | static func zPosition(_ position: CGFloat) -> CABasicAnimation { 243 | return createAnimation(keyPath: .zPosition, toValue: NSNumber(value: Double(position))) 244 | } 245 | 246 | /** 247 | Creates a CABasicaAnimation for the width key path. 248 | - Parameter width: A CGFloat. 249 | - Returns: A CABasicAnimation. 250 | */ 251 | static func width(_ width: CGFloat) -> CABasicAnimation { 252 | return createAnimation(keyPath: .width, toValue: NSNumber(floatLiteral: Double(width))) 253 | } 254 | 255 | /** 256 | Creates a CABasicaAnimation for the height key path. 257 | - Parameter height: A CGFloat. 258 | - Returns: A CABasicAnimation. 259 | */ 260 | static func height(_ height: CGFloat) -> CABasicAnimation { 261 | return createAnimation(keyPath: .height, toValue: NSNumber(floatLiteral: Double(height))) 262 | } 263 | 264 | /** 265 | Creates a CABasicaAnimation for the height key path. 266 | - Parameter size: A CGSize. 267 | - Returns: A CABasicAnimation. 268 | */ 269 | static func size(_ size: CGSize) -> CABasicAnimation { 270 | return createAnimation(keyPath: .size, toValue: NSValue(cgSize: size)) 271 | } 272 | 273 | /** 274 | Creates a CABasicAnimation for the shadowPath key path. 275 | - Parameter path: A CGPath. 276 | - Returns: A CABasicAnimation. 277 | */ 278 | static func shadow(path: CGPath) -> CABasicAnimation { 279 | return createAnimation(keyPath: .shadowPath, toValue: path) 280 | } 281 | 282 | /** 283 | Creates a CABasicAnimation for the shadowColor key path. 284 | - Parameter color: A UIColor. 285 | - Returns: A CABasicAnimation. 286 | */ 287 | static func shadow(color: UIColor) -> CABasicAnimation { 288 | return createAnimation(keyPath: .shadowColor, toValue: color.cgColor) 289 | } 290 | 291 | /** 292 | Creates a CABasicAnimation for the shadowOffset key path. 293 | - Parameter offset: A CGSize. 294 | - Returns: A CABasicAnimation. 295 | */ 296 | static func shadow(offset: CGSize) -> CABasicAnimation { 297 | return createAnimation(keyPath: .shadowOffset, toValue: NSValue(cgSize: offset)) 298 | } 299 | 300 | /** 301 | Creates a CABasicAnimation for the shadowOpacity key path. 302 | - Parameter opacity: A Float. 303 | - Returns: A CABasicAnimation. 304 | */ 305 | static func shadow(opacity: Float) -> CABasicAnimation { 306 | return createAnimation(keyPath: .shadowOpacity, toValue: NSNumber(floatLiteral: Double(opacity))) 307 | } 308 | 309 | /** 310 | Creates a CABasicAnimation for the shadowRadius key path. 311 | - Parameter radius: A CGFloat. 312 | - Returns: A CABasicAnimation. 313 | */ 314 | static func shadow(radius: CGFloat) -> CABasicAnimation { 315 | return createAnimation(keyPath: .shadowRadius, toValue: NSNumber(floatLiteral: Double(radius))) 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /Sources/MotionCoordinateSpace.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | public enum MotionCoordinateSpace { 29 | case global 30 | case local 31 | } 32 | -------------------------------------------------------------------------------- /Sources/MotionPlugin.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | class MotionPlugin: MotionCorePreprocessor, MotionAnimator { 29 | /** 30 | Determines whether or not to receive `seekTo` callback on every frame. 31 | 32 | Default is false. 33 | 34 | When **requirePerFrameCallback** is **false**, the plugin needs to start its own animations inside `animate` & `resume` 35 | The `seekTo` method is only being called during an interactive transition. 36 | 37 | When **requirePerFrameCallback** is **true**, the plugin will receive `seekTo` callback on every animation frame. Hence it is possible for the plugin to do per-frame animations without implementing `animate` & `resume` 38 | */ 39 | open var requirePerFrameCallback = false 40 | 41 | public override required init() {} 42 | 43 | /** 44 | Called before any animation. 45 | Override this method when you want to preprocess transitions for views 46 | - Parameters: 47 | - context: object holding all parsed and changed transitions, 48 | - fromViews: A flattened list of all views from source ViewController 49 | - toViews: A flattened list of all views from destination ViewController 50 | 51 | To check a view's transitions: 52 | 53 | context[view] 54 | context[view, "transitionName"] 55 | 56 | To set a view's transitions: 57 | 58 | context[view] = [("transition1", ["parameter1"]), ("transition2", [])] 59 | context[view, "transition1"] = ["parameter1", "parameter2"] 60 | 61 | */ 62 | open override func process(fromViews: [UIView], toViews: [UIView]) {} 63 | 64 | /** 65 | - Returns: return true if the plugin can handle animating the view. 66 | - Parameters: 67 | - context: object holding all parsed and changed transitions, 68 | - view: the view to check whether or not the plugin can handle the animation 69 | - isAppearing: true if the view is isAppearing(i.e. a view in destination ViewController) 70 | If return true, Motion won't animate and won't let any other plugins animate this view. 71 | The view will also be hidden automatically during the animation. 72 | */ 73 | open func canAnimate(view: UIView, isAppearing: Bool) -> Bool { return false } 74 | 75 | /** 76 | Perform the animation. 77 | 78 | Note: views in `fromViews` & `toViews` are hidden already. Unhide then if you need to take snapshots. 79 | - Parameters: 80 | - context: object holding all parsed and changed transitions, 81 | - fromViews: A flattened list of all views from source ViewController (filtered by `canAnimate`) 82 | - toViews: A flattened list of all views from destination ViewController (filtered by `canAnimate`) 83 | - Returns: The duration needed to complete the animation 84 | */ 85 | 86 | open func animate(fromViews: [UIView], toViews: [UIView]) -> TimeInterval { return 0 } 87 | 88 | /** 89 | Called when all animations are completed. 90 | 91 | Should perform cleanup and release any reference 92 | */ 93 | open func clean() {} 94 | 95 | /** 96 | For supporting interactive animation only. 97 | 98 | This method is called when an interactive animation is in place 99 | The plugin should pause the animation, and seek to the given progress 100 | - Parameters: 101 | - progress: time of the animation to seek to. 102 | */ 103 | open func seek(to progress: TimeInterval) {} 104 | 105 | /** 106 | For supporting interactive animation only. 107 | 108 | This method is called when an interactive animation is ended 109 | The plugin should resume the animation. 110 | - Parameters: 111 | - progress: will be the same value since last `seekTo` 112 | - reverse: a boolean value indicating whether or not the animation should reverse 113 | */ 114 | open func resume(at progress: TimeInterval, isReversed: Bool) -> TimeInterval { return 0 } 115 | 116 | /** 117 | For supporting interactive animation only. 118 | 119 | This method is called when user wants to override animation transitions during an interactive animation 120 | 121 | - Parameters: 122 | - state: the target state to override 123 | - view: the view to override 124 | */ 125 | open func apply(state: MotionTargetState, to view: UIView) {} 126 | } 127 | 128 | // methods for enable/disable the current plugin 129 | extension MotionPlugin { 130 | /// A boolean indicating whether plugins are available. 131 | public static var isEnabled: Bool { 132 | get { 133 | return MotionTransition.isEnabled(plugin: self) 134 | } 135 | set(value) { 136 | if value { 137 | enable() 138 | } else { 139 | disable() 140 | } 141 | } 142 | } 143 | 144 | /// Enable plugins. 145 | public static func enable() { 146 | MotionTransition.enable(plugin: self) 147 | } 148 | 149 | /// Disable plugins. 150 | public static func disable() { 151 | MotionTransition.disable(plugin: self) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Sources/MotionSnapshotType.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | public enum MotionSnapshotType { 29 | /** 30 | This setting will optimize for different types of views. 31 | For custom views or views with masking, .optimizedDefault might 32 | create snapshots that appear differently than the actual view. 33 | In that case, use .normal or .slowRender to disable the optimization. 34 | */ 35 | case optimized 36 | 37 | /// snapshotView(afterScreenUpdates:) 38 | case normal 39 | 40 | /// layer.render(in: currentContext) 41 | case layerRender 42 | 43 | /** 44 | This setting will not create a snapshot. It will animate the view directly. 45 | This will mess up the view hierarchy, therefore, view controllers have to rebuild 46 | their view structure after the transition finishes. 47 | */ 48 | case noSnapshot 49 | } 50 | -------------------------------------------------------------------------------- /Sources/MotionTargetState.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | public struct MotionTargetState { 29 | /// The identifier value to match source and destination views. 30 | public var motionIdentifier: String? 31 | 32 | /// The initial state that the transition will start at. 33 | internal var beginState: [MotionModifier]? 34 | 35 | public var conditionalModifiers: [((MotionConditionalContext) -> Bool, [MotionModifier])]? 36 | 37 | /// A reference to the position. 38 | public var position: CGPoint? 39 | 40 | /// A reference to the size. 41 | public var size: CGSize? 42 | 43 | /// A reference to the transform. 44 | public var transform: CATransform3D? 45 | 46 | /// A reference to the opacity. 47 | public var opacity: Double? 48 | 49 | /// A reference to the cornerRadius. 50 | public var cornerRadius: CGFloat? 51 | 52 | /// A reference to the backgroundColor. 53 | public var backgroundColor: CGColor? 54 | 55 | /// A reference to the zPosition. 56 | public var zPosition: CGFloat? 57 | 58 | /// A reference to the contentsRect. 59 | public var contentsRect: CGRect? 60 | 61 | /// A reference to the contentsScale. 62 | public var contentsScale: CGFloat? 63 | 64 | /// A reference to the borderWidth. 65 | public var borderWidth: CGFloat? 66 | 67 | /// A reference to the borderColor. 68 | public var borderColor: CGColor? 69 | 70 | /// A reference to the shadowColor. 71 | public var shadowColor: CGColor? 72 | 73 | /// A reference to the shadowOpacity. 74 | public var shadowOpacity: Float? 75 | 76 | /// A reference to the shadowOffset. 77 | public var shadowOffset: CGSize? 78 | 79 | /// A reference to the shadowRadius. 80 | public var shadowRadius: CGFloat? 81 | 82 | /// A reference to the shadowPath. 83 | public var shadowPath: CGPath? 84 | 85 | /// A boolean for the masksToBounds state. 86 | public var masksToBounds: Bool? 87 | 88 | /// A boolean indicating whether to display a shadow or not. 89 | public var displayShadow = true 90 | 91 | /// A reference to the overlay settings. 92 | public var overlay: (color: CGColor, opacity: CGFloat)? 93 | 94 | /// A reference to the spring animation settings. 95 | public var spring: (CGFloat, CGFloat)? 96 | 97 | /// A time delay on starting the animation. 98 | public var delay: TimeInterval = 0 99 | 100 | /// The duration of the animation. 101 | public var duration: TimeInterval? 102 | 103 | /// The timing function value of the animation. 104 | public var timingFunction: CAMediaTimingFunction? 105 | 106 | /// The arc curve value. 107 | public var arc: CGFloat? 108 | 109 | /// The cascading animation settings. 110 | public var cascade: (TimeInterval, CascadeDirection, Bool)? 111 | 112 | /** 113 | A boolean indicating whether to ignore the subview transition 114 | animations or not. 115 | */ 116 | public var ignoreSubviewTransitions: Bool? 117 | 118 | /// The coordinate space to transition views within. 119 | public var coordinateSpace: MotionCoordinateSpace? 120 | 121 | /// Change the size of a view based on a scale factor. 122 | public var useScaleBasedSizeChange: Bool? 123 | 124 | /// The type of snapshot to use. 125 | public var snapshotType: MotionSnapshotType? 126 | 127 | /// Do not fade the view when transitioning. 128 | public var nonFade = false 129 | 130 | /// Force an animation. 131 | public var forceAnimate = false 132 | 133 | /// Custom target states. 134 | public var custom: [String: Any]? 135 | 136 | /** 137 | An initializer that accepts an Array of MotionTargetStates. 138 | - Parameter transitions: An Array of MotionTargetStates. 139 | */ 140 | init(modifiers: [MotionModifier]) { 141 | append(contentsOf: modifiers) 142 | } 143 | } 144 | 145 | extension MotionTargetState { 146 | /** 147 | Adds a MotionTargetState to the current state. 148 | - Parameter _ modifier: A MotionTargetState. 149 | */ 150 | public mutating func append(_ modifier: MotionModifier) { 151 | modifier.apply(&self) 152 | } 153 | 154 | /** 155 | Adds an Array of MotionTargetStates to the current state. 156 | - Parameter contentsOf modifiers: An Array of MotionTargetStates. 157 | */ 158 | public mutating func append(contentsOf modifiers: [MotionModifier]) { 159 | for v in modifiers { 160 | v.apply(&self) 161 | } 162 | } 163 | 164 | /** 165 | A subscript that returns a custom value for a specified key. 166 | - Parameter key: A String. 167 | - Returns: An optional Any value. 168 | */ 169 | public subscript(key: String) -> Any? { 170 | get { 171 | return custom?[key] 172 | } 173 | set(value) { 174 | if nil == custom { 175 | custom = [:] 176 | } 177 | 178 | custom![key] = value 179 | } 180 | } 181 | } 182 | 183 | extension MotionTargetState: ExpressibleByArrayLiteral { 184 | /** 185 | An initializer implementing the ExpressibleByArrayLiteral protocol. 186 | - Parameter arrayLiteral elements: A list of MotionTargetStates. 187 | */ 188 | public init(arrayLiteral elements: MotionModifier...) { 189 | append(contentsOf: elements) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /Sources/MotionTransitionObserver.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import Foundation 27 | 28 | public protocol MotionTargetStateObserver { 29 | /** 30 | Executed when the elapsed time changes during a transition. 31 | - Parameter transitionObserver: A MotionTargetStateObserver. 32 | - Parameter didUpdateWith progress: A TimeInterval. 33 | */ 34 | func motion(transitionObserver: MotionTargetStateObserver, didUpdateWith progress: TimeInterval) 35 | } 36 | -------------------------------------------------------------------------------- /Sources/MotionViewOrderStrategy.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import Foundation 27 | 28 | @objc(MotionViewOrderStrategy) 29 | public enum MotionViewOrderStrategy: Int { 30 | case auto 31 | case sourceViewOnTop 32 | case destinationViewOnTop 33 | } 34 | 35 | -------------------------------------------------------------------------------- /Sources/MotionViewTransition.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | private var MotionViewTransitionKey: UInt8 = 0 29 | 30 | public extension UIView { 31 | /// The MotionViewTransition instance associated with the view. 32 | var motionViewTransition: MotionViewTransition { 33 | get { 34 | return AssociatedObject.get(base: self, key: &MotionViewTransitionKey) { 35 | MotionViewTransition(self) 36 | } 37 | } 38 | } 39 | } 40 | 41 | open class MotionViewTransition { 42 | /// A MotionViewTransitionAnimator used during a transition. 43 | private let animator = MotionViewTransitionAnimator() 44 | 45 | /// A UIView whose subviews and itself will be animating during a transition. 46 | private weak var container: UIView? 47 | 48 | /// Maximum duration of the animations. 49 | open private(set) var totalDuration: TimeInterval = 0 50 | 51 | /// Progress of the current transition. 52 | open private(set) var progress: CGFloat = 0 53 | 54 | /// A Boolean to control if the models should be updated when animations end. 55 | open var shouldUpdateModels: Bool = true 56 | 57 | /// Current duration of the animations based on totalDuration and progress. 58 | private var currentDuration: TimeInterval { 59 | return totalDuration * Double(progress) 60 | } 61 | 62 | /** 63 | An initializer that accepts a container transition view. 64 | - Parameter container: A UIView. 65 | */ 66 | fileprivate init(_ container: UIView) { 67 | self.container = container 68 | } 69 | 70 | /// Prepares the transition animations. 71 | open func start() { 72 | guard let v = container else { 73 | return 74 | } 75 | 76 | totalDuration = animator.animate(fromViews: v.flattenedViewHierarchy, toViews: []) 77 | update(0) 78 | } 79 | 80 | /** 81 | Updates the elapsed time for the transition. 82 | - Parameter progress: the current progress. 83 | */ 84 | open func update(_ progress: CGFloat) { 85 | self.progress = progress.clamp(0, 1) 86 | animator.seek(to: currentDuration) 87 | } 88 | 89 | /** 90 | Cancels the interactive transition by animatin from the current state 91 | to the **beginning** state. 92 | - Parameter isAnimated: A boolean indicating if the completion is animated. 93 | */ 94 | open func cancel(isAnimated: Bool = true) { 95 | end(isAnimated: isAnimated, isReversed: true) 96 | } 97 | 98 | /** 99 | Finishes the interactive transition by animating from the current state 100 | to the **end** state. 101 | - Parameter isAnimated: A Boolean indicating if the completion is animated. 102 | */ 103 | open func finish(isAnimated: Bool = true) { 104 | end(isAnimated: isAnimated, isReversed: false) 105 | } 106 | 107 | deinit { 108 | clean() 109 | } 110 | } 111 | 112 | private extension MotionViewTransition { 113 | /** 114 | Ends the interactive transition by animating from the current state 115 | to the **beginning** or **end** state based on the value of isReversed. 116 | - Parameter isAnimated: A Boolean indicating if the completion is animated. 117 | - Parameter isReversed: A Boolean indicating the direction of completion. 118 | */ 119 | func end(isAnimated: Bool, isReversed: Bool) { 120 | let duration = isAnimated ? currentDuration : isReversed ? 0 : totalDuration 121 | 122 | /// 0.00001 is to make sure that animator adds the animations on resume. 123 | let after = animator.resume(at: abs(duration - 0.00001), isReversed: isReversed) 124 | updateModels() 125 | if isAnimated { 126 | Motion.delay(after, execute: complete) 127 | } else { 128 | complete() 129 | } 130 | } 131 | 132 | /// Finalizes the transition. 133 | func complete() { 134 | removeAnimations() 135 | clean() 136 | } 137 | 138 | /// Resets the transition. 139 | func clean() { 140 | animator.clean() 141 | totalDuration = 0 142 | progress = 0 143 | } 144 | } 145 | 146 | private extension MotionViewTransition { 147 | /// Updates the layers with final values of animations. 148 | func updateModels() { 149 | guard shouldUpdateModels else { 150 | return 151 | } 152 | 153 | walkingThroughAnimations { layer, _, anim in 154 | /// bounds.size somehow is directly set on the layer. 155 | let toValue = anim.keyPath == "bounds.size" ? layer.bounds.size : anim.toValue 156 | layer.setValue(toValue, forKeyPath: anim.keyPath!) 157 | } 158 | } 159 | 160 | /// Removes added animations from layers. 161 | func removeAnimations() { 162 | guard shouldUpdateModels else { 163 | return 164 | } 165 | 166 | walkingThroughAnimations { layer, key, _ in 167 | layer.removeAnimation(forKey: key) 168 | } 169 | } 170 | 171 | /** 172 | Walks through each layer's animation and executes give closure with 173 | CALayer, animation key String, and CABasicAnimation. 174 | - Parameter execute: A closure accepting CALayer, String, and CABasicAnimation 175 | which is called for each layer's animation. 176 | */ 177 | func walkingThroughAnimations(execute: (CALayer, String, CABasicAnimation) -> Void) { 178 | animator.viewToContexts.keys.forEach { v in 179 | v.layer.animations.forEach { key, animation in 180 | if let anim = animation as? CABasicAnimation { 181 | execute(v.layer, key, anim) 182 | } 183 | } 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /Sources/Preprocessors/CascadePreprocessor.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | public enum CascadeDirection { 29 | case topToBottom 30 | case bottomToTop 31 | case leftToRight 32 | case rightToLeft 33 | case radial(center: CGPoint) 34 | case inverseRadial(center: CGPoint) 35 | 36 | /// Based on the cascade direction a comparator is set. 37 | var comparator: (UIView, UIView) -> Bool { 38 | switch self { 39 | case .topToBottom: 40 | return { 41 | return $0.frame.minY < $1.frame.minY 42 | } 43 | 44 | case .bottomToTop: 45 | return { 46 | return $0.frame.maxY == $1.frame.maxY ? $0.frame.maxX > $1.frame.maxX : $0.frame.maxY > $1.frame.maxY 47 | } 48 | 49 | case .leftToRight: 50 | return { 51 | return $0.frame.minX < $1.frame.minX 52 | } 53 | 54 | case .rightToLeft: 55 | return { 56 | return $0.frame.maxX > $1.frame.maxX 57 | } 58 | 59 | case .radial(let center): 60 | return { 61 | return $0.center.distance(center) < $1.center.distance(center) 62 | } 63 | 64 | case .inverseRadial(let center): 65 | return { 66 | return $0.center.distance(center) > $1.center.distance(center) 67 | } 68 | } 69 | } 70 | } 71 | 72 | class CascadePreprocessor: MotionCorePreprocessor { 73 | /** 74 | Processes the transitionary views. 75 | - Parameter fromViews: An Array of UIViews. 76 | - Parameter toViews: An Array of UIViews. 77 | */ 78 | override func process(fromViews: [UIView], toViews: [UIView]) { 79 | process(views: fromViews) 80 | process(views: toViews) 81 | } 82 | 83 | /** 84 | Process an Array of views for the cascade animation. 85 | - Parameter views: An Array of UIViews. 86 | */ 87 | func process(views: [UIView]) { 88 | for v in views { 89 | guard let (deltaTime, direction, delayMatchedViews) = context[v]?.cascade else { 90 | continue 91 | } 92 | 93 | var parentView = v 94 | 95 | if v is UITableView, let wrapperView = v.subviews.get(0) { 96 | parentView = wrapperView 97 | } 98 | 99 | let sortedSubviews = parentView.subviews.sorted(by: direction.comparator) 100 | 101 | let initialDelay = context[v]!.delay 102 | let finalDelay = TimeInterval(sortedSubviews.count) * deltaTime + initialDelay 103 | 104 | for (i, subview) in sortedSubviews.enumerated() { 105 | let delay = TimeInterval(i) * deltaTime + initialDelay 106 | 107 | func applyDelay(view: UIView) { 108 | if nil == context.pairedView(for: view) { 109 | context[view]?.delay = delay 110 | 111 | } else if delayMatchedViews, let paired = context.pairedView(for: view) { 112 | context[view]?.delay = finalDelay 113 | context[paired]?.delay = finalDelay 114 | } 115 | 116 | for subview in view.subviews { 117 | applyDelay(view: subview) 118 | } 119 | } 120 | 121 | applyDelay(view: subview) 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/Preprocessors/ConditionalPreprocessor.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | public struct MotionConditionalContext { 29 | internal weak var motion: MotionTransition! 30 | public weak var view: UIView! 31 | 32 | public private(set) var isAppearing: Bool 33 | 34 | public var isPresenting: Bool { 35 | return motion.isPresenting 36 | } 37 | 38 | public var isTabBarController: Bool { 39 | return motion.isTabBarController 40 | } 41 | 42 | public var isNavigationController: Bool { 43 | return motion.isNavigationController 44 | } 45 | 46 | public var isMatched: Bool { 47 | return nil != matchedView 48 | } 49 | 50 | public var isAncestorViewMatched: Bool { 51 | return nil != matchedAncestorView 52 | } 53 | 54 | public var matchedView: UIView? { 55 | return motion.context.pairedView(for: view) 56 | } 57 | 58 | public var matchedAncestorView: (UIView, UIView)? { 59 | var current = view.superview 60 | 61 | while let ancestor = current, ancestor != motion.context.container { 62 | if let pairedView = motion.context.pairedView(for: ancestor) { 63 | return (ancestor, pairedView) 64 | } 65 | 66 | current = ancestor.superview 67 | } 68 | 69 | return nil 70 | } 71 | 72 | public var fromViewController: UIViewController { 73 | return motion.fromViewController! 74 | } 75 | 76 | public var toViewController: UIViewController { 77 | return motion.toViewController! 78 | } 79 | 80 | public var currentViewController: UIViewController { 81 | return isAppearing ? toViewController : fromViewController 82 | } 83 | 84 | public var otherViewController: UIViewController { 85 | return isAppearing ? fromViewController : toViewController 86 | } 87 | } 88 | 89 | class ConditionalPreprocessor: MotionCorePreprocessor { 90 | override func process(fromViews: [UIView], toViews: [UIView]) { 91 | process(views: fromViews, isAppearing: false) 92 | process(views: toViews, isAppearing: true) 93 | } 94 | 95 | func process(views: [UIView], isAppearing: Bool) { 96 | for v in views { 97 | guard let conditionalModifiers = context[v]?.conditionalModifiers else { 98 | continue 99 | } 100 | 101 | for (condition, modifiers) in conditionalModifiers { 102 | if condition(MotionConditionalContext(motion: motion, view: v, isAppearing: isAppearing)) { 103 | context[v]!.append(contentsOf: modifiers) 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | -------------------------------------------------------------------------------- /Sources/Preprocessors/IgnoreSubviewModifiersPreprocessor.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | class IgnoreSubviewTransitionsPreprocessor: MotionCorePreprocessor { 29 | /** 30 | Processes the transitionary views. 31 | - Parameter fromViews: An Array of UIViews. 32 | - Parameter toViews: An Array of UIViews. 33 | */ 34 | override func process(fromViews: [UIView], toViews: [UIView]) { 35 | process(views: fromViews) 36 | process(views: toViews) 37 | } 38 | 39 | /** 40 | Process an Array of views for the cascade animation. 41 | - Parameter views: An Array of UIViews. 42 | */ 43 | func process(views: [UIView]) { 44 | for v in views { 45 | guard let recursive = context[v]?.ignoreSubviewTransitions else { 46 | continue 47 | } 48 | 49 | var parentView = v 50 | 51 | if v is UITableView, let wrapperView = v.subviews.get(0) { 52 | parentView = wrapperView 53 | } 54 | 55 | guard recursive else { 56 | for subview in parentView.subviews { 57 | context[subview] = nil 58 | } 59 | 60 | continue 61 | } 62 | 63 | cleanSubviewModifiers(for: parentView) 64 | } 65 | } 66 | } 67 | 68 | fileprivate extension IgnoreSubviewTransitionsPreprocessor { 69 | /** 70 | Clears the modifiers for a given view's subviews. 71 | - Parameter for view: A UIView. 72 | */ 73 | func cleanSubviewModifiers(for view: UIView) { 74 | for v in view.subviews { 75 | context[v] = nil 76 | cleanSubviewModifiers(for: v) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/Preprocessors/MatchPreprocessor.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | class MatchPreprocessor: MotionCorePreprocessor { 29 | /** 30 | Processes the transitionary views. 31 | - Parameter fromViews: An Array of UIViews. 32 | - Parameter toViews: An Array of UIViews. 33 | */ 34 | override func process(fromViews: [UIView], toViews: [UIView]) { 35 | for tv in toViews { 36 | guard let motionIdentifier = tv.motionIdentifier, let fv = context.sourceView(for: motionIdentifier) else { 37 | continue 38 | } 39 | 40 | var tvState = context[tv] ?? MotionTargetState() 41 | var fvState = context[fv] ?? MotionTargetState() 42 | 43 | // match is just a two-way source effect 44 | tvState.motionIdentifier = motionIdentifier 45 | fvState.motionIdentifier = motionIdentifier 46 | 47 | fvState.arc = tvState.arc 48 | fvState.duration = tvState.duration 49 | fvState.timingFunction = tvState.timingFunction 50 | fvState.delay = tvState.delay 51 | fvState.spring = tvState.spring 52 | 53 | let forceNonFade = tvState.nonFade || fvState.nonFade 54 | let isNonOpaque = !fv.isOpaque || fv.alpha < 1 || !tv.isOpaque || tv.alpha < 1 55 | 56 | if context.insertToViewFirst { 57 | fvState.opacity = 0 58 | 59 | if !forceNonFade && isNonOpaque { 60 | tvState.opacity = 0 61 | 62 | } else { 63 | tvState.opacity = nil 64 | 65 | if !tv.layer.masksToBounds && tvState.displayShadow { 66 | fvState.displayShadow = false 67 | } 68 | } 69 | 70 | } else { 71 | tvState.opacity = 0 72 | 73 | if !forceNonFade && isNonOpaque { 74 | // cross fade if from/toViews are not opaque 75 | fvState.opacity = 0 76 | } else { 77 | // no cross fade in this case, fromView is always displayed during the transition. 78 | fvState.opacity = nil 79 | 80 | // we dont want two shadows showing up. Therefore we disable toView's shadow when fromView is able to display its shadow 81 | if !fv.layer.masksToBounds && fvState.displayShadow { 82 | tvState.displayShadow = false 83 | } 84 | } 85 | } 86 | 87 | context[tv] = tvState 88 | context[fv] = fvState 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/Preprocessors/MotionCorePreprocessor.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | class MotionCorePreprocessor: MotionPreprocessor { 29 | weak public var motion: MotionTransition! 30 | 31 | /// A reference to the MotionContext. 32 | public var context: MotionContext! { 33 | return motion!.context 34 | } 35 | 36 | func process(fromViews: [UIView], toViews: [UIView]) {} 37 | } 38 | 39 | -------------------------------------------------------------------------------- /Sources/Preprocessors/MotionPreprocessor.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | public protocol MotionPreprocessor: class { 29 | /// A reference to Motion. 30 | var motion: MotionTransition! { get set } 31 | 32 | /** 33 | Processes the transitionary views. 34 | - Parameter fromViews: An Array of UIViews. 35 | - Parameter toViews: An Array of UIViews. 36 | */ 37 | func process(fromViews: [UIView], toViews: [UIView]) 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Preprocessors/SourcePreprocessor.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | class SourcePreprocessor: MotionCorePreprocessor { 29 | /** 30 | Processes the transitionary views. 31 | - Parameter fromViews: An Array of UIViews. 32 | - Parameter toViews: An Array of UIViews. 33 | */ 34 | override func process(fromViews: [UIView], toViews: [UIView]) { 35 | for fv in fromViews { 36 | guard let motionIdentifier = context[fv]?.motionIdentifier, let tv = context.destinationView(for: motionIdentifier) else { 37 | continue 38 | } 39 | 40 | prepare(view: fv, for: tv) 41 | } 42 | 43 | for tv in toViews { 44 | guard let motionIdentifier = context[tv]?.motionIdentifier, let fv = context.sourceView(for: motionIdentifier) else { 45 | continue 46 | } 47 | 48 | prepare(view: tv, for: fv) 49 | } 50 | } 51 | } 52 | 53 | fileprivate extension SourcePreprocessor { 54 | /** 55 | Prepares a given view for a target view. 56 | - Parameter view: A UIView. 57 | - Parameter for targetView: A UIView. 58 | */ 59 | func prepare(view: UIView, for targetView: UIView) { 60 | let targetPos = context.container.convert(targetView.layer.position, from: targetView.superview!) 61 | let targetTransform = context.container.layer.flatTransformTo(layer: targetView.layer) 62 | 63 | var state = context[view]! 64 | 65 | /** 66 | Use global coordinate space since over target position is 67 | converted from the global container 68 | */ 69 | state.coordinateSpace = .global 70 | 71 | state.position = targetPos 72 | state.transform = targetTransform 73 | 74 | // Remove incompatible options. 75 | state.size = nil 76 | 77 | if view.bounds.size != targetView.bounds.size { 78 | state.size = targetView.bounds.size 79 | } 80 | 81 | if view.layer.cornerRadius != targetView.layer.cornerRadius { 82 | state.cornerRadius = targetView.layer.cornerRadius 83 | } 84 | 85 | if view.layer.shadowColor != targetView.layer.shadowColor { 86 | state.shadowColor = targetView.layer.shadowColor 87 | } 88 | 89 | if view.layer.shadowOpacity != targetView.layer.shadowOpacity { 90 | state.shadowOpacity = targetView.layer.shadowOpacity 91 | } 92 | 93 | if view.layer.shadowOffset != targetView.layer.shadowOffset { 94 | state.shadowOffset = targetView.layer.shadowOffset 95 | } 96 | 97 | if view.layer.shadowRadius != targetView.layer.shadowRadius { 98 | state.shadowRadius = targetView.layer.shadowRadius 99 | } 100 | 101 | if view.layer.shadowPath != targetView.layer.shadowPath { 102 | state.shadowPath = targetView.layer.shadowPath 103 | } 104 | 105 | if view.layer.contentsRect != targetView.layer.contentsRect { 106 | state.contentsRect = targetView.layer.contentsRect 107 | } 108 | 109 | if view.layer.contentsScale != targetView.layer.contentsScale { 110 | state.contentsScale = targetView.layer.contentsScale 111 | } 112 | 113 | context[view] = state 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/Transition/MotionProgressRunner.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | protocol MotionProgressRunnerDelegate: class { 29 | func update(progress: TimeInterval) 30 | func complete(isFinishing: Bool) 31 | } 32 | 33 | class MotionProgressRunner { 34 | weak var delegate: MotionProgressRunnerDelegate? 35 | 36 | var isRunning: Bool { 37 | return nil != displayLink 38 | } 39 | 40 | internal var progress: TimeInterval = 0 41 | internal var duration: TimeInterval = 0 42 | internal var displayLink: CADisplayLink? 43 | internal var isReversed = false 44 | 45 | @objc 46 | func displayUpdate(_ link: CADisplayLink) { 47 | progress += isReversed ? -link.duration : link.duration 48 | 49 | if isReversed, progress <= 1.0 / 120 { 50 | delegate?.complete(isFinishing: false) 51 | stop() 52 | return 53 | } 54 | 55 | if !isReversed, progress > duration - 1.0 / 120 { 56 | delegate?.complete(isFinishing: true) 57 | stop() 58 | return 59 | } 60 | 61 | delegate?.update(progress: progress / duration) 62 | } 63 | 64 | func start(progress: TimeInterval, duration: TimeInterval, isReversed: Bool) { 65 | stop() 66 | 67 | self.progress = progress 68 | self.isReversed = isReversed 69 | self.duration = duration 70 | 71 | displayLink = CADisplayLink(target: self, selector: #selector(displayUpdate(_:))) 72 | displayLink!.add(to: RunLoop.main, forMode: RunLoop.Mode(rawValue: RunLoop.Mode.common.rawValue)) 73 | } 74 | 75 | func stop() { 76 | displayLink?.isPaused = true 77 | displayLink?.remove(from: RunLoop.main, forMode: RunLoop.Mode(rawValue: RunLoop.Mode.common.rawValue)) 78 | displayLink = nil 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/Transition/MotionTransition+Animate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | extension MotionTransition { 29 | /// Starts the transition animation. 30 | func animate() { 31 | guard .starting == state else { 32 | return 33 | } 34 | 35 | state = .animating 36 | 37 | if let tv = toView { 38 | context.unhide(view: tv) 39 | } 40 | 41 | for v in animatingFromViews { 42 | context.hide(view: v) 43 | } 44 | 45 | for v in animatingToViews { 46 | context.hide(view: v) 47 | } 48 | 49 | var t: TimeInterval = 0 50 | var animatorWantsInteractive = false 51 | 52 | if context.insertToViewFirst { 53 | for v in animatingToViews { 54 | context.snapshotView(for: v) 55 | } 56 | 57 | for v in animatingFromViews { 58 | context.snapshotView(for: v) 59 | } 60 | 61 | } else { 62 | for v in animatingFromViews { 63 | context.snapshotView(for: v) 64 | } 65 | 66 | for v in animatingToViews { 67 | context.snapshotView(for: v) 68 | } 69 | } 70 | 71 | // UIKit appears to set fromView setNeedLayout to be true. 72 | // We don't want fromView to layout after our animation starts. 73 | // Therefore we kick off the layout beforehand 74 | fromView?.layoutIfNeeded() 75 | for animator in animators { 76 | let duration = animator.animate(fromViews: animatingFromViews.filter { 77 | return animator.canAnimate(view: $0, isAppearing: false) 78 | }, toViews: animatingToViews.filter { 79 | return animator.canAnimate(view: $0, isAppearing: true) 80 | }) 81 | 82 | if .infinity == duration { 83 | animatorWantsInteractive = true 84 | } else { 85 | t = max(t, duration) 86 | } 87 | } 88 | 89 | totalDuration = t 90 | 91 | if let forceFinishing = forceFinishing { 92 | complete(isFinishing: forceFinishing) 93 | } else if let startingProgress = startingProgress { 94 | update(startingProgress) 95 | } else if animatorWantsInteractive { 96 | update(0) 97 | } else { 98 | complete(after: totalDuration, isFinishing: true) 99 | } 100 | 101 | fullScreenSnapshot?.removeFromSuperview() 102 | } 103 | } 104 | 105 | -------------------------------------------------------------------------------- /Sources/Transition/MotionTransition+Complete.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | extension MotionTransition { 29 | /** 30 | Complete the transition. 31 | - Parameter isFinishing: A Boolean indicating if the transition 32 | has completed. 33 | */ 34 | @objc 35 | func complete(isFinishing: Bool) { 36 | if state == .notified { 37 | forceFinishing = isFinishing 38 | } 39 | 40 | guard .animating == state || .starting == state else { 41 | return 42 | } 43 | 44 | defer { 45 | transitionContext = nil 46 | fromViewController = nil 47 | toViewController = nil 48 | transitioningViewController = nil 49 | forceNonInteractive = false 50 | animatingToViews.removeAll() 51 | animatingFromViews.removeAll() 52 | transitionObservers = nil 53 | transitionContainer = nil 54 | completionCallback = nil 55 | forceFinishing = nil 56 | container = nil 57 | startingProgress = nil 58 | preprocessors.removeAll() 59 | animators.removeAll() 60 | plugins.removeAll() 61 | context = nil 62 | progress = 0 63 | totalDuration = 0 64 | state = .possible 65 | defaultAnimation = .auto 66 | containerBackgroundColor = .black 67 | isModalTransition = false 68 | } 69 | 70 | state = .completing 71 | 72 | progressRunner.stop() 73 | context.clean() 74 | 75 | if let tv = toView, let fv = fromView { 76 | if isFinishing && isPresenting && toOverFullScreen { 77 | // finished presenting a overFullScreen view controller. 78 | context.unhide(rootView: tv) 79 | context.removeSnapshots(rootView: tv) 80 | context.storeViewAlpha(rootView: fv) 81 | 82 | fromViewController!.motionStoredSnapshot = container 83 | fv.removeFromSuperview() 84 | fv.addSubview(container) 85 | 86 | } else if !isFinishing && !isPresenting && fromOverFullScreen { 87 | // Cancelled dismissing a overFullScreen view controller. 88 | context.unhide(rootView: fv) 89 | context.removeSnapshots(rootView: fv) 90 | context.storeViewAlpha(rootView: tv) 91 | 92 | toViewController!.motionStoredSnapshot = container 93 | container.superview?.addSubview(tv) 94 | tv.addSubview(container) 95 | 96 | } else { 97 | context.unhideAll() 98 | context.removeAllSnapshots() 99 | } 100 | 101 | // Move fromView & toView back from our container back to the one supplied by UIKit. 102 | if (toOverFullScreen && isFinishing) || (fromOverFullScreen && !isFinishing) { 103 | transitionContainer?.addSubview(isFinishing ? fv : tv) 104 | } 105 | 106 | transitionContainer?.addSubview(isFinishing ? tv : fv) 107 | 108 | if isPresenting != isFinishing, !isContainerController { 109 | // Only happens when present a .overFullScreen view controller. 110 | // bug: http://openradar.appspot.com/radar?id=5320103646199808 111 | Application.shared.keyWindow?.addSubview(isPresenting ? fv : tv) 112 | } 113 | } 114 | 115 | if container.superview == transitionContainer { 116 | container.removeFromSuperview() 117 | } 118 | 119 | for a in animators { 120 | a.clean() 121 | } 122 | 123 | transitionContainer?.isUserInteractionEnabled = true 124 | transitioningViewController?.view.isUserInteractionEnabled = true 125 | 126 | completionCallback?(isFinishing) 127 | 128 | if isFinishing { 129 | toViewController?.tabBarController?.tabBar.layer.removeAllAnimations() 130 | } else { 131 | fromViewController?.tabBarController?.tabBar.layer.removeAllAnimations() 132 | } 133 | 134 | let tContext = transitionContext 135 | let fvc = fromViewController 136 | let tvc = toViewController 137 | 138 | if isFinishing { 139 | processEndTransitionDelegation(transitionContext: tContext, fromViewController: fvc, toViewController: tvc) 140 | } else { 141 | processCancelTransitionDelegation(transitionContext: tContext, fromViewController: fvc, toViewController: tvc) 142 | } 143 | 144 | tContext?.completeTransition(isFinishing) 145 | 146 | let isModalDismissal = isModalTransition && !isPresenting 147 | if isModalDismissal { 148 | Application.shared.fixRootViewY() 149 | } 150 | } 151 | } 152 | 153 | 154 | private extension UIApplication { 155 | /** 156 | When in-call, hotspot, or recording status bar is enabled, just after (custom) modal 157 | dismissal transition animation ends `UITransitionView` is removed from the hierarchy 158 | and that removal was moving `rootViewController.view` 20 points upwards. This function 159 | should be called after transitioningContext.completeTransition(_:) upon modal dismissal 160 | transition. It applies the work that `UITransitionView` should ideally have done after 161 | custom modal dismissal. `UIKit` modal dismissals do not suffer from this. 162 | 163 | Fixes issue-44. See issue-44 for more info. 164 | */ 165 | func fixRootViewY() { 166 | guard statusBarFrame.height == 40, let window = keyWindow, let vc = window.rootViewController else { 167 | return 168 | } 169 | 170 | if vc.view.frame.maxY + 20 == window.frame.height { 171 | vc.view.frame.origin.y += 20 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Sources/Transition/MotionTransition+CustomTransition.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | extension MotionTransition { 29 | /** 30 | A helper transition function. 31 | - Parameter from: A UIViewController. 32 | - Parameter to: A UIViewController. 33 | - Parameter in view: A UIView. 34 | - Parameter completion: An optional completion handler. 35 | */ 36 | public func transition(from: UIViewController, to: UIViewController, in view: UIView, completion: ((Bool) -> Void)? = nil) { 37 | guard !isTransitioning else { 38 | return 39 | } 40 | 41 | state = .notified 42 | isPresenting = true 43 | transitionContainer = view 44 | fromViewController = from 45 | toViewController = to 46 | 47 | setCompletionCallbackForNextTransition(completion) 48 | start() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Transition/MotionTransition+Interactive.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | public extension MotionTransition { 29 | /** 30 | Updates the elapsed time for the interactive transition. 31 | - Parameter progress t: the current progress, must be between -1...1. 32 | */ 33 | func update(_ percentageComplete: TimeInterval) { 34 | guard .animating == state else { 35 | startingProgress = percentageComplete 36 | return 37 | } 38 | 39 | progressRunner.stop() 40 | progress = Double(CGFloat(percentageComplete).clamp(0, 1)) 41 | } 42 | 43 | /** 44 | Finish the interactive transition. 45 | Will stop the interactive transition and animate from the 46 | current state to the **end** state. 47 | - Parameter isAnimated: A Boolean. 48 | */ 49 | func finish(isAnimated: Bool = true) { 50 | guard .animating == state || .notified == state || .starting == state else { 51 | return 52 | } 53 | 54 | guard isAnimated else { 55 | complete(isFinishing: true) 56 | return 57 | } 58 | 59 | var d: TimeInterval = 0 60 | 61 | for a in animators { 62 | d = max(d, a.resume(at: progress * totalDuration, isReversed: false)) 63 | } 64 | 65 | complete(after: d, isFinishing: true) 66 | } 67 | 68 | /** 69 | Cancel the interactive transition. 70 | Will stop the interactive transition and animate from the 71 | current state to the **begining** state 72 | - Parameter isAnimated: A boolean indicating if the completion is animated. 73 | */ 74 | func cancel(isAnimated: Bool = true) { 75 | guard .animating == state || .notified == state || .starting == state else { 76 | return 77 | } 78 | 79 | guard isAnimated else { 80 | complete(isFinishing: false) 81 | return 82 | } 83 | 84 | var d: TimeInterval = 0 85 | 86 | for a in animators { 87 | var t = progress 88 | if t < 0 { 89 | t = -t 90 | } 91 | 92 | d = max(d, a.resume(at: t * totalDuration, isReversed: true)) 93 | } 94 | 95 | complete(after: d, isFinishing: false) 96 | } 97 | 98 | /** 99 | Override transition animations during an interactive animation. 100 | 101 | For example: 102 | 103 | Motion.shared.apply([.position(x:50, y:50)], to: view) 104 | 105 | will set the view's position to 50, 50 106 | - Parameter modifiers: An Array of MotionModifier. 107 | - Parameter to view: A UIView. 108 | */ 109 | func apply(modifiers: [MotionModifier], to view: UIView) { 110 | guard .animating == state else { 111 | return 112 | } 113 | 114 | let targetState = MotionTargetState(modifiers: modifiers) 115 | if let otherView = context.pairedView(for: view) { 116 | for animator in animators { 117 | animator.apply(state: targetState, to: otherView) 118 | } 119 | } 120 | 121 | for animator in self.animators { 122 | animator.apply(state: targetState, to: view) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/Transition/MotionTransition+Start.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | extension MotionTransition { 29 | /// Starts the transition animation. 30 | func start() { 31 | guard .notified == state else { 32 | return 33 | } 34 | 35 | state = .starting 36 | 37 | prepareViewFrame() 38 | prepareViewControllers() 39 | prepareSnapshotView() 40 | preparePlugins() 41 | preparePreprocessors() 42 | prepareAnimators() 43 | 44 | for v in plugins { 45 | preprocessors.append(v) 46 | animators.append(v) 47 | } 48 | 49 | prepareTransitionContainer() 50 | prepareContainer() 51 | prepareContext() 52 | 53 | prepareViewHierarchy() 54 | prepareAnimatingViews() 55 | prepareToView() 56 | 57 | processPreprocessors() 58 | processAnimation() 59 | } 60 | } 61 | 62 | fileprivate extension MotionTransition { 63 | /// Prepares the views frames. 64 | func prepareViewFrame() { 65 | guard let fv = fromView else { 66 | return 67 | } 68 | 69 | guard let tv = toView else { 70 | return 71 | } 72 | 73 | if let toViewController = toViewController, let transitionContext = transitionContext { 74 | tv.frame = transitionContext.finalFrame(for: toViewController) 75 | } else { 76 | tv.frame = fv.frame 77 | } 78 | 79 | tv.setNeedsLayout() 80 | tv.layoutIfNeeded() 81 | } 82 | 83 | /// Prepares the from and to view controllers. 84 | func prepareViewControllers() { 85 | processStartTransitionDelegation(transitionContext: transitionContext, fromViewController: fromViewController, toViewController: toViewController) 86 | } 87 | 88 | /// Prepares the snapshot view, which hides any flashing that may occur. 89 | func prepareSnapshotView() { 90 | fullScreenSnapshot = transitionContainer?.window?.snapshotView(afterScreenUpdates: false) ?? fromView?.snapshotView(afterScreenUpdates: false) 91 | 92 | if let v = fullScreenSnapshot { 93 | (transitionContainer?.window ?? transitionContainer)?.addSubview(v) 94 | } 95 | 96 | if let v = fromViewController?.motionStoredSnapshot { 97 | v.removeFromSuperview() 98 | fromViewController?.motionStoredSnapshot = nil 99 | } 100 | 101 | if let v = toViewController?.motionStoredSnapshot { 102 | v.removeFromSuperview() 103 | toViewController?.motionStoredSnapshot = nil 104 | } 105 | } 106 | 107 | /// Prepares the plugins. 108 | func preparePlugins() { 109 | plugins = MotionTransition.enabledPlugins.map { 110 | return $0.init() 111 | } 112 | } 113 | 114 | /// Prepares the preprocessors. 115 | func preparePreprocessors() { 116 | preprocessors = [IgnoreSubviewTransitionsPreprocessor(), 117 | ConditionalPreprocessor(), 118 | TransitionPreprocessor(), 119 | MatchPreprocessor(), 120 | SourcePreprocessor(), 121 | CascadePreprocessor()] 122 | } 123 | 124 | /// Prepares the animators. 125 | func prepareAnimators() { 126 | animators = [MotionCoreAnimator()] 127 | 128 | if #available(iOS 10, tvOS 10, *) { 129 | animators.append(MotionCoreAnimator()) 130 | } 131 | } 132 | 133 | /// Prepares the transition container. 134 | func prepareTransitionContainer() { 135 | transitioningViewController?.view.isUserInteractionEnabled = isUserInteractionEnabled 136 | transitionContainer?.isUserInteractionEnabled = isUserInteractionEnabled 137 | } 138 | 139 | /// Prepares the view that holds all the animating views. 140 | func prepareContainer() { 141 | container = UIView(frame: transitionContainer?.bounds ?? .zero) 142 | 143 | if !toOverFullScreen && !fromOverFullScreen { 144 | container.backgroundColor = containerBackgroundColor 145 | } 146 | 147 | transitionContainer?.addSubview(container) 148 | } 149 | 150 | /// Prepares the MotionContext instance. 151 | func prepareContext() { 152 | context = MotionContext(container: container) 153 | 154 | for v in preprocessors { 155 | v.motion = self 156 | } 157 | 158 | for v in animators { 159 | v.motion = self 160 | } 161 | 162 | guard let tv = toView else { 163 | return 164 | } 165 | 166 | guard let fv = fromView else { 167 | return 168 | } 169 | 170 | context.loadViewAlpha(rootView: tv) 171 | container.addSubview(tv) 172 | 173 | context.loadViewAlpha(rootView: fv) 174 | container.addSubview(fv) 175 | 176 | tv.setNeedsUpdateConstraints() 177 | tv.updateConstraintsIfNeeded() 178 | tv.setNeedsLayout() 179 | tv.layoutIfNeeded() 180 | 181 | context.set(fromViews: fv.flattenedViewHierarchy, toViews: tv.flattenedViewHierarchy) 182 | } 183 | 184 | /// Prepares the view hierarchy. 185 | func prepareViewHierarchy() { 186 | if (.auto == viewOrderStrategy && !isPresenting && !isTabBarController) || 187 | .sourceViewOnTop == viewOrderStrategy { 188 | context.insertToViewFirst = true 189 | } 190 | 191 | for v in preprocessors { 192 | v.process(fromViews: context.fromViews, toViews: context.toViews) 193 | } 194 | } 195 | 196 | /// Prepares the transition fromView & toView pairs. 197 | func prepareAnimatingViews() { 198 | animatingFromViews = context.fromViews.filter { (view) -> Bool in 199 | for animator in animators { 200 | if animator.canAnimate(view: view, isAppearing: false) { 201 | return true 202 | } 203 | } 204 | return false 205 | } 206 | 207 | animatingToViews = context.toViews.filter { (view) -> Bool in 208 | for animator in animators { 209 | if animator.canAnimate(view: view, isAppearing: true) { 210 | return true 211 | } 212 | } 213 | return false 214 | } 215 | } 216 | 217 | /// Prepares the to view. 218 | func prepareToView() { 219 | guard let tv = toView else { 220 | return 221 | } 222 | 223 | context.hide(view: tv) 224 | } 225 | } 226 | 227 | fileprivate extension MotionTransition { 228 | /// Executes the preprocessors' process function. 229 | func processPreprocessors() { 230 | for v in preprocessors { 231 | v.process(fromViews: context.fromViews, toViews: context.toViews) 232 | } 233 | } 234 | 235 | /// Processes the animations. 236 | func processAnimation() { 237 | #if os(tvOS) 238 | animate() 239 | 240 | #else 241 | if isNavigationController { 242 | // When animating within navigationController, we have to dispatch later into the main queue. 243 | // otherwise snapshots will be pure white. Possibly a bug with UIKit 244 | Motion.async { [weak self] in 245 | self?.animate() 246 | } 247 | 248 | } else { 249 | animate() 250 | } 251 | 252 | #endif 253 | } 254 | } 255 | 256 | -------------------------------------------------------------------------------- /Sources/Transition/MotionTransition+UINavigationControllerDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | extension MotionTransition: UINavigationControllerDelegate { 29 | public func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { 30 | guard !isTransitioning else { 31 | return nil 32 | } 33 | 34 | state = .notified 35 | isPresenting = .push == operation 36 | fromViewController = fromViewController ?? fromVC 37 | toViewController = toViewController ?? toVC 38 | transitioningViewController = navigationController 39 | 40 | return self 41 | } 42 | 43 | public func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 44 | return interactiveTransitioning 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Transition/MotionTransition+UITabBarControllerDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | extension MotionTransition: UITabBarControllerDelegate { 29 | public func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { 30 | 31 | guard false != tabBarController.previousTabBarDelegate?.tabBarController?(tabBarController, shouldSelect: viewController) else { 32 | return false 33 | } 34 | 35 | if isTransitioning { 36 | cancel(isAnimated: false) 37 | } 38 | 39 | return true 40 | } 41 | 42 | public func tabBarController(_ tabBarController: UITabBarController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 43 | return interactiveTransitioning 44 | } 45 | 46 | public func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { 47 | guard !isTransitioning else { 48 | return nil 49 | } 50 | 51 | state = .notified 52 | 53 | let fromVCIndex = tabBarController.children.firstIndex(of: fromVC)! 54 | let toVCIndex = tabBarController.children.firstIndex(of: toVC)! 55 | 56 | isPresenting = toVCIndex > fromVCIndex 57 | fromViewController = fromViewController ?? fromVC 58 | toViewController = toViewController ?? toVC 59 | transitioningViewController = tabBarController 60 | 61 | return self 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /Sources/Transition/MotionTransition+UIViewControllerTransitioningDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (C) 2019, CosmicMind, Inc. . 5 | * All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | import UIKit 27 | 28 | extension MotionTransition: UIViewControllerTransitioningDelegate { 29 | /// A reference to the interactive transitioning instance. 30 | var interactiveTransitioning: UIViewControllerInteractiveTransitioning? { 31 | return forceNonInteractive ? nil : self 32 | } 33 | 34 | public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 35 | guard !isTransitioning else { 36 | return nil 37 | } 38 | 39 | state = .notified 40 | isPresenting = true 41 | isModalTransition = true 42 | fromViewController = fromViewController ?? presenting 43 | toViewController = toViewController ?? presented 44 | 45 | return self 46 | } 47 | 48 | public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 49 | guard !isTransitioning else { 50 | return nil 51 | } 52 | 53 | state = .notified 54 | isPresenting = false 55 | isModalTransition = true 56 | fromViewController = fromViewController ?? dismissed 57 | return self 58 | } 59 | 60 | public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 61 | return interactiveTransitioning 62 | } 63 | 64 | public func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 65 | return interactiveTransitioning 66 | } 67 | } 68 | 69 | extension MotionTransition: UIViewControllerAnimatedTransitioning { 70 | /** 71 | The animation method that is used to coordinate the transition. 72 | - Parameter using transitionContext: A UIViewControllerContextTransitioning. 73 | */ 74 | public func animateTransition(using context: UIViewControllerContextTransitioning) { 75 | transitionContext = context 76 | fromViewController = fromViewController ?? context.viewController(forKey: .from) 77 | toViewController = toViewController ?? context.viewController(forKey: .to) 78 | transitionContainer = context.containerView 79 | 80 | start() 81 | } 82 | 83 | /** 84 | Returns the transition duration time interval. 85 | - Parameter using transitionContext: An optional UIViewControllerContextTransitioning. 86 | - Returns: A TimeInterval that is the total animation time including delays. 87 | */ 88 | public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 89 | return 0 // Time will be updated dynamically. 90 | } 91 | 92 | public func animationEnded(_ transitionCompleted: Bool) { 93 | state = .possible 94 | } 95 | } 96 | 97 | extension MotionTransition: UIViewControllerInteractiveTransitioning { 98 | public var wantsInteractiveStart: Bool { 99 | return true 100 | } 101 | 102 | public func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) { 103 | animateTransition(using: transitionContext) 104 | } 105 | } 106 | --------------------------------------------------------------------------------