Commit fa58ac50 by Daniel Dahan

added Motion 1.1.2

parent 9dd14996
...@@ -10,6 +10,44 @@ ...@@ -10,6 +10,44 @@
961154CC1F32A7B100A78D74 /* Chips.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961154CB1F32A7B100A78D74 /* Chips.swift */; }; 961154CC1F32A7B100A78D74 /* Chips.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961154CB1F32A7B100A78D74 /* Chips.swift */; };
961409B11E43D15C00E7BA99 /* FABMenu.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96A183621E0C6CE200083C30 /* FABMenu.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 961409B11E43D15C00E7BA99 /* FABMenu.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96A183621E0C6CE200083C30 /* FABMenu.swift */; settings = {ATTRIBUTES = (Public, ); }; };
961409B21E43D15C00E7BA99 /* FABMenuController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96A183641E0C6DD400083C30 /* FABMenuController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 961409B21E43D15C00E7BA99 /* FABMenuController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96A183641E0C6DD400083C30 /* FABMenuController.swift */; settings = {ATTRIBUTES = (Public, ); }; };
9615277E1F393C0E00E8B2AC /* LICENSE.md in Sources */ = {isa = PBXBuildFile; fileRef = 961527541F393C0E00E8B2AC /* LICENSE.md */; };
9615277F1F393C0E00E8B2AC /* README.md in Sources */ = {isa = PBXBuildFile; fileRef = 961527551F393C0E00E8B2AC /* README.md */; };
961527801F393C0E00E8B2AC /* MotionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527581F393C0E00E8B2AC /* MotionAnimator.swift */; };
961527811F393C0E00E8B2AC /* MotionAnimatorViewContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527591F393C0E00E8B2AC /* MotionAnimatorViewContext.swift */; };
961527821F393C0E00E8B2AC /* MotionCoreAnimationViewContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9615275A1F393C0E00E8B2AC /* MotionCoreAnimationViewContext.swift */; };
961527831F393C0E00E8B2AC /* MotionHasInsertOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9615275B1F393C0E00E8B2AC /* MotionHasInsertOrder.swift */; };
961527841F393C0E00E8B2AC /* MotionTransitionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9615275C1F393C0E00E8B2AC /* MotionTransitionAnimator.swift */; };
961527851F393C0E00E8B2AC /* MotionViewPropertyViewContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9615275D1F393C0E00E8B2AC /* MotionViewPropertyViewContext.swift */; };
961527861F393C0E00E8B2AC /* Motion+Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9615275F1F393C0E00E8B2AC /* Motion+Array.swift */; };
961527871F393C0E00E8B2AC /* Motion+CALayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527601F393C0E00E8B2AC /* Motion+CALayer.swift */; };
961527881F393C0E00E8B2AC /* Motion+CAMediaTimingFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527611F393C0E00E8B2AC /* Motion+CAMediaTimingFunction.swift */; };
961527891F393C0E00E8B2AC /* Motion+CG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527621F393C0E00E8B2AC /* Motion+CG.swift */; };
9615278A1F393C0E00E8B2AC /* Motion+Obj-C.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527631F393C0E00E8B2AC /* Motion+Obj-C.swift */; };
9615278B1F393C0E00E8B2AC /* Motion+UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527641F393C0E00E8B2AC /* Motion+UIKit.swift */; };
9615278C1F393C0E00E8B2AC /* Motion+UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527651F393C0E00E8B2AC /* Motion+UIView.swift */; };
9615278D1F393C0E00E8B2AC /* Motion+UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527661F393C0E00E8B2AC /* Motion+UIViewController.swift */; };
9615278E1F393C0E00E8B2AC /* MotionAnimationFillMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527671F393C0E00E8B2AC /* MotionAnimationFillMode.swift */; };
9615278F1F393C0E00E8B2AC /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 961527681F393C0E00E8B2AC /* LICENSE */; };
961527901F393C0E00E8B2AC /* Motion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527691F393C0E00E8B2AC /* Motion.swift */; };
961527911F393C0E00E8B2AC /* MotionAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9615276A1F393C0E00E8B2AC /* MotionAnimation.swift */; };
961527921F393C0E00E8B2AC /* MotionAnimationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9615276B1F393C0E00E8B2AC /* MotionAnimationState.swift */; };
961527931F393C0E00E8B2AC /* MotionCAAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9615276C1F393C0E00E8B2AC /* MotionCAAnimation.swift */; };
961527941F393C0E00E8B2AC /* MotionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9615276D1F393C0E00E8B2AC /* MotionContext.swift */; };
961527951F393C0E00E8B2AC /* MotionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9615276E1F393C0E00E8B2AC /* MotionController.swift */; };
961527961F393C0E00E8B2AC /* MotionCoordinateSpace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9615276F1F393C0E00E8B2AC /* MotionCoordinateSpace.swift */; };
961527971F393C0E00E8B2AC /* MotionIndependentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527701F393C0E00E8B2AC /* MotionIndependentController.swift */; };
961527981F393C0E00E8B2AC /* MotionPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527711F393C0E00E8B2AC /* MotionPlugin.swift */; };
961527991F393C0E00E8B2AC /* MotionSnapshotType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527721F393C0E00E8B2AC /* MotionSnapshotType.swift */; };
9615279A1F393C0E00E8B2AC /* MotionTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527731F393C0E00E8B2AC /* MotionTransition.swift */; };
9615279B1F393C0E00E8B2AC /* MotionTransitionObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527741F393C0E00E8B2AC /* MotionTransitionObserver.swift */; };
9615279C1F393C0E00E8B2AC /* MotionTransitionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527751F393C0E00E8B2AC /* MotionTransitionState.swift */; };
9615279D1F393C0E00E8B2AC /* CascadePreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527771F393C0E00E8B2AC /* CascadePreprocessor.swift */; };
9615279E1F393C0E00E8B2AC /* DurationPreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527781F393C0E00E8B2AC /* DurationPreprocessor.swift */; };
9615279F1F393C0E00E8B2AC /* IgnoreSubviewModifiersPreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527791F393C0E00E8B2AC /* IgnoreSubviewModifiersPreprocessor.swift */; };
961527A01F393C0E00E8B2AC /* MatchPreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9615277A1F393C0E00E8B2AC /* MatchPreprocessor.swift */; };
961527A11F393C0E00E8B2AC /* MotionPreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9615277B1F393C0E00E8B2AC /* MotionPreprocessor.swift */; };
961527A21F393C0E00E8B2AC /* SourcePreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9615277C1F393C0E00E8B2AC /* SourcePreprocessor.swift */; };
961527A31F393C0E00E8B2AC /* TransitionPreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9615277D1F393C0E00E8B2AC /* TransitionPreprocessor.swift */; };
9617B07D1DFCA8CF00410F8F /* Application.swift in Headers */ = {isa = PBXBuildFile; fileRef = 961E6BDE1DDA2A95004E6C93 /* Application.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9617B07D1DFCA8CF00410F8F /* Application.swift in Headers */ = {isa = PBXBuildFile; fileRef = 961E6BDE1DDA2A95004E6C93 /* Application.swift */; settings = {ATTRIBUTES = (Public, ); }; };
9617B07E1DFCA8CF00410F8F /* Card.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB75D1CB40DC500C806FE /* Card.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9617B07E1DFCA8CF00410F8F /* Card.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB75D1CB40DC500C806FE /* Card.swift */; settings = {ATTRIBUTES = (Public, ); }; };
9617B07F1DFCA8CF00410F8F /* ImageCard.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7621CB40DC500C806FE /* ImageCard.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9617B07F1DFCA8CF00410F8F /* ImageCard.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7621CB40DC500C806FE /* ImageCard.swift */; settings = {ATTRIBUTES = (Public, ); }; };
...@@ -174,6 +212,44 @@ ...@@ -174,6 +212,44 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
961154CB1F32A7B100A78D74 /* Chips.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Chips.swift; sourceTree = "<group>"; }; 961154CB1F32A7B100A78D74 /* Chips.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Chips.swift; sourceTree = "<group>"; };
961276621DCD8B1800A7D920 /* CharacterAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CharacterAttribute.swift; sourceTree = "<group>"; }; 961276621DCD8B1800A7D920 /* CharacterAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CharacterAttribute.swift; sourceTree = "<group>"; };
961527541F393C0E00E8B2AC /* LICENSE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = "<group>"; };
961527551F393C0E00E8B2AC /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
961527581F393C0E00E8B2AC /* MotionAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionAnimator.swift; sourceTree = "<group>"; };
961527591F393C0E00E8B2AC /* MotionAnimatorViewContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionAnimatorViewContext.swift; sourceTree = "<group>"; };
9615275A1F393C0E00E8B2AC /* MotionCoreAnimationViewContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionCoreAnimationViewContext.swift; sourceTree = "<group>"; };
9615275B1F393C0E00E8B2AC /* MotionHasInsertOrder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionHasInsertOrder.swift; sourceTree = "<group>"; };
9615275C1F393C0E00E8B2AC /* MotionTransitionAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionTransitionAnimator.swift; sourceTree = "<group>"; };
9615275D1F393C0E00E8B2AC /* MotionViewPropertyViewContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionViewPropertyViewContext.swift; sourceTree = "<group>"; };
9615275F1F393C0E00E8B2AC /* Motion+Array.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Motion+Array.swift"; sourceTree = "<group>"; };
961527601F393C0E00E8B2AC /* Motion+CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Motion+CALayer.swift"; sourceTree = "<group>"; };
961527611F393C0E00E8B2AC /* Motion+CAMediaTimingFunction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Motion+CAMediaTimingFunction.swift"; sourceTree = "<group>"; };
961527621F393C0E00E8B2AC /* Motion+CG.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Motion+CG.swift"; sourceTree = "<group>"; };
961527631F393C0E00E8B2AC /* Motion+Obj-C.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Motion+Obj-C.swift"; sourceTree = "<group>"; };
961527641F393C0E00E8B2AC /* Motion+UIKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Motion+UIKit.swift"; sourceTree = "<group>"; };
961527651F393C0E00E8B2AC /* Motion+UIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Motion+UIView.swift"; sourceTree = "<group>"; };
961527661F393C0E00E8B2AC /* Motion+UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Motion+UIViewController.swift"; sourceTree = "<group>"; };
961527671F393C0E00E8B2AC /* MotionAnimationFillMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionAnimationFillMode.swift; sourceTree = "<group>"; };
961527681F393C0E00E8B2AC /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
961527691F393C0E00E8B2AC /* Motion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Motion.swift; sourceTree = "<group>"; };
9615276A1F393C0E00E8B2AC /* MotionAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionAnimation.swift; sourceTree = "<group>"; };
9615276B1F393C0E00E8B2AC /* MotionAnimationState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionAnimationState.swift; sourceTree = "<group>"; };
9615276C1F393C0E00E8B2AC /* MotionCAAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionCAAnimation.swift; sourceTree = "<group>"; };
9615276D1F393C0E00E8B2AC /* MotionContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionContext.swift; sourceTree = "<group>"; };
9615276E1F393C0E00E8B2AC /* MotionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionController.swift; sourceTree = "<group>"; };
9615276F1F393C0E00E8B2AC /* MotionCoordinateSpace.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionCoordinateSpace.swift; sourceTree = "<group>"; };
961527701F393C0E00E8B2AC /* MotionIndependentController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionIndependentController.swift; sourceTree = "<group>"; };
961527711F393C0E00E8B2AC /* MotionPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionPlugin.swift; sourceTree = "<group>"; };
961527721F393C0E00E8B2AC /* MotionSnapshotType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionSnapshotType.swift; sourceTree = "<group>"; };
961527731F393C0E00E8B2AC /* MotionTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionTransition.swift; sourceTree = "<group>"; };
961527741F393C0E00E8B2AC /* MotionTransitionObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionTransitionObserver.swift; sourceTree = "<group>"; };
961527751F393C0E00E8B2AC /* MotionTransitionState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionTransitionState.swift; sourceTree = "<group>"; };
961527771F393C0E00E8B2AC /* CascadePreprocessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CascadePreprocessor.swift; sourceTree = "<group>"; };
961527781F393C0E00E8B2AC /* DurationPreprocessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DurationPreprocessor.swift; sourceTree = "<group>"; };
961527791F393C0E00E8B2AC /* IgnoreSubviewModifiersPreprocessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IgnoreSubviewModifiersPreprocessor.swift; sourceTree = "<group>"; };
9615277A1F393C0E00E8B2AC /* MatchPreprocessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MatchPreprocessor.swift; sourceTree = "<group>"; };
9615277B1F393C0E00E8B2AC /* MotionPreprocessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionPreprocessor.swift; sourceTree = "<group>"; };
9615277C1F393C0E00E8B2AC /* SourcePreprocessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SourcePreprocessor.swift; sourceTree = "<group>"; };
9615277D1F393C0E00E8B2AC /* TransitionPreprocessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionPreprocessor.swift; sourceTree = "<group>"; };
961E6BDE1DDA2A95004E6C93 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; }; 961E6BDE1DDA2A95004E6C93 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
961E6BE11DDA2AF3004E6C93 /* Screen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; }; 961E6BE11DDA2AF3004E6C93 /* Screen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; };
961EFC571D738FF600E84652 /* SnackbarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnackbarController.swift; sourceTree = "<group>"; }; 961EFC571D738FF600E84652 /* SnackbarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnackbarController.swift; sourceTree = "<group>"; };
...@@ -291,6 +367,83 @@ ...@@ -291,6 +367,83 @@
name = Chip; name = Chip;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
961527531F393C0E00E8B2AC /* Motion */ = {
isa = PBXGroup;
children = (
961527541F393C0E00E8B2AC /* LICENSE.md */,
961527551F393C0E00E8B2AC /* README.md */,
961527561F393C0E00E8B2AC /* Sources */,
);
path = Motion;
sourceTree = "<group>";
};
961527561F393C0E00E8B2AC /* Sources */ = {
isa = PBXGroup;
children = (
961527571F393C0E00E8B2AC /* Animator */,
9615275E1F393C0E00E8B2AC /* Extensions */,
961527681F393C0E00E8B2AC /* LICENSE */,
961527691F393C0E00E8B2AC /* Motion.swift */,
9615276A1F393C0E00E8B2AC /* MotionAnimation.swift */,
9615276B1F393C0E00E8B2AC /* MotionAnimationState.swift */,
9615276C1F393C0E00E8B2AC /* MotionCAAnimation.swift */,
9615276D1F393C0E00E8B2AC /* MotionContext.swift */,
9615276E1F393C0E00E8B2AC /* MotionController.swift */,
9615276F1F393C0E00E8B2AC /* MotionCoordinateSpace.swift */,
961527701F393C0E00E8B2AC /* MotionIndependentController.swift */,
961527711F393C0E00E8B2AC /* MotionPlugin.swift */,
961527721F393C0E00E8B2AC /* MotionSnapshotType.swift */,
961527731F393C0E00E8B2AC /* MotionTransition.swift */,
961527741F393C0E00E8B2AC /* MotionTransitionObserver.swift */,
961527751F393C0E00E8B2AC /* MotionTransitionState.swift */,
961527761F393C0E00E8B2AC /* Preprocessors */,
);
path = Sources;
sourceTree = "<group>";
};
961527571F393C0E00E8B2AC /* Animator */ = {
isa = PBXGroup;
children = (
961527581F393C0E00E8B2AC /* MotionAnimator.swift */,
961527591F393C0E00E8B2AC /* MotionAnimatorViewContext.swift */,
9615275A1F393C0E00E8B2AC /* MotionCoreAnimationViewContext.swift */,
9615275B1F393C0E00E8B2AC /* MotionHasInsertOrder.swift */,
9615275C1F393C0E00E8B2AC /* MotionTransitionAnimator.swift */,
9615275D1F393C0E00E8B2AC /* MotionViewPropertyViewContext.swift */,
);
path = Animator;
sourceTree = "<group>";
};
9615275E1F393C0E00E8B2AC /* Extensions */ = {
isa = PBXGroup;
children = (
9615275F1F393C0E00E8B2AC /* Motion+Array.swift */,
961527601F393C0E00E8B2AC /* Motion+CALayer.swift */,
961527611F393C0E00E8B2AC /* Motion+CAMediaTimingFunction.swift */,
961527621F393C0E00E8B2AC /* Motion+CG.swift */,
961527631F393C0E00E8B2AC /* Motion+Obj-C.swift */,
961527641F393C0E00E8B2AC /* Motion+UIKit.swift */,
961527651F393C0E00E8B2AC /* Motion+UIView.swift */,
961527661F393C0E00E8B2AC /* Motion+UIViewController.swift */,
961527671F393C0E00E8B2AC /* MotionAnimationFillMode.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
961527761F393C0E00E8B2AC /* Preprocessors */ = {
isa = PBXGroup;
children = (
961527771F393C0E00E8B2AC /* CascadePreprocessor.swift */,
961527781F393C0E00E8B2AC /* DurationPreprocessor.swift */,
961527791F393C0E00E8B2AC /* IgnoreSubviewModifiersPreprocessor.swift */,
9615277A1F393C0E00E8B2AC /* MatchPreprocessor.swift */,
9615277B1F393C0E00E8B2AC /* MotionPreprocessor.swift */,
9615277C1F393C0E00E8B2AC /* SourcePreprocessor.swift */,
9615277D1F393C0E00E8B2AC /* TransitionPreprocessor.swift */,
);
path = Preprocessors;
sourceTree = "<group>";
};
961E6BDD1DDA2A7E004E6C93 /* Application */ = { 961E6BDD1DDA2A7E004E6C93 /* Application */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
...@@ -359,6 +512,7 @@ ...@@ -359,6 +512,7 @@
9630ACB71F29A26B00B4217D /* Frameworks */ = { 9630ACB71F29A26B00B4217D /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
961527531F393C0E00E8B2AC /* Motion */,
); );
path = Frameworks; path = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -838,6 +992,7 @@ ...@@ -838,6 +992,7 @@
96334EF61C8B84660083986B /* Assets.xcassets in Resources */, 96334EF61C8B84660083986B /* Assets.xcassets in Resources */,
96BCB7F71CB40DE900C806FE /* Roboto-Medium.ttf in Resources */, 96BCB7F71CB40DE900C806FE /* Roboto-Medium.ttf in Resources */,
96BCB7F31CB40DE900C806FE /* Roboto-Bold.ttf in Resources */, 96BCB7F31CB40DE900C806FE /* Roboto-Bold.ttf in Resources */,
9615278F1F393C0E00E8B2AC /* LICENSE in Resources */,
96BCB7FB1CB40DE900C806FE /* Roboto-Thin.ttf in Resources */, 96BCB7FB1CB40DE900C806FE /* Roboto-Thin.ttf in Resources */,
96BCB7F91CB40DE900C806FE /* Roboto-Regular.ttf in Resources */, 96BCB7F91CB40DE900C806FE /* Roboto-Regular.ttf in Resources */,
96BCB7F51CB40DE900C806FE /* Roboto-Light.ttf in Resources */, 96BCB7F51CB40DE900C806FE /* Roboto-Light.ttf in Resources */,
...@@ -853,26 +1008,37 @@ ...@@ -853,26 +1008,37 @@
files = ( files = (
961E6BE21DDA2AF3004E6C93 /* Screen.swift in Sources */, 961E6BE21DDA2AF3004E6C93 /* Screen.swift in Sources */,
965E81261DD4D7C800D61E4B /* CharacterAttribute.swift in Sources */, 965E81261DD4D7C800D61E4B /* CharacterAttribute.swift in Sources */,
961527861F393C0E00E8B2AC /* Motion+Array.swift in Sources */,
965E80FF1DD4D5C800D61E4B /* BottomNavigationController.swift in Sources */, 965E80FF1DD4D5C800D61E4B /* BottomNavigationController.swift in Sources */,
965E81031DD4D5C800D61E4B /* CollectionView.swift in Sources */, 965E81031DD4D5C800D61E4B /* CollectionView.swift in Sources */,
965E81041DD4D5C800D61E4B /* CollectionViewCell.swift in Sources */, 965E81041DD4D5C800D61E4B /* CollectionViewCell.swift in Sources */,
961527931F393C0E00E8B2AC /* MotionCAAnimation.swift in Sources */,
965E81071DD4D5C800D61E4B /* CollectionViewLayout.swift in Sources */, 965E81071DD4D5C800D61E4B /* CollectionViewLayout.swift in Sources */,
961527A31F393C0E00E8B2AC /* TransitionPreprocessor.swift in Sources */,
965E81081DD4D5C800D61E4B /* CollectionReusableView.swift in Sources */, 965E81081DD4D5C800D61E4B /* CollectionReusableView.swift in Sources */,
961527951F393C0E00E8B2AC /* MotionController.swift in Sources */,
965E81091DD4D5C800D61E4B /* DataSourceItem.swift in Sources */, 965E81091DD4D5C800D61E4B /* DataSourceItem.swift in Sources */,
965E810A1DD4D5C800D61E4B /* Font.swift in Sources */, 965E810A1DD4D5C800D61E4B /* Font.swift in Sources */,
965E810B1DD4D5C800D61E4B /* RobotoFont.swift in Sources */, 965E810B1DD4D5C800D61E4B /* RobotoFont.swift in Sources */,
965E810C1DD4D5C800D61E4B /* DynamicFontType.swift in Sources */, 965E810C1DD4D5C800D61E4B /* DynamicFontType.swift in Sources */,
961527A11F393C0E00E8B2AC /* MotionPreprocessor.swift in Sources */,
9615279D1F393C0E00E8B2AC /* CascadePreprocessor.swift in Sources */,
96A183651E0C6DD400083C30 /* FABMenuController.swift in Sources */, 96A183651E0C6DD400083C30 /* FABMenuController.swift in Sources */,
965E81101DD4D5C800D61E4B /* NavigationBar.swift in Sources */, 965E81101DD4D5C800D61E4B /* NavigationBar.swift in Sources */,
965E81111DD4D5C800D61E4B /* NavigationController.swift in Sources */, 965E81111DD4D5C800D61E4B /* NavigationController.swift in Sources */,
965E81121DD4D5C800D61E4B /* NavigationItem.swift in Sources */, 965E81121DD4D5C800D61E4B /* NavigationItem.swift in Sources */,
9615278D1F393C0E00E8B2AC /* Motion+UIViewController.swift in Sources */,
965E81131DD4D5C800D61E4B /* NavigationDrawerController.swift in Sources */, 965E81131DD4D5C800D61E4B /* NavigationDrawerController.swift in Sources */,
9656895F1F002F16001C656D /* CardCollectionViewCell.swift in Sources */, 9656895F1F002F16001C656D /* CardCollectionViewCell.swift in Sources */,
965E81161DD4D5C800D61E4B /* DisplayStyle.swift in Sources */, 965E81161DD4D5C800D61E4B /* DisplayStyle.swift in Sources */,
961527921F393C0E00E8B2AC /* MotionAnimationState.swift in Sources */,
965E81171DD4D5C800D61E4B /* TransitionController.swift in Sources */, 965E81171DD4D5C800D61E4B /* TransitionController.swift in Sources */,
965E81181DD4D5C800D61E4B /* Snackbar.swift in Sources */, 965E81181DD4D5C800D61E4B /* Snackbar.swift in Sources */,
965E81191DD4D5C800D61E4B /* SnackbarController.swift in Sources */, 965E81191DD4D5C800D61E4B /* SnackbarController.swift in Sources */,
961527811F393C0E00E8B2AC /* MotionAnimatorViewContext.swift in Sources */,
965E811A1DD4D5C800D61E4B /* StatusBarController.swift in Sources */, 965E811A1DD4D5C800D61E4B /* StatusBarController.swift in Sources */,
9615277F1F393C0E00E8B2AC /* README.md in Sources */,
961527871F393C0E00E8B2AC /* Motion+CALayer.swift in Sources */,
965E811B1DD4D5C800D61E4B /* Switch.swift in Sources */, 965E811B1DD4D5C800D61E4B /* Switch.swift in Sources */,
965E811C1DD4D5C800D61E4B /* TabBar.swift in Sources */, 965E811C1DD4D5C800D61E4B /* TabBar.swift in Sources */,
965E811D1DD4D5C800D61E4B /* TableViewCell.swift in Sources */, 965E811D1DD4D5C800D61E4B /* TableViewCell.swift in Sources */,
...@@ -884,26 +1050,40 @@ ...@@ -884,26 +1050,40 @@
965E80E81DD4C55200D61E4B /* Material+CALayer.swift in Sources */, 965E80E81DD4C55200D61E4B /* Material+CALayer.swift in Sources */,
965E80E91DD4C55200D61E4B /* Material+String.swift in Sources */, 965E80E91DD4C55200D61E4B /* Material+String.swift in Sources */,
965E80F71DD4D59500D61E4B /* Card.swift in Sources */, 965E80F71DD4D59500D61E4B /* Card.swift in Sources */,
9615279E1F393C0E00E8B2AC /* DurationPreprocessor.swift in Sources */,
965E80EA1DD4C55200D61E4B /* Material+UIFont.swift in Sources */, 965E80EA1DD4C55200D61E4B /* Material+UIFont.swift in Sources */,
9615278A1F393C0E00E8B2AC /* Motion+Obj-C.swift in Sources */,
965E80EB1DD4C55200D61E4B /* Material+UIImage.swift in Sources */, 965E80EB1DD4C55200D61E4B /* Material+UIImage.swift in Sources */,
9615279C1F393C0E00E8B2AC /* MotionTransitionState.swift in Sources */,
965E80EC1DD4C55200D61E4B /* Material+Array.swift in Sources */, 965E80EC1DD4C55200D61E4B /* Material+Array.swift in Sources */,
965E80ED1DD4C55200D61E4B /* Material+UIWindow.swift in Sources */, 965E80ED1DD4C55200D61E4B /* Material+UIWindow.swift in Sources */,
965E80E41DD4C53300D61E4B /* PulseView.swift in Sources */, 965E80E41DD4C53300D61E4B /* PulseView.swift in Sources */,
966C17731F0439F600D3E83C /* Material+MotionAnimation.swift in Sources */, 966C17731F0439F600D3E83C /* Material+MotionAnimation.swift in Sources */,
961527841F393C0E00E8B2AC /* MotionTransitionAnimator.swift in Sources */,
965E80E51DD4C53300D61E4B /* PulseAnimation.swift in Sources */, 965E80E51DD4C53300D61E4B /* PulseAnimation.swift in Sources */,
965E80FE1DD4D59500D61E4B /* ToolbarController.swift in Sources */, 965E80FE1DD4D59500D61E4B /* ToolbarController.swift in Sources */,
96328B971E05C0BB009A4C90 /* TableView.swift in Sources */, 96328B971E05C0BB009A4C90 /* TableView.swift in Sources */,
965E80F81DD4D59500D61E4B /* ImageCard.swift in Sources */, 965E80F81DD4D59500D61E4B /* ImageCard.swift in Sources */,
9615279B1F393C0E00E8B2AC /* MotionTransitionObserver.swift in Sources */,
961527821F393C0E00E8B2AC /* MotionCoreAnimationViewContext.swift in Sources */,
96328B991E05C0CE009A4C90 /* TableViewController.swift in Sources */, 96328B991E05C0CE009A4C90 /* TableViewController.swift in Sources */,
961527851F393C0E00E8B2AC /* MotionViewPropertyViewContext.swift in Sources */,
961527891F393C0E00E8B2AC /* Motion+CG.swift in Sources */,
965E80F91DD4D59500D61E4B /* PresenterCard.swift in Sources */, 965E80F91DD4D59500D61E4B /* PresenterCard.swift in Sources */,
9615278E1F393C0E00E8B2AC /* MotionAnimationFillMode.swift in Sources */,
9615278C1F393C0E00E8B2AC /* Motion+UIView.swift in Sources */,
961527961F393C0E00E8B2AC /* MotionCoordinateSpace.swift in Sources */,
96E09DC81F2287E50000B121 /* TabsController.swift in Sources */, 96E09DC81F2287E50000B121 /* TabsController.swift in Sources */,
961154CC1F32A7B100A78D74 /* Chips.swift in Sources */, 961154CC1F32A7B100A78D74 /* Chips.swift in Sources */,
961527941F393C0E00E8B2AC /* MotionContext.swift in Sources */,
965689611F002F4C001C656D /* CardCollectionViewController.swift in Sources */, 965689611F002F4C001C656D /* CardCollectionViewController.swift in Sources */,
965E80CC1DD4C50600D61E4B /* Bar.swift in Sources */, 965E80CC1DD4C50600D61E4B /* Bar.swift in Sources */,
961527901F393C0E00E8B2AC /* Motion.swift in Sources */,
965E80CD1DD4C50600D61E4B /* Button.swift in Sources */, 965E80CD1DD4C50600D61E4B /* Button.swift in Sources */,
965E80CE1DD4C50600D61E4B /* FABButton.swift in Sources */, 965E80CE1DD4C50600D61E4B /* FABButton.swift in Sources */,
965E80CF1DD4C50600D61E4B /* FlatButton.swift in Sources */, 965E80CF1DD4C50600D61E4B /* FlatButton.swift in Sources */,
965E80D01DD4C50600D61E4B /* RaisedButton.swift in Sources */, 965E80D01DD4C50600D61E4B /* RaisedButton.swift in Sources */,
961527831F393C0E00E8B2AC /* MotionHasInsertOrder.swift in Sources */,
96A183631E0C6CE200083C30 /* FABMenu.swift in Sources */, 96A183631E0C6CE200083C30 /* FABMenu.swift in Sources */,
965E80D11DD4C50600D61E4B /* IconButton.swift in Sources */, 965E80D11DD4C50600D61E4B /* IconButton.swift in Sources */,
965E80D21DD4C50600D61E4B /* Color.swift in Sources */, 965E80D21DD4C50600D61E4B /* Color.swift in Sources */,
...@@ -911,23 +1091,35 @@ ...@@ -911,23 +1091,35 @@
965E80D31DD4C50600D61E4B /* Device.swift in Sources */, 965E80D31DD4C50600D61E4B /* Device.swift in Sources */,
965E80FD1DD4D59500D61E4B /* Toolbar.swift in Sources */, 965E80FD1DD4D59500D61E4B /* Toolbar.swift in Sources */,
965E80D41DD4C50600D61E4B /* Divider.swift in Sources */, 965E80D41DD4C50600D61E4B /* Divider.swift in Sources */,
961527971F393C0E00E8B2AC /* MotionIndependentController.swift in Sources */,
965E80D51DD4C50600D61E4B /* Grid.swift in Sources */, 965E80D51DD4C50600D61E4B /* Grid.swift in Sources */,
965E80D61DD4C50600D61E4B /* HeightPreset.swift in Sources */, 965E80D61DD4C50600D61E4B /* HeightPreset.swift in Sources */,
961527A01F393C0E00E8B2AC /* MatchPreprocessor.swift in Sources */,
9615278B1F393C0E00E8B2AC /* Motion+UIKit.swift in Sources */,
961E6BDF1DDA2A95004E6C93 /* Application.swift in Sources */, 961E6BDF1DDA2A95004E6C93 /* Application.swift in Sources */,
965E80D71DD4C50600D61E4B /* Icon.swift in Sources */, 965E80D71DD4C50600D61E4B /* Icon.swift in Sources */,
965E80FC1DD4D59500D61E4B /* SearchBarController.swift in Sources */, 965E80FC1DD4D59500D61E4B /* SearchBarController.swift in Sources */,
965E80D81DD4C50600D61E4B /* Layer.swift in Sources */, 965E80D81DD4C50600D61E4B /* Layer.swift in Sources */,
961527A21F393C0E00E8B2AC /* SourcePreprocessor.swift in Sources */,
965E80D91DD4C50600D61E4B /* Layout.swift in Sources */, 965E80D91DD4C50600D61E4B /* Layout.swift in Sources */,
965E80DA1DD4C50600D61E4B /* Border.swift in Sources */, 965E80DA1DD4C50600D61E4B /* Border.swift in Sources */,
965E80DB1DD4C50600D61E4B /* InterimSpace.swift in Sources */, 965E80DB1DD4C50600D61E4B /* InterimSpace.swift in Sources */,
961527881F393C0E00E8B2AC /* Motion+CAMediaTimingFunction.swift in Sources */,
965E80DC1DD4C50600D61E4B /* Depth.swift in Sources */, 965E80DC1DD4C50600D61E4B /* Depth.swift in Sources */,
961527991F393C0E00E8B2AC /* MotionSnapshotType.swift in Sources */,
965E80DD1DD4C50600D61E4B /* EdgeInsets.swift in Sources */, 965E80DD1DD4C50600D61E4B /* EdgeInsets.swift in Sources */,
965E80DE1DD4C50600D61E4B /* Gravity.swift in Sources */, 965E80DE1DD4C50600D61E4B /* Gravity.swift in Sources */,
965E80DF1DD4C50600D61E4B /* CornerRadius.swift in Sources */, 965E80DF1DD4C50600D61E4B /* CornerRadius.swift in Sources */,
965E80FB1DD4D59500D61E4B /* SearchBar.swift in Sources */, 965E80FB1DD4D59500D61E4B /* SearchBar.swift in Sources */,
965E80E01DD4C50600D61E4B /* Shape.swift in Sources */, 965E80E01DD4C50600D61E4B /* Shape.swift in Sources */,
965E80E11DD4C50600D61E4B /* Offset.swift in Sources */, 965E80E11DD4C50600D61E4B /* Offset.swift in Sources */,
961527981F393C0E00E8B2AC /* MotionPlugin.swift in Sources */,
961527801F393C0E00E8B2AC /* MotionAnimator.swift in Sources */,
965E80E21DD4C50600D61E4B /* View.swift in Sources */, 965E80E21DD4C50600D61E4B /* View.swift in Sources */,
961527911F393C0E00E8B2AC /* MotionAnimation.swift in Sources */,
9615277E1F393C0E00E8B2AC /* LICENSE.md in Sources */,
9615279A1F393C0E00E8B2AC /* MotionTransition.swift in Sources */,
9615279F1F393C0E00E8B2AC /* IgnoreSubviewModifiersPreprocessor.swift in Sources */,
96328B7A1E020A41009A4C90 /* CollectionViewController.swift in Sources */, 96328B7A1E020A41009A4C90 /* CollectionViewController.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
......
The MIT License (MIT)
Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
![Motion Logo](http://www.cosmicmind.com/motion/logo/motion_logo.png)
# Motion
Welcome to **Motion,** a library used to create beautiful animations and transitions for views, layers, and view controllers.
## Photos Sample
Take a look at a sample [Photos](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/Photos) project to get started.
![Photos](http://www.cosmicmind.com/motion/projects/photos.gif)
* [Photos Sample](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/Photos)
### Who is Motion for?
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.
### What you will learn
You will learn how to use Motion with a general introduction to fundamental concepts and easy to use code snippets.
# Transitions
Motion transitions a source view to a destination view using a linking identifier property named `motionIdentifier`.
| Match | Translate | Rotate | Arc | Scale |
|:---:|:---:|:---:|:---:|:---:|
| ![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) |
### Example Usage
An example of transitioning from one view controller to another with transitions:
#### View Controller 1
```swift
greyView.motionIdentifier = "foo"
orangeView.motionIdentifier = "bar"
```
#### View Controller 2
```swift
isMotionEnabled = true
greyView.motionIdentifier = "foo"
orangeView.motionIdentifier = "bar"
orangeView.transition(.translate(x: -100))
```
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.
* [MotionTransition API](https://cosmicmind.gitbooks.io/motion/content/motion_transition_api.html)
* [Code Samples](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/TransitionsWithIdentifier)
## UINavigationController, UITabBarController, and UIViewController Transitions
Motion offers default transitions that may be used by UINavigationControllers, UITabBarControllers, and presenting UIViewControllers.
| Push | Slide | ZoomSlide | Cover | Page | Fade | Zoom |
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| ![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)|
### Example Usage
An example of transitioning from one view controller to another using a UINavigationController with a zoom transition:
#### UINavigationController
```swift
class AppNavigationController: UINavigationController {
open override func viewDidLoad() {
super.viewDidLoad()
isMotionEnabled = true
motionNavigationTransitionType = .zoom
}
}
```
To add an automatic reverse transition, use `autoReverse`.
```swift
motionNavigationTransitionType = .autoReverse(presenting: .zoom)
```
* [Code Samples](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/Transitions)
# Animations
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.
| Background Color | Corder Radius | Fade | Rotate | Size | Spring |
|:---:|:---:|:---:|:---:|:---:|:---:|
| ![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) |
| Border Color & Border Width | Depth | Position | Scale | Spin | Translate |
|:---:|:---:|:---:|:---:|:---:|:---:|
|![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) |
### Example Usage
```swift
let box = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
box.backgroundColor = .blue
box.animate(.background(color: .red), .rotate(180), .delay(1))
```
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.
* [MotionAnimation API](https://cosmicmind.gitbooks.io/motion/content/motion_animation_api.html)
* [Code Samples](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/Animations)
## Requirements
* iOS 8.0+
* Xcode 8.0+
## Communication
- If you **need help**, use [Stack Overflow](http://stackoverflow.com/questions/tagged/cosmicmind). (Tag 'cosmicmind')
- If you'd like to **ask a general question**, use [Stack Overflow](http://stackoverflow.com/questions/tagged/cosmicmind).
- If you **found a bug**, _and can provide steps to reliably reproduce it_, open an issue.
- If you **have a feature request**, open an issue.
- If you **want to contribute**, submit a pull request.
## Installation
> **Embedded frameworks require a minimum deployment target of iOS 8.**
> - [Download Motion](https://github.com/CosmicMind/Motion/archive/master.zip)
Visit the [Installation](https://cosmicmind.gitbooks.io/motion/content/installation.html) page to learn how to install Motion using [CocoaPods](http://cocoapods.org) and [Carthage](https://github.com/Carthage/Carthage).
## Change Log
Motion is a growing project and will encounter changes throughout its development. It is recommended that the [Change Log](https://cosmicmind.gitbooks.io/motion/content/change_log.html) be reviewed prior to updating versions.
## License
The MIT License (MIT)
Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
public protocol MotionAnimator: class {
/// A reference to a MotionContext.
weak var context: MotionContext! { get set }
/// Cleans the contexts.
func clean()
/**
A function that determines if a view can be animated.
- Parameter view: A UIView.
- Parameter isAppearing: A boolean that determines whether the
view is appearing.
*/
func canAnimate(view: UIView, isAppearing: Bool) -> Bool
/**
Animates the fromViews to the toViews.
- Parameter fromViews: An Array of UIViews.
- Parameter toViews: An Array of UIViews.
- Returns: A TimeInterval.
*/
func animate(fromViews: [UIView], toViews: [UIView]) -> TimeInterval
/**
Moves the view's animation to the given elapsed time.
- Parameter to elapsedTime: A TimeInterval.
*/
func seek(to elapsedTime: TimeInterval)
/**
Resumes the animation with a given elapsed time and
optional reversed boolean.
- Parameter at elapsedTime: A TimeInterval.
- Parameter isReversed: A boolean to reverse the animation
or not.
*/
func resume(at elapsedTime: TimeInterval, isReversed: Bool) -> TimeInterval
/**
Applies the given state to the given view.
- Parameter state: A MotionTransitionState.
- Parameter to view: A UIView.
*/
func apply(state: MotionTransitionState, to view: UIView)
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
internal class MotionAnimatorViewContext {
/// An optional reference to a MotionAnimator.
var animator: MotionAnimator?
/// A reference to the snapshot UIView.
var snapshot: UIView
/// Animation duration time.
var duration: TimeInterval = 0
/// The animation target state.
var targetState: MotionTransitionState
/// The computed current time of the snapshot layer.
var currentTime: TimeInterval {
return snapshot.layer.convertTime(CACurrentMediaTime(), from: nil)
}
/// A container view for the transition.
var container: UIView? {
return animator?.context.container
}
/**
An initializer.
- Parameter animator: A MotionAnimator.
- Parameter snapshot: A UIView.
- Parameter targetState: A MotionTransitionState.
*/
required init(animator: MotionAnimator, snapshot: UIView, targetState: MotionTransitionState) {
self.animator = animator
self.snapshot = snapshot
self.targetState = targetState
}
/// Cleans the context.
func clean() {
animator = nil
}
/**
A class function that determines if a view can be animated
to a given state.
- Parameter view: A UIView.
- Parameter state: A MotionTransitionState.
- Parameter isAppearing: A boolean that determines whether the
view is appearing.
*/
class func canAnimate(view: UIView, state: MotionTransitionState, isAppearing: Bool) -> Bool {
return false
}
/**
Resumes the animation with a given elapsed time and
optional reversed boolean.
- Parameter at elapsedTime: A TimeInterval.
- Parameter isReversed: A boolean to reverse the animation
or not.
*/
func resume(at elapsedTime: TimeInterval, isReversed: Bool) {}
/**
Moves the animation to the given elapsed time.
- Parameter to elapsedTime: A TimeInterval.
*/
func seek(to elapsedTime: TimeInterval) {}
/**
Applies the given state to the target state.
- Parameter state: A MotionTransitionState.
*/
func apply(state: MotionTransitionState) {}
/**
Starts the animations with an appearing boolean flag.
- Parameter isAppearing: A boolean value whether the view
is appearing or not.
*/
func startAnimations(isAppearing: Bool) {}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext {
/// The transition states.
fileprivate var transitionStates = [String: (Any?, Any?)]()
/// A reference to the animation timing function.
fileprivate var timingFunction = CAMediaTimingFunction.standard
/// Layer which holds the content.
fileprivate var contentLayer: CALayer? {
return snapshot.layer.sublayers?.get(0)
}
/// Layer which holds the overlay.
fileprivate var overlayLayer: CALayer?
override func clean() {
super.clean()
overlayLayer = nil
}
override class func canAnimate(view: UIView, state: MotionTransitionState, isAppearing: Bool) -> Bool {
return nil != state.position ||
nil != state.size ||
nil != state.transform ||
nil != state.cornerRadius ||
nil != state.opacity ||
nil != state.overlay ||
nil != state.backgroundColor ||
nil != state.borderColor ||
nil != state.borderWidth ||
nil != state.shadowOpacity ||
nil != state.shadowRadius ||
nil != state.shadowOffset ||
nil != state.shadowColor ||
nil != state.shadowPath ||
nil != state.contentsRect ||
state.forceAnimate
}
override func apply(state: MotionTransitionState) {
let ts = viewState(targetState: state)
for (key, targetValue) in ts {
if nil == transitionStates[key] {
let current = currentValue(for: key)
transitionStates[key] = (current, current)
}
animate(key: key, beginTime: 0, fromValue: targetValue, toValue: targetValue)
}
}
override func resume(at elapsedTime: TimeInterval, isReversed: Bool) {
for (key, (fromValue, toValue)) in transitionStates {
transitionStates[key] = (currentValue(for: key), !isReversed ? toValue : fromValue)
}
targetState.duration = isReversed ? elapsedTime - targetState.delay : duration - elapsedTime
animate(delay: max(0, targetState.delay - elapsedTime))
}
override func seek(to elapsedTime: TimeInterval) {
seek(layer: snapshot.layer, elapsedTime: elapsedTime)
if let v = contentLayer {
seek(layer: v, elapsedTime: elapsedTime)
}
if let v = overlayLayer {
seek(layer: v, elapsedTime: elapsedTime)
}
}
override func startAnimations(isAppearing: Bool) {
if let beginState = targetState.beginState?.state {
let appeared = viewState(targetState: beginState)
for (k, v) in appeared {
snapshot.layer.setValue(v, forKeyPath: k)
}
if let (k, v) = beginState.overlay {
let overlay = getOverlayLayer()
overlay.backgroundColor = k
overlay.opacity = Float(v)
}
}
let disappeared = viewState(targetState: targetState)
for (k, v) in disappeared {
let isAppearingState = currentValue(for: k)
let toValue = isAppearing ? isAppearingState : v
let fromValue = !isAppearing ? isAppearingState : v
transitionStates[k] = (fromValue, toValue)
}
animate(delay: targetState.delay)
}
}
extension MotionCoreAnimationViewContext {
/**
Lazy loads the overlay layer.
- Returns: A CALayer.
*/
fileprivate func getOverlayLayer() -> CALayer {
if nil == overlayLayer {
overlayLayer = CALayer()
overlayLayer!.frame = snapshot.bounds
overlayLayer!.opacity = 0
snapshot.layer.addSublayer(overlayLayer!)
}
return overlayLayer!
}
/**
Retrieves the overlay key for a given key.
- Parameter for key: A String.
- Returns: An optional String.
*/
fileprivate func overlayKey(for key: String) -> String? {
guard key.hasPrefix("overlay.") else {
return nil
}
var k = key
k.removeSubrange(k.startIndex..<k.index(key.startIndex, offsetBy: 8))
return k
}
/**
Retrieves the current value for a given key.
- Parameter for key: A String.
- Returns: An optional Any value.
*/
fileprivate func currentValue(for key: String) -> Any? {
if let key = overlayKey(for: key) {
return overlayLayer?.value(forKeyPath: key)
}
if false != snapshot.layer.animationKeys()?.isEmpty {
return snapshot.layer.value(forKeyPath:key)
}
return (snapshot.layer.presentation() ?? snapshot.layer).value(forKeyPath: key)
}
/**
Retrieves the animation for a given key.
- Parameter key: String.
- Parameter beginTime: A TimeInterval.
- Parameter fromValue: An optional Any value.
- Parameter toValue: An optional Any value.
- Parameter ignoreArc: A Boolean value to ignore an arc position.
*/
fileprivate func getAnimation(key: String, beginTime: TimeInterval, fromValue: Any?, toValue: Any?, ignoreArc: Bool = false) -> CAPropertyAnimation {
let key = overlayKey(for: key) ?? key
let anim: CAPropertyAnimation
if !ignoreArc, "position" == key, let arcIntensity = targetState.arc,
let fromPos = (fromValue as? NSValue)?.cgPointValue,
let toPos = (toValue as? NSValue)?.cgPointValue,
abs(fromPos.x - toPos.x) >= 1, abs(fromPos.y - toPos.y) >= 1 {
let a = CAKeyframeAnimation(keyPath: key)
let path = CGMutablePath()
let maxControl = fromPos.y > toPos.y ? CGPoint(x: toPos.x, y: fromPos.y) : CGPoint(x: fromPos.x, y: toPos.y)
let minControl = (toPos - fromPos) / 2 + fromPos
path.move(to: fromPos)
path.addQuadCurve(to: toPos, control: minControl + (maxControl - minControl) * arcIntensity)
a.values = [fromValue!, toValue!]
a.path = path
a.duration = duration
a.timingFunctions = [timingFunction]
anim = a
} else if #available(iOS 9.0, *), "cornerRadius" != key, let (stiffness, damping) = targetState.spring {
let a = CASpringAnimation(keyPath: key)
a.stiffness = stiffness
a.damping = damping
a.duration = a.settlingDuration * 0.9
a.fromValue = fromValue
a.toValue = toValue
anim = a
} else {
let a = CABasicAnimation(keyPath: key)
a.duration = duration
a.fromValue = fromValue
a.toValue = toValue
a.timingFunction = timingFunction
anim = a
}
anim.fillMode = kCAFillModeBoth
anim.isRemovedOnCompletion = false
anim.beginTime = beginTime
return anim
}
/**
Retrieves the duration of an animation, including the
duration of the animation and the initial delay.
- Parameter key: A String.
- Parameter beginTime: A TimeInterval.
- Parameter fromValue: A optional Any value.
- Parameter toValue: A optional Any value.
- Returns: A TimeInterval.
*/
@discardableResult
fileprivate func animate(key: String, beginTime: TimeInterval, fromValue: Any?, toValue: Any?) -> TimeInterval {
let anim = getAnimation(key: key, beginTime:beginTime, fromValue: fromValue, toValue: toValue)
if let overlayKey = overlayKey(for: key) {
getOverlayLayer().add(anim, forKey: overlayKey)
} else {
snapshot.layer.add(anim, forKey: key)
switch key {
case "cornerRadius", "contentsRect", "contentsScale":
contentLayer?.add(anim, forKey: key)
overlayLayer?.add(anim, forKey: key)
case "bounds.size":
guard let fs = (fromValue as? NSValue)?.cgSizeValue else {
return 0
}
guard let ts = (toValue as? NSValue)?.cgSizeValue else {
return 0
}
// for the snapshotView(UIReplicantView): there is a
// subview(UIReplicantContentView) that is hosting the real snapshot image.
// because we are using CAAnimations and not UIView animations,
// The snapshotView will not layout during animations.
// we have to add two more animations to manually layout the content view.
let fpn = NSValue(cgPoint: fs.center)
let tpn = NSValue(cgPoint: ts.center)
let a = getAnimation(key: "position", beginTime: 0, fromValue: fpn, toValue: tpn, ignoreArc: true)
a.beginTime = anim.beginTime
a.timingFunction = anim.timingFunction
a.duration = anim.duration
contentLayer?.add(a, forKey: "position")
contentLayer?.add(anim, forKey: key)
overlayLayer?.add(a, forKey: "position")
overlayLayer?.add(anim, forKey: key)
default: break
}
}
return anim.duration + anim.beginTime - beginTime
}
/**
Animates the contentLayer and overlayLayer with a given delay.
- Parameter delay: A TimeInterval.
*/
fileprivate func animate(delay: TimeInterval) {
if let v = targetState.timingFunction {
timingFunction = v
}
if let v = targetState.duration {
duration = v
}
let beginTime = currentTime + delay
var finalDuration: TimeInterval = duration
for (key, (fromValue, toValue)) in transitionStates {
let neededTime = animate(key: key, beginTime: beginTime, fromValue: fromValue, toValue: toValue)
finalDuration = max(finalDuration, neededTime + delay)
}
duration = finalDuration
}
/**
Constructs a map of key paths to animation state values.
- Parameter targetState state: A MotionTransitionState.
- Returns: A map of key paths to animation values.
*/
fileprivate func viewState(targetState ts: MotionTransitionState) -> [String: Any?] {
var ts = ts
var values = [String: Any?]()
if let size = ts.size {
if ts.useScaleBasedSizeChange ?? targetState.useScaleBasedSizeChange ?? false {
let currentSize = snapshot.bounds.size
ts.append(.scale(x: size.width / currentSize.width, y: size.height / currentSize.height))
} else {
values["bounds.size"] = NSValue(cgSize:size)
}
}
if let position = ts.position {
values["position"] = NSValue(cgPoint:position)
}
if let opacity = ts.opacity, !(snapshot is UIVisualEffectView) {
values["opacity"] = NSNumber(value: opacity)
}
if let cornerRadius = ts.cornerRadius {
values["cornerRadius"] = NSNumber(value: cornerRadius.native)
}
if let backgroundColor = ts.backgroundColor {
values["backgroundColor"] = backgroundColor
}
if let zPosition = ts.zPosition {
values["zPosition"] = NSNumber(value: zPosition.native)
}
if let borderWidth = ts.borderWidth {
values["borderWidth"] = NSNumber(value: borderWidth.native)
}
if let borderColor = ts.borderColor {
values["borderColor"] = borderColor
}
if let masksToBounds = ts.masksToBounds {
values["masksToBounds"] = masksToBounds
}
if ts.displayShadow {
if let shadowColor = ts.shadowColor {
values["shadowColor"] = shadowColor
}
if let shadowRadius = ts.shadowRadius {
values["shadowRadius"] = NSNumber(value: shadowRadius.native)
}
if let shadowOpacity = ts.shadowOpacity {
values["shadowOpacity"] = NSNumber(value: shadowOpacity)
}
if let shadowPath = ts.shadowPath {
values["shadowPath"] = shadowPath
}
if let shadowOffset = ts.shadowOffset {
values["shadowOffset"] = NSValue(cgSize: shadowOffset)
}
}
if let contentsRect = ts.contentsRect {
values["contentsRect"] = NSValue(cgRect: contentsRect)
}
if let contentsScale = ts.contentsScale {
values["contentsScale"] = NSNumber(value: contentsScale.native)
}
if let transform = ts.transform {
values["transform"] = NSValue(caTransform3D: transform)
}
if let (color, opacity) = ts.overlay {
values["overlay.backgroundColor"] = color
values["overlay.opacity"] = NSNumber(value: opacity.native)
}
return values
}
/**
Moves a layer's animation to a given elapsed time.
- Parameter layer: A CALayer.
- Parameter elapsedTime: A TimeInterval.
*/
fileprivate func seek(layer: CALayer, elapsedTime: TimeInterval) {
let timeOffset = elapsedTime - targetState.delay
for (key, anim) in layer.animations {
anim.speed = 0
anim.timeOffset = max(0, min(anim.duration - 0.01, timeOffset))
layer.removeAnimation(forKey: key)
layer.add(anim, forKey: key)
}
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
internal protocol MotionHasInsertOrder: class {
/// A boolean indicating whether to insert the toView first or not.
var insertToViewFirst: Bool { get set }
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
internal class MotionTransitionAnimator<T: MotionAnimatorViewContext>: MotionAnimator, MotionHasInsertOrder {
/// A reference to a MotionContext.
weak public var context: MotionContext!
/// An index of views to their corresponding animator context.
var viewToContexts = [UIView: T]()
var insertToViewFirst = false
}
extension MotionTransitionAnimator {
/**
Animates a given view.
- Parameter view: A UIView.
- Parameter isAppearing: A boolean that determines whether the
view is appearing.
*/
fileprivate func animate(view: UIView, isAppearing: Bool) {
let s = context.snapshotView(for: view)
let v = T(animator: self, snapshot: s, targetState: context[view]!)
viewToContexts[view] = v
v.startAnimations(isAppearing: isAppearing)
}
}
extension MotionTransitionAnimator {
/// Cleans the contexts.
func clean() {
for v in viewToContexts.values {
v.clean()
}
viewToContexts.removeAll()
insertToViewFirst = false
}
/**
A function that determines if a view can be animated.
- Parameter view: A UIView.
- Parameter isAppearing: A boolean that determines whether the
view is appearing.
*/
func canAnimate(view: UIView, isAppearing: Bool) -> Bool {
guard let state = context[view] else {
return false
}
return T.canAnimate(view: view, state: state, isAppearing: isAppearing)
}
/**
Animates the fromViews to the toViews.
- Parameter fromViews: An Array of UIViews.
- Parameter toViews: An Array of UIViews.
- Returns: A TimeInterval.
*/
func animate(fromViews: [UIView], toViews: [UIView]) -> TimeInterval {
var duration: TimeInterval = 0
if insertToViewFirst {
for v in toViews {
animate(view: v, isAppearing: true)
}
for v in fromViews {
animate(view: v, isAppearing: false)
}
} else {
for v in fromViews {
animate(view: v, isAppearing: false)
}
for v in toViews {
animate(view: v, isAppearing: true)
}
}
for v in viewToContexts.values {
duration = max(duration, v.duration)
}
return duration
}
/**
Moves the view's animation to the given elapsed time.
- Parameter to elapsedTime: A TimeInterval.
*/
func seek(to elapsedTime: TimeInterval) {
for v in viewToContexts.values {
v.seek(to: elapsedTime)
}
}
/**
Resumes the animation with a given elapsed time and
optional reversed boolean.
- Parameter at elapsedTime: A TimeInterval.
- Parameter isReversed: A boolean to reverse the animation
or not.
*/
func resume(at elapsedTime: TimeInterval, isReversed: Bool) -> TimeInterval {
var duration: TimeInterval = 0
for (_, v) in viewToContexts {
v.resume(at: elapsedTime, isReversed: isReversed)
duration = max(duration, v.duration)
}
return duration
}
/**
Applies the given state to the given view.
- Parameter state: A MotionTransitionState.
- Parameter to view: A UIView.
*/
func apply(state: MotionTransitionState, to view: UIView) {
guard let v = viewToContexts[view] else {
return
}
v.apply(state: state)
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
@available(iOS 10, tvOS 10, *)
internal class MotionViewPropertyViewContext: MotionAnimatorViewContext {
/// A reference to the UIViewPropertyAnimator.
fileprivate var viewPropertyAnimator: UIViewPropertyAnimator?
override class func canAnimate(view: UIView, state: MotionTransitionState, isAppearing: Bool) -> Bool {
return view is UIVisualEffectView && nil != state.opacity
}
override func resume(at elapsedTime: TimeInterval, isReversed: Bool) {
viewPropertyAnimator?.finishAnimation(at: isReversed ? .start : .end)
}
override func seek(to elapsedTime: TimeInterval) {
viewPropertyAnimator?.pauseAnimation()
viewPropertyAnimator?.fractionComplete = CGFloat(elapsedTime / duration)
}
override func clean() {
super.clean()
viewPropertyAnimator?.stopAnimation(true)
viewPropertyAnimator = nil
}
override func startAnimations(isAppearing: Bool) {
guard let v = snapshot as? UIVisualEffectView else {
return
}
let appearedEffect = v.effect
let disappearedEffect = 0 == targetState.opacity ? nil : v.effect
v.effect = isAppearing ? disappearedEffect : appearedEffect
duration = targetState.duration!
viewPropertyAnimator = UIViewPropertyAnimator(duration: duration, curve: .easeInOut) {
v.effect = isAppearing ? appearedEffect : disappearedEffect
}
viewPropertyAnimator?.startAnimation()
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
internal extension Array {
/**
Retrieves the element at the given index if it exists.
- Parameter _ index: An Int.
- Returns: An optional Element value.
*/
func get(_ index: Int) -> Element? {
if index < count {
return self[index]
}
return nil
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
@available(iOS 10, *)
extension CALayer: CAAnimationDelegate {}
internal extension CALayer {
/// Retrieves all currently running animations for the layer.
var animations: [(String, CAAnimation)] {
guard let keys = animationKeys() else {
return []
}
return keys.map {
return ($0, self.animation(forKey: $0)!.copy() as! CAAnimation)
}
}
}
public extension CALayer {
/**
A function that accepts CAAnimation objects and executes them on the
view's backing layer.
- Parameter animation: A CAAnimation instance.
*/
func animate(_ animations: CAAnimation...) {
animate(animations)
}
/**
A function that accepts CAAnimation objects and executes them on the
view's backing layer.
- Parameter animation: A CAAnimation instance.
*/
func animate(_ animations: [CAAnimation]) {
for animation in animations {
if nil == animation.delegate {
animation.delegate = self
}
if let a = animation as? CABasicAnimation {
a.fromValue = (presentation() ?? self).value(forKeyPath: a.keyPath!)
}
if let a = animation as? CAPropertyAnimation {
add(a, forKey: a.keyPath!)
} else if let a = animation as? CAAnimationGroup {
add(a, forKey: nil)
} else if let a = animation as? CATransition {
add(a, forKey: kCATransition)
}
}
}
/**
Executed when an animation has started.
- Parameter _ anim: A CAAnimation.
*/
func animationDidStart(_ anim: CAAnimation) {}
/**
A delegation function that is executed when the backing layer stops
running an animation.
- Parameter animation: The CAAnimation instance that stopped running.
- Parameter flag: A boolean that indicates if the animation stopped
because it was completed or interrupted. True if completed, false
if interrupted.
*/
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
guard let a = anim as? CAPropertyAnimation else {
if let a = (anim as? CAAnimationGroup)?.animations {
for x in a {
animationDidStop(x, finished: true)
}
}
return
}
guard let b = a as? CABasicAnimation else {
return
}
guard let v = b.toValue else {
return
}
guard let k = b.keyPath else {
return
}
setValue(v, forKeyPath: k)
removeAnimation(forKey: k)
}
/**
A function that accepts a list of MotionAnimation values and executes them.
- Parameter animations: A list of MotionAnimation values.
*/
func animate(_ animations: MotionAnimation...) {
animate(animations)
}
/**
A function that accepts an Array of MotionAnimation values and executes them.
- Parameter animations: An Array of MotionAnimation values.
- Parameter completion: An optional completion block.
*/
func animate(_ animations: [MotionAnimation], completion: (() -> Void)? = nil) {
startAnimations(animations, completion: completion)
}
}
fileprivate extension CALayer {
/**
A function that executes an Array of MotionAnimation values.
- Parameter _ animations: An Array of MotionAnimations.
- Parameter completion: An optional completion block.
*/
func startAnimations(_ animations: [MotionAnimation], completion: (() -> Void)? = nil) {
let ts = MotionAnimationState(animations: animations)
Motion.delay(ts.delay) { [weak self,
ts = ts,
completion = completion] in
guard let s = self else {
return
}
var anims = [CABasicAnimation]()
var duration = 0 == ts.duration ? 0.01 : ts.duration
if let v = ts.backgroundColor {
let a = MotionCAAnimation.background(color: UIColor(cgColor: v))
a.fromValue = s.backgroundColor
anims.append(a)
}
if let v = ts.borderColor {
let a = MotionCAAnimation.border(color: UIColor(cgColor: v))
a.fromValue = s.borderColor
anims.append(a)
}
if let v = ts.borderWidth {
let a = MotionCAAnimation.border(width: v)
a.fromValue = NSNumber(floatLiteral: Double(s.borderWidth))
anims.append(a)
}
if let v = ts.cornerRadius {
let a = MotionCAAnimation.corner(radius: v)
a.fromValue = NSNumber(floatLiteral: Double(s.cornerRadius))
anims.append(a)
}
if let v = ts.transform {
let a = MotionCAAnimation.transform(v)
a.fromValue = NSValue(caTransform3D: s.transform)
anims.append(a)
}
if let v = ts.spin {
var a = MotionCAAnimation.spin(x: v.x)
a.fromValue = NSNumber(floatLiteral: 0)
anims.append(a)
a = MotionCAAnimation.spin(y: v.y)
a.fromValue = NSNumber(floatLiteral: 0)
anims.append(a)
a = MotionCAAnimation.spin(z: v.z)
a.fromValue = NSNumber(floatLiteral: 0)
anims.append(a)
}
if let v = ts.position {
let a = MotionCAAnimation.position(v)
a.fromValue = NSValue(cgPoint: s.position)
anims.append(a)
}
if let v = ts.opacity {
let a = MotionCAAnimation.fade(v)
a.fromValue = s.value(forKeyPath: MotionAnimationKeyPath.opacity.rawValue) ?? NSNumber(floatLiteral: 1)
anims.append(a)
}
if let v = ts.zPosition {
let a = MotionCAAnimation.zPosition(v)
a.fromValue = s.value(forKeyPath: MotionAnimationKeyPath.zPosition.rawValue) ?? NSNumber(floatLiteral: 0)
anims.append(a)
}
if let v = ts.size {
let a = MotionCAAnimation.size(v)
a.fromValue = NSValue(cgSize: s.bounds.size)
anims.append(a)
}
if let v = ts.shadowPath {
let a = MotionCAAnimation.shadow(path: v)
a.fromValue = s.shadowPath
anims.append(a)
}
if let v = ts.shadowColor {
let a = MotionCAAnimation.shadow(color: UIColor(cgColor: v))
a.fromValue = s.shadowColor
anims.append(a)
}
if let v = ts.shadowOffset {
let a = MotionCAAnimation.shadow(offset: v)
a.fromValue = NSValue(cgSize: s.shadowOffset)
anims.append(a)
}
if let v = ts.shadowOpacity {
let a = MotionCAAnimation.shadow(opacity: v)
a.fromValue = NSNumber(floatLiteral: Double(s.shadowOpacity))
anims.append(a)
}
if let v = ts.shadowRadius {
let a = MotionCAAnimation.shadow(radius: v)
a.fromValue = NSNumber(floatLiteral: Double(s.shadowRadius))
anims.append(a)
}
if #available(iOS 9.0, *), let (stiffness, damping) = ts.spring {
for i in 0..<anims.count where nil != anims[i].keyPath {
let v = anims[i]
guard "cornerRadius" != v.keyPath else {
continue
}
let a = MotionCAAnimation.convert(animation: v, stiffness: stiffness, damping: damping)
anims[i] = a
if a.settlingDuration > duration {
duration = a.settlingDuration
}
}
}
let g = Motion.animate(group: anims, duration: duration)
g.fillMode = MotionAnimationFillModeToValue(mode: .both)
g.isRemovedOnCompletion = false
g.timingFunction = ts.timingFunction
s.animate(g)
if let v = ts.completion {
Motion.delay(duration, execute: v)
}
if let v = completion {
Motion.delay(duration, execute: v)
}
}
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
public extension CAMediaTimingFunction {
// Default
static let linear = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
static let easeIn = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
static let easeOut = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
static let easeInOut = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
// Material
static let standard = CAMediaTimingFunction(controlPoints: 0.4, 0.0, 0.2, 1.0)
static let deceleration = CAMediaTimingFunction(controlPoints: 0.0, 0.0, 0.2, 1)
static let acceleration = CAMediaTimingFunction(controlPoints: 0.4, 0.0, 1, 1)
static let sharp = CAMediaTimingFunction(controlPoints: 0.4, 0.0, 0.6, 1)
// Easing.net
static let easeOutBack = CAMediaTimingFunction(controlPoints: 0.175, 0.885, 0.32, 1.75)
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import MetalKit
extension CGSize {
/// THe center point based on width and height.
var center: CGPoint {
return CGPoint(x: width / 2, y: height / 2)
}
/// Top left point based on the size.
var topLeft: CGPoint {
return .zero
}
/// Top right point based on the size.
var topRight: CGPoint {
return CGPoint(x: width, y: 0)
}
/// Bottom left point based on the size.
var bottomLeftPoint: CGPoint {
return CGPoint(x: 0, y: height)
}
/// Bottom right point based on the size.
var bottomRight: CGPoint {
return CGPoint(x: width, y: height)
}
/**
Retrieves the size based on a given CGAffineTransform.
- Parameter _ t: A CGAffineTransform.
- Returns: A CGSize.
*/
func transform(_ t: CGAffineTransform) -> CGSize {
return applying(t)
}
/**
Retrieves the size based on a given CATransform3D.
- Parameter _ t: A CGAffineTransform.
- Returns: A CGSize.
*/
func transform(_ t: CATransform3D) -> CGSize {
return applying(CATransform3DGetAffineTransform(t))
}
}
extension CGRect {
/// A center point based on the origin and size values.
var center: CGPoint {
return CGPoint(x: origin.x + size.width / 2, y: origin.y + size.height / 2)
}
/// The bounding box size based from from the frame's rect.
var bounds: CGRect {
return CGRect(origin: .zero, size: size)
}
/**
An initializer with a given point and size.
- Parameter center: A CGPoint.
- Parameter size: A CGSize.
*/
init(center: CGPoint, size: CGSize) {
self.init(x: center.x - size.width / 2, y: center.y - size.height / 2, width: size.width, height: size.height)
}
}
extension CGFloat {
/**
Calculates the limiting position to an area.
- Parameter _ a: A CGFloat.
- Parameter _ b: A CGFloat.
- Returns: A CGFloat.
*/
func clamp(_ a: CGFloat, _ b: CGFloat) -> CGFloat {
return self < a ? a : self > b ? b : self
}
}
extension CGPoint {
/**
Calculates a translation point based on the origin value.
- Parameter _ dx: A CGFloat.
- Parameter _ dy: A CGFloat.
- Returns: A CGPoint.
*/
func translate(_ dx: CGFloat, dy: CGFloat) -> CGPoint {
return CGPoint(x: x + dx, y: y + dy)
}
/**
Calculates a transform point based on a given CGAffineTransform.
- Parameter _ t: CGAffineTransform.
- Returns: A CGPoint.
*/
func transform(_ t: CGAffineTransform) -> CGPoint {
return applying(t)
}
/**
Calculates a transform point based on a given CATransform3D.
- Parameter _ t: CATransform3D.
- Returns: A CGPoint.
*/
func transform(_ t: CATransform3D) -> CGPoint {
return applying(CATransform3DGetAffineTransform(t))
}
/**
Calculates the distance between the CGPoint and given CGPoint.
- Parameter _ b: A CGPoint.
- Returns: A CGFloat.
*/
func distance(_ b: CGPoint) -> CGFloat {
return sqrt(pow(x - b.x, 2) + pow(y - b.y, 2))
}
}
/**
A handler for the (+) operator.
- Parameter left: A CGPoint.
- Parameter right: A CGPoint.
- Returns: A CGPoint.
*/
func +(left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x + right.x, y: left.y + right.y)
}
/**
A handler for the (-) operator.
- Parameter left: A CGPoint.
- Parameter right: A CGPoint.
- Returns: A CGPoint.
*/
func -(left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x - right.x, y: left.y - right.y)
}
/**
A handler for the (/) operator.
- Parameter left: A CGPoint.
- Parameter right: A CGFloat.
- Returns: A CGPoint.
*/
func /(left: CGPoint, right: CGFloat) -> CGPoint {
return CGPoint(x: left.x / right, y: left.y / right)
}
/**
A handler for the (/) operator.
- Parameter left: A CGPoint.
- Parameter right: A CGPoint.
- Returns: A CGPoint.
*/
func /(left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x / right.x, y: left.y / right.y)
}
/**
A handler for the (/) operator.
- Parameter left: A CGSize.
- Parameter right: A CGSize.
- Returns: A CGSize.
*/
func /(left: CGSize, right: CGSize) -> CGSize {
return CGSize(width: left.width / right.width, height: left.height / right.height)
}
/**
A handler for the (*) operator.
- Parameter left: A CGPoint.
- Parameter right: A CGFloat.
- Returns: A CGPoint.
*/
func *(left: CGPoint, right: CGFloat) -> CGPoint {
return CGPoint(x: left.x * right, y: left.y * right)
}
/**
A handler for the (*) operator.
- Parameter left: A CGPoint.
- Parameter right: A CGSize.
- Returns: A CGPoint.
*/
func *(left: CGPoint, right: CGSize) -> CGPoint {
return CGPoint(x: left.x * right.width, y: left.y * right.width)
}
/**
A handler for the (*) operator.
- Parameter left: A CGFloat.
- Parameter right: A CGPoint.
- Returns: A CGPoint.
*/
func *(left: CGFloat, right: CGPoint) -> CGPoint {
return right * left
}
/**
A handler for the (*) operator.
- Parameter left: A CGPoint.
- Parameter right: A CGPoint.
- Returns: A CGPoint.
*/
func *(left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x * right.x, y: left.y * right.y)
}
/**
A handler for the (*) prefix.
- Parameter left: A CGSize.
- Parameter right: A CGFloat.
- Returns: A CGSize.
*/
func *(left: CGSize, right: CGFloat) -> CGSize {
return CGSize(width: left.width * right, height: left.height * right)
}
/**
A handler for the (*) prefix.
- Parameter left: A CGSize.
- Parameter right: A CGSize.
- Returns: A CGSize.
*/
func *(left: CGSize, right: CGSize) -> CGSize {
return CGSize(width: left.width * right.width, height: left.height * right.width)
}
/**
A handler for the (==) operator.
- Parameter lhs: A CATransform3D.
- Parameter rhs: A CATransform3D.
- Returns: A Bool.
*/
func ==(lhs: CATransform3D, rhs: CATransform3D) -> Bool {
var lhs = lhs
var rhs = rhs
return memcmp(&lhs, &rhs, MemoryLayout<CATransform3D>.size) == 0
}
/**
A handler for the (!=) operator.
- Parameter lhs: A CATransform3D.
- Parameter rhs: A CATransform3D.
- Returns: A Bool.
*/
func !=(lhs: CATransform3D, rhs: CATransform3D) -> Bool {
return !(lhs == rhs)
}
/**
A handler for the (-) prefix.
- Parameter point: A CGPoint.
- Returns: A CGPoint.
*/
prefix func -(point: CGPoint) -> CGPoint {
return CGPoint.zero - point
}
/**
A handler for the (abs) function.
- Parameter _ p: A CGPoint.
- Returns: A CGPoint.
*/
func abs(_ p: CGPoint) -> CGPoint {
return CGPoint(x: abs(p.x), y: abs(p.y))
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
internal struct AssociatedObject {
/**
Gets the Obj-C reference for the instance object within the UIView extension.
- Parameter base: Base object.
- Parameter key: Memory key pointer.
- Parameter initializer: Object initializer.
- Returns: The associated reference for the initializer object.
*/
static func get<T: Any>(base: Any, key: UnsafePointer<UInt8>, initializer: () -> T) -> T {
if let v = objc_getAssociatedObject(base, key) as? T {
return v
}
let v = initializer()
objc_setAssociatedObject(base, key, v, .OBJC_ASSOCIATION_RETAIN)
return v
}
/**
Sets the Obj-C reference for the instance object within the UIView extension.
- Parameter base: Base object.
- Parameter key: Memory key pointer.
- Parameter value: The object instance to set for the associated object.
- Returns: The associated reference for the initializer object.
*/
static func set<T: Any>(base: Any, key: UnsafePointer<UInt8>, value: T) {
objc_setAssociatedObject(base, key, value, .OBJC_ASSOCIATION_RETAIN)
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
fileprivate let parameterRegex = "(?:\\-?\\d+(\\.?\\d+)?)|\\w+"
fileprivate let transitionsRegex = "(\\w+)(?:\\(([^\\)]*)\\))?"
internal extension NSObject {
/// Copies an object using NSKeyedArchiver.
func copyWithArchiver() -> Any? {
return NSKeyedUnarchiver.unarchiveObject(with: NSKeyedArchiver.archivedData(withRootObject: self))!
}
}
internal extension UIColor {
/// A tuple of the rgba components.
var components: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) {
var r: CGFloat = 0
var g: CGFloat = 0
var b: CGFloat = 0
var a: CGFloat = 0
getRed(&r, green: &g, blue: &b, alpha: &a)
return (r, g, b, a)
}
/// The alpha component value.
var alphaComponent: CGFloat {
return components.a
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
fileprivate var AssociatedInstanceKey: UInt8 = 0
fileprivate struct AssociatedInstance {
/// A boolean indicating whether Motion is enabled.
fileprivate var isEnabled: Bool
/// An optional reference to the motion identifier.
fileprivate var identifier: String?
/// An optional reference to the motion animations.
fileprivate var animations: [MotionAnimation]?
/// An optional reference to the motion transition animations.
fileprivate var transitions: [MotionTransition]?
/// An alpha value.
fileprivate var alpha: CGFloat?
}
fileprivate extension UIView {
/// AssociatedInstance reference.
fileprivate var associatedInstance: AssociatedInstance {
get {
return AssociatedObject.get(base: self, key: &AssociatedInstanceKey) {
return AssociatedInstance(isEnabled: true, identifier: nil, animations: nil, transitions: nil, alpha: 1)
}
}
set(value) {
AssociatedObject.set(base: self, key: &AssociatedInstanceKey, value: value)
}
}
}
public extension UIView {
/// A boolean that indicates whether motion is enabled.
@IBInspectable
var isMotionEnabled: Bool {
get {
return associatedInstance.isEnabled
}
set(value) {
associatedInstance.isEnabled = value
}
}
/// An identifier value used to connect views across UIViewControllers.
@IBInspectable
var motionIdentifier: String? {
get {
return associatedInstance.identifier
}
set(value) {
associatedInstance.identifier = value
}
}
/**
A function that accepts CAAnimation objects and executes them on the
view's backing layer.
- Parameter animations: A list of CAAnimations.
*/
func animate(_ animations: CAAnimation...) {
layer.animate(animations)
}
/**
A function that accepts an Array of CAAnimation objects and executes
them on the view's backing layer.
- Parameter animations: An Array of CAAnimations.
*/
func animate(_ animations: [CAAnimation]) {
layer.animate(animations)
}
/**
A function that accepts a list of MotionAnimation values and executes
them on the view's backing layer.
- Parameter animations: A list of MotionAnimation values.
*/
func animate(_ animations: MotionAnimation...) {
layer.animate(animations)
}
/**
A function that accepts an Array of MotionAnimation values and executes
them on the view's backing layer.
- Parameter animations: An Array of MotionAnimation values.
- Parameter completion: An optional completion block.
*/
func animate(_ animations: [MotionAnimation], completion: (() -> Void)? = nil) {
layer.animate(animations, completion: completion)
}
/**
A function that accepts a list of MotionTransition values.
- Parameter transitions: A list of MotionTransition values.
*/
func transition(_ transitions: MotionTransition...) {
transition(transitions)
}
/**
A function that accepts an Array of MotionTransition values.
- Parameter transitions: An Array of MotionTransition values.
*/
func transition(_ transitions: [MotionTransition]) {
motionTransitions = transitions
}
}
internal extension UIView {
/// The animations to run while in transition.
var motionTransitions: [MotionTransition]? {
get {
return associatedInstance.transitions
}
set(value) {
associatedInstance.transitions = value
}
}
/// The animations to run while in transition.
var motionAlpha: CGFloat? {
get {
return associatedInstance.alpha
}
set(value) {
associatedInstance.alpha = value
}
}
/// Computes the rotate of the view.
var motionRotationAngle: CGFloat {
get {
return CGFloat(atan2f(Float(transform.b), Float(transform.a))) * 180 / CGFloat(Double.pi)
}
set(value) {
transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi) * value / 180)
}
}
/// The global position of a view.
var motionPosition: CGPoint {
return superview?.convert(layer.position, to: nil) ?? layer.position
}
/// The layer.transform of a view.
var motionTransform: CATransform3D {
get {
return layer.transform
}
set(value) {
layer.transform = value
}
}
/// Computes the scale X axis value of the view.
var motionScaleX: CGFloat {
return transform.a
}
/// Computes the scale Y axis value of the view.
var motionScaleY: CGFloat {
return transform.b
}
}
internal extension UIView {
/// Retrieves a single Array of UIViews that are in the view hierarchy.
var flattenedViewHierarchy: [UIView] {
guard isMotionEnabled else {
return []
}
if #available(iOS 9.0, *) {
return isHidden && (superview is UICollectionView || superview is UIStackView || self is UITableViewCell) ? [] : ([self] + subviews.flatMap { $0.flattenedViewHierarchy })
}
return isHidden && (superview is UICollectionView || self is UITableViewCell) ? [] : ([self] + subviews.flatMap { $0.flattenedViewHierarchy })
}
/**
Retrieves the optimized duration based on the given parameters.
- Parameter fromPosition: A CGPoint.
- Parameter toPosition: An optional CGPoint.
- Parameter size: An optional CGSize.
- Parameter transform: An optional CATransform3D.
- Returns: A TimeInterval.
*/
func optimizedDuration(fromPosition: CGPoint, toPosition: CGPoint?, size: CGSize?, transform: CATransform3D?) -> TimeInterval {
let toPos = toPosition ?? fromPosition
let fromSize = (layer.presentation() ?? layer).bounds.size
let toSize = size ?? fromSize
let fromTransform = (layer.presentation() ?? layer).transform
let toTransform = transform ?? fromTransform
let realFromPos = CGPoint.zero.transform(fromTransform) + fromPosition
let realToPos = CGPoint.zero.transform(toTransform) + toPos
let realFromSize = fromSize.transform(fromTransform)
let realToSize = toSize.transform(toTransform)
let movePoints = realFromPos.distance(realToPos) + realFromSize.bottomRight.distance(realToSize.bottomRight)
// duration is 0.2 @ 0 to 0.375 @ 500
return 0.208 + Double(movePoints.clamp(0, 500)) / 3000
}
/**
Takes a snapshot of a view usinag the UI graphics context.
- Returns: A UIView with an embedded UIImageView.
*/
func slowSnapshotView() -> UIView {
UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0)
layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let imageView = UIImageView(image: image)
imageView.frame = bounds
let snapshotView = UIView(frame: bounds)
snapshotView.addSubview(imageView)
return snapshotView
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
fileprivate var AssociatedInstanceKey: UInt8 = 0
fileprivate struct AssociatedInstance {
/// A reference to the modal animation.
var modalTransitionType: MotionTransitionType
/// A reference to the navigation animation.
var navigationTransitionType: MotionTransitionType
/// A reference to the tabBar animation.
var tabBarTransitionType: MotionTransitionType
/// A reference to the stored snapshot.
var storedSnapshot: UIView?
/**
A reference to the previous navigation controller delegate
before Motion was enabled.
*/
var previousNavigationDelegate: UINavigationControllerDelegate?
/**
A reference to the previous tab bar controller delegate
before Motion was enabled.
*/
var previousTabBarDelegate: UITabBarControllerDelegate?
}
extension UIViewController {
/// AssociatedInstance reference.
fileprivate var associatedInstance: AssociatedInstance {
get {
return AssociatedObject.get(base: self, key: &AssociatedInstanceKey) {
return AssociatedInstance(modalTransitionType: .auto,
navigationTransitionType: .auto,
tabBarTransitionType: .auto,
storedSnapshot: nil,
previousNavigationDelegate: nil,
previousTabBarDelegate: nil)
}
}
set(value) {
AssociatedObject.set(base: self, key: &AssociatedInstanceKey, value: value)
}
}
/// default motion animation type for presenting & dismissing modally
public var motionModalTransitionType: MotionTransitionType {
get {
return associatedInstance.modalTransitionType
}
set(value) {
associatedInstance.modalTransitionType = value
}
}
/// used for .overFullScreen presentation
internal var motionStoredSnapshot: UIView? {
get {
return associatedInstance.storedSnapshot
}
set(value) {
associatedInstance.storedSnapshot = value
}
}
/**
A reference to the previous navigation controller delegate
before Motion was enabled.
*/
internal var previousNavigationDelegate: UINavigationControllerDelegate? {
get {
return associatedInstance.previousNavigationDelegate
}
set(value) {
associatedInstance.previousNavigationDelegate = value
}
}
/**
A reference to the previous tab bar controller delegate
before Motion was enabled.
*/
internal var previousTabBarDelegate: UITabBarControllerDelegate? {
get {
return associatedInstance.previousTabBarDelegate
}
set(value) {
associatedInstance.previousTabBarDelegate = value
}
}
///A boolean that indicates whether Motion is enabled or disabled.
@IBInspectable
public var isMotionEnabled: Bool {
get {
return transitioningDelegate is Motion
}
set(value) {
guard value != isMotionEnabled else {
return
}
if value {
transitioningDelegate = Motion.shared
if let v = self as? UINavigationController {
previousNavigationDelegate = v.delegate
v.delegate = Motion.shared
}
if let v = self as? UITabBarController {
previousTabBarDelegate = v.delegate
v.delegate = Motion.shared
}
} else {
transitioningDelegate = nil
if let v = self as? UINavigationController, v.delegate is Motion {
v.delegate = previousNavigationDelegate
}
if let v = self as? UITabBarController, v.delegate is Motion {
v.delegate = previousTabBarDelegate
}
}
}
}
}
extension UINavigationController {
/// Default motion animation type for push and pop within the navigation controller.
public var motionNavigationTransitionType: MotionTransitionType {
get {
return associatedInstance.navigationTransitionType
}
set(value) {
associatedInstance.navigationTransitionType = value
}
}
}
extension UITabBarController {
/// Default motion animation type for switching tabs within the tab bar controller.
public var motionTabBarTransitionType: MotionTransitionType {
get {
return associatedInstance.tabBarTransitionType
}
set(value) {
associatedInstance.tabBarTransitionType = value
}
}
}
extension UIViewController {
/**
Dismiss the current view controller with animation. Will perform a
navigationController.popViewController if the current view controller
is contained inside a navigationController
*/
@IBAction
public func motionDismissViewController() {
if let v = navigationController, self != v.viewControllers.first {
v.popViewController(animated: true)
} else {
dismiss(animated: true)
}
}
/// Unwind to the root view controller using Motion.
@IBAction
public func motionUnwindToRootViewController() {
motionUnwindToViewController { $0.presentingViewController == nil }
}
/// Unwind to a specific view controller using Motion.
public func motionUnwindToViewController(_ toViewController: UIViewController) {
motionUnwindToViewController { $0 == toViewController }
}
/// Unwind to a view controller that responds to the given selector using Motion.
public func motionUnwindToViewController(withSelector: Selector) {
motionUnwindToViewController { $0.responds(to: withSelector) }
}
/// Unwind to a view controller with given class using Motion
public func motionUnwindToViewController(withClass: AnyClass) {
motionUnwindToViewController { $0.isKind(of: withClass) }
}
/// Unwind to a view controller that the matchBlock returns true on.
public func motionUnwindToViewController(withMatchBlock: (UIViewController) -> Bool) {
var target: UIViewController?
var current: UIViewController? = self
while nil == target && nil != current {
if let childViewControllers = (current as? UINavigationController)?.childViewControllers ?? current!.navigationController?.childViewControllers {
for v in childViewControllers.reversed() {
if self != v, withMatchBlock(v) {
target = v
break
}
}
}
guard nil == target else {
continue
}
current = current?.presentingViewController
guard let v = current, withMatchBlock(v) else {
continue
}
target = v
}
guard let v = target else {
return
}
guard nil != v.presentedViewController else {
v.navigationController?.popToViewController(v, animated: true)
return
}
v.navigationController?.popToViewController(v, animated: false)
let fromVC = navigationController ?? self
let toVC = v.navigationController ?? v
if v.presentedViewController != fromVC {
/**
UIKit's UIViewController.dismiss will jump to target.presentedViewController then perform the dismiss.
We overcome this behavior by inserting a snapshot into target.presentedViewController
And also force Motion to use the current view controller as the fromViewController.
*/
Motion.shared.fromViewController = fromVC
guard let snapshot = fromVC.view.snapshotView(afterScreenUpdates: true) else {
return
}
toVC.presentedViewController?.view.addSubview(snapshot)
}
toVC.dismiss(animated: true)
}
/**
Replace the current view controller with another view controller within the
navigation/modal stack.
- Parameter with next: A UIViewController.
*/
public func motionReplaceViewController(with next: UIViewController) {
guard !Motion.shared.isTransitioning else {
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.")
return
}
if let nc = navigationController {
var v = nc.childViewControllers
if !v.isEmpty {
v.removeLast()
v.append(next)
}
if nc.isMotionEnabled {
Motion.shared.forceNonInteractive = true
}
nc.setViewControllers(v, animated: true)
} else if let container = view.superview {
let presentingVC = presentingViewController
Motion.shared.transition(from: self, to: next, in: container) { [weak self] (isFinished) in
guard isFinished else {
return
}
UIApplication.shared.keyWindow?.addSubview(next.view)
guard let pvc = presentingVC else {
UIApplication.shared.keyWindow?.rootViewController = next
return
}
self?.dismiss(animated: false) {
pvc.present(next, animated: false)
}
}
}
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
@objc(MotionAnimationFillMode)
public enum MotionAnimationFillMode: Int {
case forwards
case backwards
case both
case removed
}
/**
Converts the MotionAnimationFillMode enum value to a corresponding String.
- Parameter mode: An MotionAnimationFillMode enum value.
*/
public func MotionAnimationFillModeToValue(mode: MotionAnimationFillMode) -> String {
switch mode {
case .forwards:
return kCAFillModeForwards
case .backwards:
return kCAFillModeBackwards
case .both:
return kCAFillModeBoth
case .removed:
return kCAFillModeRemoved
}
}
The MIT License (MIT)
Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
@objc(MotionViewControllerDelegate)
public protocol MotionViewControllerDelegate {
/**
An optional delegation method that is executed motion will start the transition.
- Parameter motion: A Motion instance.
- Parameter willStartTransitionFrom viewController: A UIViewController.
*/
@objc
optional func motionWillStartTransition(motion: Motion)
/**
An optional delegation method that is executed motion did end the transition.
- Parameter motion: A Motion instance.
- Parameter willStartTransitionFrom viewController: A UIViewController.
*/
@objc
optional func motionDidEndTransition(motion: Motion)
/**
An optional delegation method that is executed motion did cancel the transition.
- Parameter motion: A Motion instance.
- Parameter willStartTransitionFrom viewController: A UIViewController.
*/
@objc
optional func motionDidCancelTransition(motion: Motion)
/**
An optional delegation method that is executed when the source
view controller will start the transition.
- Parameter motion: A Motion instance.
- Parameter willStartTransitionFrom viewController: A UIViewController.
*/
@objc
optional func motion(motion: Motion, willStartTransitionFrom viewController: UIViewController)
/**
An optional delegation method that is executed when the source
view controller did end the transition.
- Parameter motion: A Motion instance.
- Parameter willStartTransitionFrom viewController: A UIViewController.
*/
@objc
optional func motion(motion: Motion, didEndTransitionFrom viewController: UIViewController)
/**
An optional delegation method that is executed when the source
view controller did cancel the transition.
- Parameter motion: A Motion instance.
- Parameter willStartTransitionFrom viewController: A UIViewController.
*/
@objc
optional func motion(motion: Motion, didCancelTransitionFrom viewController: UIViewController)
/**
An optional delegation method that is executed when the destination
view controller will start the transition.
- Parameter motion: A Motion instance.
- Parameter willStartTransitionFrom viewController: A UIViewController.
*/
@objc
optional func motion(motion: Motion, willStartTransitionTo viewController: UIViewController)
/**
An optional delegation method that is executed when the destination
view controller did end the transition.
- Parameter motion: A Motion instance.
- Parameter willStartTransitionFrom viewController: A UIViewController.
*/
@objc
optional func motion(motion: Motion, didEndTransitionTo viewController: UIViewController)
/**
An optional delegation method that is executed when the destination
view controller did cancel the transition.
- Parameter motion: A Motion instance.
- Parameter willStartTransitionFrom viewController: A UIViewController.
*/
@objc
optional func motion(motion: Motion, didCancelTransitionTo viewController: UIViewController)
}
/**
### The singleton class/object for controlling interactive transitions.
```swift
Motion.shared
```
#### Use the following methods for controlling the interactive transition:
```swift
func update(progress:Double)
func end()
func cancel()
func apply(transitions: [MotionTransition], to view: UIView)
```
*/
public class Motion: MotionController {
/// Shared singleton object for controlling the transition
public static let shared = Motion()
/// Source view controller.
public internal(set) var fromViewController: UIViewController?
/// Destination view controller.
public internal(set) var toViewController: UIViewController?
/// Whether or not we are presenting the destination view controller.
public internal(set) var isPresenting = true
/// Progress of the current transition, 0 if a transition is not happening.
public override var elapsedTime: TimeInterval {
didSet {
guard isTransitioning else {
return
}
transitionContext?.updateInteractiveTransition(CGFloat(elapsedTime))
}
}
/// Indicates whether the transition is animating or not.
public var isAnimating = false
/**
A UIViewControllerContextTransitioning object provided by UIKit, which
might be nil when isTransitioning. This happens when calling motionReplaceViewController
*/
internal weak var transitionContext: UIViewControllerContextTransitioning?
/// A reference to a fullscreen snapshot.
internal var fullScreenSnapshot: UIView!
/// Default animation type.
internal var defaultAnimation = MotionTransitionType.auto
/// The color of the transitioning container.
internal var containerBackgroundColor: UIColor?
/**
By default, Motion will always appear to be interactive to UIKit. This forces it to appear non-interactive.
Used when doing a motionReplaceViewController within a UINavigationController, to fix a bug with
UINavigationController.setViewControllers not able to handle interactive transitions.
*/
internal var forceNonInteractive = false
/// Inserts the toViews first.
internal var insertToViewFirst = false
/// Indicates whether a UINavigationController is transitioning.
internal var isNavigationController = false
/// Indicates whether a UITabBarController is transitioning.
internal var isTabBarController = false
/// Indicates whether a UINavigationController or UITabBarController is transitioning.
internal var isContainerController: Bool {
return isNavigationController || isTabBarController
}
/// Indicates whether the from view controller is full screen.
internal var fromOverFullScreen: Bool {
guard let v = fromViewController else {
return false
}
return !isContainerController && (.overFullScreen == v.modalPresentationStyle || .overCurrentContext == v.modalPresentationStyle)
}
/// Indicates whether the to view controller is full screen.
internal var toOverFullScreen: Bool {
guard let v = toViewController else {
return false
}
return !isContainerController && (.overFullScreen == v.modalPresentationStyle || .overCurrentContext == v.modalPresentationStyle)
}
/// A reference to the fromView, fromViewController.view.
internal var fromView: UIView? {
return fromViewController?.view
}
/// A reference to the toView, toViewController.view.
internal var toView: UIView? {
return toViewController?.view
}
/// An initializer.
internal override init() {
super.init()
}
}
public extension Motion {
/// Turn off built-in animations for the next transition.
func disableDefaultAnimationForNextTransition() {
defaultAnimation = .none
}
/**
Set the default animation for the next transition. This may override the
root-view's motionTransitions during the transition.
- Parameter animation: A MotionTransitionType.
*/
func setAnimationForNextTransition(_ animation: MotionTransitionType) {
defaultAnimation = animation
}
/**
Set the container background color for the next transition.
- Parameter _ color: An optional UIColor.
*/
func setContainerBackgroundColorForNextTransition(_ color: UIColor?) {
containerBackgroundColor = color
}
}
fileprivate extension Motion {
/// Starts the transition animation.
func start() {
guard isTransitioning else {
return
}
prepareViewControllers()
prepareSnapshotView()
prepareTransition()
prepareContext()
prepareToView()
prepareViewHierarchy()
processContext()
prepareTransitionPairs()
processForAnimation()
}
}
internal extension Motion {
override func animate() {
guard let tv = toView else {
return
}
context.unhide(view: tv)
updateContainerBackgroundColor()
updateInsertOrder()
super.animate()
fullScreenSnapshot?.removeFromSuperview()
}
override func complete(isFinished: Bool) {
guard isTransitioning else {
return
}
guard let c = container else {
return
}
guard let tc = transitionContainer else {
return
}
guard let fv = fromView else {
return
}
guard let tv = toView else {
return
}
context.clean()
if isFinished && isPresenting && toOverFullScreen {
// finished presenting a overFullScreen view controller.
context.unhide(rootView: tv)
context.removeSnapshots(rootView: tv)
context.storeViewAlpha(rootView: fv)
fromViewController!.motionStoredSnapshot = container
fv.removeFromSuperview()
fv.addSubview(c)
} else if !isFinished && !isPresenting && fromOverFullScreen {
// Cancelled dismissing a overFullScreen view controller.
context.unhide(rootView: fv)
context.removeSnapshots(rootView: fv)
context.storeViewAlpha(rootView: tv)
toViewController!.motionStoredSnapshot = container
tv.removeFromSuperview()
tv.addSubview(c)
} else {
context.unhideAll()
context.removeAllSnapshots()
c.removeFromSuperview()
}
// Move fromView & toView back from our container back to the one supplied by UIKit.
if (toOverFullScreen && isFinished) || (fromOverFullScreen && !isFinished) {
tc.addSubview(isFinished ? fv : tv)
}
tc.addSubview(isFinished ? tv : fv)
if isPresenting != isFinished, !isContainerController {
// Only happens when present a .overFullScreen view controller.
// bug: http://openradar.appspot.com/radar?id=5320103646199808
UIApplication.shared.keyWindow!.addSubview(isPresenting ? fv : tv)
}
// use temp variables to remember these values
// because we have to reset everything before calling
// any delegate or completion block
let tContext = transitionContext
let fvc = fromViewController
let tvc = toViewController
resetTransition()
super.complete(isFinished: isFinished)
if isFinished {
processEndTransitionDelegation(transitionContext: tContext, fromViewController: fvc, toViewController: tvc)
} else {
processCancelTransitionDelegation(transitionContext: tContext, fromViewController: fvc, toViewController: tvc)
}
tContext?.completeTransition(isFinished)
}
}
fileprivate extension Motion {
/// Resets the transition values.
func resetTransition() {
transitionContext = nil
fromViewController = nil
toViewController = nil
containerBackgroundColor = nil
isNavigationController = false
isTabBarController = false
forceNonInteractive = false
insertToViewFirst = false
defaultAnimation = .auto
}
}
fileprivate extension Motion {
/// Prepares the from and to view controllers.
func prepareViewControllers() {
processStartTransitionDelegation(fromViewController: fromViewController, toViewController: toViewController)
}
/// Prepares the snapshot view, which hides any flashing that may occur.
func prepareSnapshotView() {
guard let v = transitionContainer else {
return
}
fullScreenSnapshot = v.window?.snapshotView(afterScreenUpdates: true) ?? fromView?.snapshotView(afterScreenUpdates: true)
(v.window ?? transitionContainer)?.addSubview(fullScreenSnapshot)
if let v = fromViewController?.motionStoredSnapshot {
v.removeFromSuperview()
fromViewController?.motionStoredSnapshot = nil
}
if let v = toViewController?.motionStoredSnapshot {
v.removeFromSuperview()
toViewController?.motionStoredSnapshot = nil
}
}
/// Prepares the MotionContext instance.
func prepareContext() {
guard let v = container else {
return
}
guard let fv = fromView else {
return
}
guard let tv = toView else {
return
}
context.loadViewAlpha(rootView: tv)
v.addSubview(tv)
context.loadViewAlpha(rootView: fv)
v.addSubview(fv)
}
/// Prepares the toView instance.
func prepareToView() {
guard let fv = fromView else {
return
}
guard let tv = toView else {
return
}
tv.frame = fv.frame
tv.updateConstraints()
tv.setNeedsLayout()
tv.layoutIfNeeded()
}
/// Prepares the view hierarchy.
func prepareViewHierarchy() {
guard let fv = fromView else {
return
}
guard let tv = toView else {
return
}
context.set(fromViews: fv.flattenedViewHierarchy, toViews: tv.flattenedViewHierarchy)
}
}
internal extension Motion {
override func prepareTransition() {
super.prepareTransition()
insert(preprocessor: TransitionPreprocessor(motion: self), before: DurationPreprocessor.self)
}
override func prepareTransitionPairs() {
super.prepareTransitionPairs()
guard let tv = toView else {
return
}
context.hide(view: tv)
}
}
fileprivate extension Motion {
/// Processes the animations.
func processForAnimation() {
#if os(tvOS)
animate()
#else
if isNavigationController {
// When animating within navigationController, we have to dispatch later into the main queue.
// otherwise snapshots will be pure white. Possibly a bug with UIKit
DispatchQueue.main.async { [weak self] in
self?.animate()
}
} else {
animate()
}
#endif
}
/**
Processes the start transition delegation methods.
- Parameter fromViewController: An optional UIViewController.
- Parameter toViewController: An optional UIViewController.
*/
func processStartTransitionDelegation(fromViewController: UIViewController?, toViewController: UIViewController?) {
guard let fvc = fromViewController else {
return
}
guard let tvc = toViewController else {
return
}
processForMotionDelegate(viewController: fvc) { [weak self] in
guard let s = self else {
return
}
$0.motion?(motion: s, willStartTransitionTo: tvc)
$0.motionWillStartTransition?(motion: s)
}
processForMotionDelegate(viewController: tvc) { [weak self] in
guard let s = self else {
return
}
$0.motion?(motion: s, willStartTransitionFrom: fvc)
$0.motionWillStartTransition?(motion: s)
}
}
/**
Processes the end transition delegation methods.
- Parameter transitionContext: An optional UIViewControllerContextTransitioning.
- Parameter fromViewController: An optional UIViewController.
- Parameter toViewController: An optional UIViewController.
*/
func processEndTransitionDelegation(transitionContext: UIViewControllerContextTransitioning?, fromViewController: UIViewController?, toViewController: UIViewController?) {
guard let fvc = fromViewController else {
return
}
guard let tvc = toViewController else {
return
}
processForMotionDelegate(viewController: fvc) { [weak self] in
guard let s = self else {
return
}
$0.motion?(motion: s, didEndTransitionTo: tvc)
$0.motionDidEndTransition?(motion: s)
}
processForMotionDelegate(viewController: tvc) { [weak self] in
guard let s = self else {
return
}
$0.motion?(motion: s, didEndTransitionFrom: fvc)
$0.motionDidEndTransition?(motion: s)
}
transitionContext?.finishInteractiveTransition()
}
/**
Processes the cancel transition delegation methods.
- Parameter transitionContext: An optional UIViewControllerContextTransitioning.
- Parameter fromViewController: An optional UIViewController.
- Parameter toViewController: An optional UIViewController.
*/
func processCancelTransitionDelegation(transitionContext: UIViewControllerContextTransitioning?, fromViewController: UIViewController?, toViewController: UIViewController?) {
guard let fvc = fromViewController else {
return
}
guard let tvc = toViewController else {
return
}
processForMotionDelegate(viewController: fvc) { [weak self] in
guard let s = self else {
return
}
$0.motion?(motion: s, didCancelTransitionTo: tvc)
$0.motionDidCancelTransition?(motion: s)
}
processForMotionDelegate(viewController: tvc) { [weak self] in
guard let s = self else {
return
}
$0.motion?(motion: s, didCancelTransitionFrom: fvc)
$0.motionDidCancelTransition?(motion: s)
}
transitionContext?.finishInteractiveTransition()
}
}
fileprivate extension Motion {
/// Updates the container background color.
func updateContainerBackgroundColor() {
if let v = containerBackgroundColor {
container?.backgroundColor = v
} else if !toOverFullScreen && !fromOverFullScreen {
container?.backgroundColor = toView?.backgroundColor
}
}
/// Updates the insertToViewFirst boolean for animators.
func updateInsertOrder() {
if fromOverFullScreen {
insertToViewFirst = true
}
for v in animators {
(v as? MotionHasInsertOrder)?.insertToViewFirst = insertToViewFirst
}
}
}
internal extension Motion {
/**
A helper transition function.
- Parameter from: A UIViewController.
- Parameter to: A UIViewController.
- Parameter in view: A UIView.
- Parameter completion: An optional completion handler.
*/
func transition(from: UIViewController, to: UIViewController, in view: UIView, completion: ((Bool) -> Void)? = nil) {
guard !isTransitioning else {
return
}
isPresenting = true
transitionContainer = view
fromViewController = from
toViewController = to
completionCallback = completion
start()
}
}
internal extension Motion {
/**
Helper for processing the MotionViewControllerDelegate.
- Parameter viewController: A UIViewController of type `T`.
- Parameter execute: A callback for execution during processing.
*/
func processForMotionDelegate<T: UIViewController>(viewController: T, execute: (MotionViewControllerDelegate) -> Void) {
if let delegate = viewController as? MotionViewControllerDelegate {
execute(delegate)
}
if let v = viewController as? UINavigationController,
let delegate = v.topViewController as? MotionViewControllerDelegate {
execute(delegate)
}
if let v = viewController as? UITabBarController,
let delegate = v.viewControllers?[v.selectedIndex] as? MotionViewControllerDelegate {
execute(delegate)
}
}
}
extension Motion: UIViewControllerAnimatedTransitioning {
/**
The animation method that is used to coordinate the transition.
- Parameter using transitionContext: A UIViewControllerContextTransitioning.
*/
public func animateTransition(using context: UIViewControllerContextTransitioning) {
guard !isTransitioning else {
return
}
transitionContext = context
fromViewController = fromViewController ?? context.viewController(forKey: .from)
toViewController = toViewController ?? context.viewController(forKey: .to)
transitionContainer = context.containerView
start()
}
/**
Returns the transition duration time interval.
- Parameter using transitionContext: An optional UIViewControllerContextTransitioning.
- Returns: A TimeInterval that is the total animation time including delays.
*/
public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0 // Will be updated dynamically.
}
public func animationEnded(_ transitionCompleted: Bool) {
isAnimating = !transitionCompleted
}
}
extension Motion: UIViewControllerTransitioningDelegate {
/// A reference to the interactive transitioning instance.
var interactiveTransitioning: UIViewControllerInteractiveTransitioning? {
return forceNonInteractive ? nil : self
}
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.isPresenting = true
self.fromViewController = fromViewController ?? presenting
self.toViewController = toViewController ?? presented
return self
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.isPresenting = false
self.fromViewController = fromViewController ?? dismissed
return self
}
public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactiveTransitioning
}
public func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactiveTransitioning
}
}
extension Motion: UIViewControllerInteractiveTransitioning {
public var wantsInteractiveStart: Bool {
return true
}
public func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
animateTransition(using: transitionContext)
}
}
extension Motion: UINavigationControllerDelegate {
public func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.isPresenting = .push == operation
self.fromViewController = fromViewController ?? fromVC
self.toViewController = toViewController ?? toVC
self.isNavigationController = true
return self
}
public func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactiveTransitioning
}
}
extension Motion: UITabBarControllerDelegate {
public func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
return !isAnimating
}
public func tabBarController(_ tabBarController: UITabBarController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactiveTransitioning
}
public func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
isAnimating = true
let fromVCIndex = tabBarController.childViewControllers.index(of: fromVC)!
let toVCIndex = tabBarController.childViewControllers.index(of: toVC)!
self.isPresenting = toVCIndex > fromVCIndex
self.fromViewController = fromViewController ?? fromVC
self.toViewController = toViewController ?? toVC
self.isTabBarController = true
return self
}
}
public typealias MotionDelayCancelBlock = (Bool) -> Void
extension Motion {
/**
Executes a block of code after a time delay.
- Parameter duration: An animation duration time.
- Parameter animations: An animation block.
- Parameter execute block: A completion block that is executed once
the animations have completed.
*/
@discardableResult
public class func delay(_ time: TimeInterval, execute: @escaping () -> Void) -> MotionDelayCancelBlock? {
var cancelable: MotionDelayCancelBlock?
let delayed: MotionDelayCancelBlock = {
if !$0 {
DispatchQueue.main.async(execute: execute)
}
cancelable = nil
}
cancelable = delayed
DispatchQueue.main.asyncAfter(deadline: .now() + time) {
cancelable?(false)
}
return cancelable
}
/**
Cancels the delayed MotionDelayCancelBlock.
- Parameter delayed completion: An MotionDelayCancelBlock.
*/
public class func cancel(delayed completion: MotionDelayCancelBlock) {
completion(true)
}
/**
Disables the default animations set on CALayers.
- Parameter animations: A callback that wraps the animations to disable.
*/
public class func disable(_ animations: (() -> Void)) {
animate(duration: 0, animations: animations)
}
/**
Runs an animation with a specified duration.
- Parameter duration: An animation duration time.
- Parameter animations: An animation block.
- Parameter timingFunction: A CAMediaTimingFunction.
- Parameter completion: A completion block that is executed once
the animations have completed.
*/
public class func animate(duration: CFTimeInterval, timingFunction: CAMediaTimingFunction = .easeInOut, animations: (() -> Void), completion: (() -> Void)? = nil) {
CATransaction.begin()
CATransaction.setAnimationDuration(duration)
CATransaction.setCompletionBlock(completion)
CATransaction.setAnimationTimingFunction(timingFunction)
animations()
CATransaction.commit()
}
/**
Creates a CAAnimationGroup.
- Parameter animations: An Array of CAAnimation objects.
- Parameter timingFunction: A CAMediaTimingFunction.
- Parameter duration: An animation duration time for the group.
- Returns: A CAAnimationGroup.
*/
public class func animate(group animations: [CAAnimation], timingFunction: CAMediaTimingFunction = .easeInOut, duration: CFTimeInterval = 0.5) -> CAAnimationGroup {
let group = CAAnimationGroup()
group.fillMode = MotionAnimationFillModeToValue(mode: .both)
group.isRemovedOnCompletion = false
group.animations = animations
group.duration = duration
group.timingFunction = timingFunction
return group
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
public class MotionAnimation {
/// A reference to the callback that applies the MotionAnimationState.
internal let apply: (inout MotionAnimationState) -> Void
/**
An initializer that accepts a given callback.
- Parameter applyFunction: A given callback.
*/
init(applyFunction: @escaping (inout MotionAnimationState) -> Void) {
apply = applyFunction
}
}
public extension MotionAnimation {
/**
Animates a view's current background color to the
given color.
- Parameter color: A UIColor.
- Returns: A MotionAnimation.
*/
static func background(color: UIColor) -> MotionAnimation {
return MotionAnimation {
$0.backgroundColor = color.cgColor
}
}
/**
Animates a view's current border color to the
given color.
- Parameter color: A UIColor.
- Returns: A MotionAnimation.
*/
static func border(color: UIColor) -> MotionAnimation {
return MotionAnimation {
$0.borderColor = color.cgColor
}
}
/**
Animates a view's current border width to the
given width.
- Parameter width: A CGFloat.
- Returns: A MotionAnimation.
*/
static func border(width: CGFloat) -> MotionAnimation {
return MotionAnimation {
$0.borderWidth = width
}
}
/**
Animates a view's current corner radius to the
given radius.
- Parameter radius: A CGFloat.
- Returns: A MotionAnimation.
*/
static func corner(radius: CGFloat) -> MotionAnimation {
return MotionAnimation {
$0.cornerRadius = radius
}
}
/**
Animates a view's current transform (perspective, scale, rotate)
to the given one.
- Parameter _ transform: A CATransform3D.
- Returns: A MotionAnimation.
*/
static func transform(_ transform: CATransform3D) -> MotionAnimation {
return MotionAnimation {
$0.transform = transform
}
}
/**
Animates a view's current rotation to the given x, y,
and z values.
- Parameter x: A CGFloat.
- Parameter y: A CGFloat.
- Parameter z: A CGFloat.
- Returns: A MotionAnimation.
*/
static func rotate(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) -> MotionAnimation {
return MotionAnimation {
var t = $0.transform ?? CATransform3DIdentity
t = CATransform3DRotate(t, CGFloat(Double.pi) * x / 180, 1, 0, 0)
t = CATransform3DRotate(t, CGFloat(Double.pi) * y / 180, 0, 1, 0)
$0.transform = CATransform3DRotate(t, CGFloat(Double.pi) * z / 180, 0, 0, 1)
}
}
/**
Animates a view's current rotation to the given point.
- Parameter _ point: A CGPoint.
- Parameter z: A CGFloat.
- Returns: A MotionAnimation.
*/
static func rotate(_ point: CGPoint, z: CGFloat = 0) -> MotionAnimation {
return .rotate(x: point.x, y: point.y, z: z)
}
/**
Rotate 2d.
- Parameter _ z: A CGFloat.
- Returns: A MotionAnimation.
*/
static func rotate(_ z: CGFloat) -> MotionAnimation {
return .rotate(z: z)
}
/**
Animates a view's current spin to the given x, y,
and z values.
- Parameter x: A CGFloat.
- Parameter y: A CGFloat.
- Parameter z: A CGFloat.
- Returns: A MotionAnimation.
*/
static func spin(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) -> MotionAnimation {
return MotionAnimation {
$0.spin = (x, y, z)
}
}
/**
Animates a view's current spin to the given point.
- Parameter _ point: A CGPoint.
- Parameter z: A CGFloat.
- Returns: A MotionAnimation.
*/
static func spin(_ point: CGPoint, z: CGFloat = 0) -> MotionAnimation {
return .spin(x: point.x, y: point.y, z: z)
}
/**
Spin 2d.
- Parameter _ z: A CGFloat.
- Returns: A MotionAnimation.
*/
static func spin(_ z: CGFloat) -> MotionAnimation {
return .spin(z: z)
}
/**
Animates the view's current scale to the given x, y, and z scale values.
- Parameter x: A CGFloat.
- Parameter y: A CGFloat.
- Parameter z: A CGFloat.
- Returns: A MotionAnimation.
*/
static func scale(x: CGFloat = 1, y: CGFloat = 1, z: CGFloat = 1) -> MotionAnimation {
return MotionAnimation {
$0.transform = CATransform3DScale($0.transform ?? CATransform3DIdentity, x, y, z)
}
}
/**
Animates the view's current x & y scale to the given scale value.
- Parameter _ xy: A CGFloat.
- Returns: A MotionAnimation.
*/
static func scale(_ xy: CGFloat) -> MotionAnimation {
return .scale(x: xy, y: xy)
}
/**
Animates a view equal to the distance given by the x, y, and z values.
- Parameter x: A CGFloat.
- Parameter y: A CGFloat.
- Parameter z: A CGFloat.
- Returns: A MotionAnimation.
*/
static func translate(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) -> MotionAnimation {
return MotionAnimation {
$0.transform = CATransform3DTranslate($0.transform ?? CATransform3DIdentity, x, y, z)
}
}
/**
Animates a view equal to the distance given by a point and z value.
- Parameter _ point: A CGPoint.
- Parameter z: A CGFloat.
- Returns: A MotionAnimation.
*/
static func translate(_ point: CGPoint, z: CGFloat = 0) -> MotionAnimation {
return .translate(x: point.x, y: point.y, z: z)
}
/**
Animates a view's current position to the given point.
- Parameter _ point: A CGPoint.
- Returns: A MotionAnimation.
*/
static func position(_ point: CGPoint) -> MotionAnimation {
return MotionAnimation {
$0.position = point
}
}
/// Fades the view in during an animation.
static var fadeIn = MotionAnimation.fade(1)
/// Fades the view out during an animation.
static var fadeOut = MotionAnimation.fade(0)
/**
Animates a view's current opacity to the given one.
- Parameter _ opacity: A Double.
- Returns: A MotionAnimation.
*/
static func fade(_ opacity: Double) -> MotionAnimation {
return MotionAnimation {
$0.opacity = opacity
}
}
/**
Animates a view's current zPosition to the given position.
- Parameter _ position: An Int.
- Returns: A MotionAnimation.
*/
static func zPosition(_ position: CGFloat) -> MotionAnimation {
return MotionAnimation {
$0.zPosition = position
}
}
/**
Animates a view's current size to the given one.
- Parameter _ size: A CGSize.
- Returns: A MotionAnimation.
*/
static func size(_ size: CGSize) -> MotionAnimation {
return MotionAnimation {
$0.size = size
}
}
/**
Animates a view's current shadow path to the given one.
- Parameter path: A CGPath.
- Returns: A MotionAnimation.
*/
static func shadow(path: CGPath) -> MotionAnimation {
return MotionAnimation {
$0.shadowPath = path
}
}
/**
Animates a view's current shadow color to the given one.
- Parameter color: A UIColor.
- Returns: A MotionAnimation.
*/
static func shadow(color: UIColor) -> MotionAnimation {
return MotionAnimation {
$0.shadowColor = color.cgColor
}
}
/**
Animates a view's current shadow offset to the given one.
- Parameter offset: A CGSize.
- Returns: A MotionAnimation.
*/
static func shadow(offset: CGSize) -> MotionAnimation {
return MotionAnimation {
$0.shadowOffset = offset
}
}
/**
Animates a view's current shadow opacity to the given one.
- Parameter opacity: A Float.
- Returns: A MotionAnimation.
*/
static func shadow(opacity: Float) -> MotionAnimation {
return MotionAnimation {
$0.shadowOpacity = opacity
}
}
/**
Animates a view's current shadow radius to the given one.
- Parameter radius: A CGFloat.
- Returns: A MotionAnimation.
*/
static func shadow(radius: CGFloat) -> MotionAnimation {
return MotionAnimation {
$0.shadowRadius = radius
}
}
/**
Animates the views shadow offset, opacity, and radius.
- Parameter offset: A CGSize.
- Parameter opacity: A Float.
- Parameter radius: A CGFloat.
*/
static func depth(offset: CGSize, opacity: Float, radius: CGFloat) -> MotionAnimation {
return MotionAnimation {
$0.shadowOffset = offset
$0.shadowOpacity = opacity
$0.shadowRadius = radius
}
}
/**
Animates the views shadow offset, opacity, and radius.
- Parameter _ depth: A tuple (CGSize, FLoat, CGFloat).
*/
static func depth(_ depth: (CGSize, Float, CGFloat)) -> MotionAnimation {
return .depth(offset: depth.0, opacity: depth.1, radius: depth.2)
}
/**
Available in iOS 9+, animates a view using the spring API,
given a stiffness and damping.
- Parameter stiffness: A CGFlloat.
- Parameter damping: A CGFloat.
- Returns: A MotionAnimation.
*/
@available(iOS 9, *)
static func spring(stiffness: CGFloat, damping: CGFloat) -> MotionAnimation {
return MotionAnimation {
$0.spring = (stiffness, damping)
}
}
/**
The duration of the view's animation. If a duration of 0 is used,
the value will be converted to 0.01, to give a close to zero value.
- Parameter _ duration: A TimeInterval.
- Returns: A MotionAnimation.
*/
static func duration(_ duration: TimeInterval) -> MotionAnimation {
return MotionAnimation {
$0.duration = duration
}
}
/**
Delays the animation of a given view.
- Parameter _ time: TimeInterval.
- Returns: A MotionAnimation.
*/
static func delay(_ time: TimeInterval) -> MotionAnimation {
return MotionAnimation {
$0.delay = time
}
}
/**
Sets the view's timing function for the animation.
- Parameter _ timingFunction: A CAMediaTimingFunction.
- Returns: A MotionAnimation.
*/
static func timingFunction(_ timingFunction: CAMediaTimingFunction) -> MotionAnimation {
return MotionAnimation {
$0.timingFunction = timingFunction
}
}
/**
Creates a completion block handler that is executed once the animation
is done.
- Parameter _ execute: A callback to execute once completed.
*/
static func completion(_ execute: @escaping () -> Void) -> MotionAnimation {
return MotionAnimation {
$0.completion = execute
}
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
public struct MotionAnimationState {
/// A reference to the position.
public var position: CGPoint?
/// A reference to the size.
public var size: CGSize?
/// A reference to the transform.
public var transform: CATransform3D?
/// A reference to the spin tuple.
public var spin: (x: CGFloat, y: CGFloat, z: CGFloat)?
/// A reference to the opacity.
public var opacity: Double?
/// A reference to the cornerRadius.
public var cornerRadius: CGFloat?
/// A reference to the backgroundColor.
public var backgroundColor: CGColor?
/// A reference to the zPosition.
public var zPosition: CGFloat?
/// A reference to the borderWidth.
public var borderWidth: CGFloat?
/// A reference to the borderColor.
public var borderColor: CGColor?
/// A reference to the shadowColor.
public var shadowColor: CGColor?
/// A reference to the shadowOpacity.
public var shadowOpacity: Float?
/// A reference to the shadowOffset.
public var shadowOffset: CGSize?
/// A reference to the shadowRadius.
public var shadowRadius: CGFloat?
/// A reference to the shadowPath.
public var shadowPath: CGPath?
/// A reference to the spring animation settings.
public var spring: (CGFloat, CGFloat)?
/// A time delay on starting the animation.
public var delay: TimeInterval = 0
/// The duration of the animation.
public var duration: TimeInterval = 0.35
/// The timing function value of the animation.
public var timingFunction = CAMediaTimingFunction.easeInOut
/// Custom target states.
public var custom: [String: Any]?
/// Completion block.
public var completion: (() -> Void)?
/**
An initializer that accepts an Array of MotionAnimations.
- Parameter animations: An Array of MotionAnimations.
*/
init(animations: [MotionAnimation]) {
append(contentsOf: animations)
}
}
extension MotionAnimationState {
/**
Adds a MotionAnimation to the current state.
- Parameter _ element: A MotionAnimation.
*/
public mutating func append(_ element: MotionAnimation) {
element.apply(&self)
}
/**
Adds an Array of MotionAnimations to the current state.
- Parameter contentsOf elements: An Array of MotionAnimations.
*/
public mutating func append(contentsOf elements: [MotionAnimation]) {
for v in elements {
v.apply(&self)
}
}
/**
A subscript that returns a custom value for a specified key.
- Parameter key: A String.
- Returns: An optional Any value.
*/
public subscript(key: String) -> Any? {
get {
return custom?[key]
}
set(value) {
if nil == custom {
custom = [:]
}
custom![key] = value
}
}
}
extension MotionAnimationState: ExpressibleByArrayLiteral {
/**
An initializer implementing the ExpressibleByArrayLiteral protocol.
- Parameter arrayLiteral elements: A list of MotionAnimations.
*/
public init(arrayLiteral elements: MotionAnimation...) {
append(contentsOf: elements)
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
public enum MotionAnimationKeyPath: String {
case backgroundColor
case borderColor
case borderWidth
case cornerRadius
case transform
case rotate = "transform.rotation"
case rotateX = "transform.rotation.x"
case rotateY = "transform.rotation.y"
case rotateZ = "transform.rotation.z"
case scale = "transform.scale"
case scaleX = "transform.scale.x"
case scaleY = "transform.scale.y"
case scaleZ = "transform.scale.z"
case translation = "transform.translation"
case translationX = "transform.translation.x"
case translationY = "transform.translation.y"
case translationZ = "transform.translation.z"
case position
case opacity
case zPosition
case width = "bounds.size.width"
case height = "bounds.size.height"
case size = "bounds.size"
case shadowPath
case shadowColor
case shadowOffset
case shadowOpacity
case shadowRadius
}
extension CABasicAnimation {
/**
A convenience initializer that takes a given MotionAnimationKeyPath.
- Parameter keyPath: An MotionAnimationKeyPath.
*/
public convenience init(keyPath: MotionAnimationKeyPath) {
self.init(keyPath: keyPath.rawValue)
}
}
public struct MotionCAAnimation {}
fileprivate extension MotionCAAnimation {
/**
Creates a CABasicAnimation.
- Parameter keyPath: A MotionAnimationKeyPath.
- Parameter toValue: An Any value that is the end state of the animation.
*/
static func createAnimation(keyPath: MotionAnimationKeyPath, toValue: Any) -> CABasicAnimation {
let a = CABasicAnimation(keyPath: keyPath)
a.toValue = toValue
return a
}
}
@available(iOS 9.0, *)
internal extension MotionCAAnimation {
/**
Converts a CABasicAnimation to a CASpringAnimation.
- Parameter animation: A CABasicAnimation.
- Parameter stiffness: A CGFloat.
- Parameter damping: A CGFloat.
*/
static func convert(animation: CABasicAnimation, stiffness: CGFloat, damping: CGFloat) -> CASpringAnimation {
let a = CASpringAnimation(keyPath: animation.keyPath)
a.fromValue = animation.fromValue
a.toValue = animation.toValue
a.stiffness = stiffness
a.damping = damping
return a
}
}
public extension MotionCAAnimation {
/**
Creates a CABasicAnimation for the backgroundColor key path.
- Parameter color: A UIColor.
- Returns: A CABasicAnimation.
*/
static func background(color: UIColor) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .backgroundColor, toValue: color.cgColor)
}
/**
Creates a CABasicAnimation for the borderColor key path.
- Parameter color: A UIColor.
- Returns: A CABasicAnimation.
*/
static func border(color: UIColor) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .borderColor, toValue: color.cgColor)
}
/**
Creates a CABasicAnimation for the borderWidth key path.
- Parameter width: A CGFloat.
- Returns: A CABasicAnimation.
*/
static func border(width: CGFloat) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .borderWidth, toValue: NSNumber(floatLiteral: Double(width)))
}
/**
Creates a CABasicAnimation for the cornerRadius key path.
- Parameter radius: A CGFloat.
- Returns: A CABasicAnimation.
*/
static func corner(radius: CGFloat) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .cornerRadius, toValue: NSNumber(floatLiteral: Double(radius)))
}
/**
Creates a CABasicAnimation for the transform key path.
- Parameter _ t: A CATransform3D object.
- Returns: A CABasicAnimation.
*/
static func transform(_ t: CATransform3D) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .transform, toValue: NSValue(caTransform3D: t))
}
/**
Creates a CABasicAnimation for the transform.scale key path.
- Parameter xyz: A CGFloat.
- Returns: A CABasicAnimation.
*/
public static func scale(_ xyz: CGFloat) -> CABasicAnimation {
let a = CABasicAnimation(keyPath: .scale)
a.toValue = NSNumber(value: Double(xyz))
return a
}
/**
Creates a CABasicAnimation for the transform.scale.x key path.
- Parameter x: A CGFloat.
- Returns: A CABasicAnimation.
*/
public static func scale(x: CGFloat) -> CABasicAnimation {
let a = CABasicAnimation(keyPath: .scaleX)
a.toValue = NSNumber(value: Double(x))
return a
}
/**
Creates a CABasicAnimation for the transform.scale.y key path.
- Parameter y: A CGFloat.
- Returns: A CABasicAnimation.
*/
public static func scale(y: CGFloat) -> CABasicAnimation {
let a = CABasicAnimation(keyPath: .scaleY)
a.toValue = NSNumber(value: Double(y))
return a
}
/**
Creates a CABasicAnimation for the transform.scale.z key path.
- Parameter z: A CGFloat.
- Returns: A CABasicAnimation.
*/
public static func scale(z: CGFloat) -> CABasicAnimation {
let a = CABasicAnimation(keyPath: .scaleZ)
a.toValue = NSNumber(value: Double(z))
return a
}
/**
Creates a CABasicAnimation for the transform.rotate.x key path.
- Parameter x: An optional CGFloat.
- Returns: A CABasicAnimation.
*/
static func spin(x: CGFloat) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .rotateX, toValue: NSNumber(value: Double(CGFloat(Double.pi) * 2 * x)))
}
/**
Creates a CABasicAnimation for the transform.rotate.y key path.
- Parameter y: An optional CGFloat.
- Returns: A CABasicAnimation.
*/
static func spin(y: CGFloat) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .rotateY, toValue: NSNumber(value: Double(CGFloat(Double.pi) * 2 * y)))
}
/**
Creates a CABasicAnimation for the transform.rotate.z key path.
- Parameter z: An optional CGFloat.
- Returns: A CABasicAnimation.
*/
static func spin(z: CGFloat) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .rotateZ, toValue: NSNumber(value: Double(CGFloat(Double.pi) * 2 * z)))
}
/**
Creates a CABasicAnimation for the position key path.
- Parameter _ point: A CGPoint.
- Returns: A CABasicAnimation.
*/
static func position(_ point: CGPoint) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .position, toValue: NSValue(cgPoint: point))
}
/**
Creates a CABasicAnimation for the opacity key path.
- Parameter _ opacity: A Double.
- Returns: A CABasicAnimation.
*/
static func fade(_ opacity: Double) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .opacity, toValue: NSNumber(floatLiteral: opacity))
}
/**
Creates a CABasicaAnimation for the zPosition key path.
- Parameter _ position: A CGFloat.
- Returns: A CABasicAnimation.
*/
static func zPosition(_ position: CGFloat) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .zPosition, toValue: NSNumber(value: Double(position)))
}
/**
Creates a CABasicaAnimation for the width key path.
- Parameter width: A CGFloat.
- Returns: A CABasicAnimation.
*/
static func width(_ width: CGFloat) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .width, toValue: NSNumber(floatLiteral: Double(width)))
}
/**
Creates a CABasicaAnimation for the height key path.
- Parameter height: A CGFloat.
- Returns: A CABasicAnimation.
*/
static func height(_ height: CGFloat) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .height, toValue: NSNumber(floatLiteral: Double(height)))
}
/**
Creates a CABasicaAnimation for the height key path.
- Parameter size: A CGSize.
- Returns: A CABasicAnimation.
*/
static func size(_ size: CGSize) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .size, toValue: NSValue(cgSize: size))
}
/**
Creates a CABasicAnimation for the shadowPath key path.
- Parameter path: A CGPath.
- Returns: A CABasicAnimation.
*/
static func shadow(path: CGPath) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .shadowPath, toValue: path)
}
/**
Creates a CABasicAnimation for the shadowColor key path.
- Parameter color: A UIColor.
- Returns: A CABasicAnimation.
*/
static func shadow(color: UIColor) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .shadowColor, toValue: color.cgColor)
}
/**
Creates a CABasicAnimation for the shadowOffset key path.
- Parameter offset: A CGSize.
- Returns: A CABasicAnimation.
*/
static func shadow(offset: CGSize) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .shadowOffset, toValue: NSValue(cgSize: offset))
}
/**
Creates a CABasicAnimation for the shadowOpacity key path.
- Parameter opacity: A Float.
- Returns: A CABasicAnimation.
*/
static func shadow(opacity: Float) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .shadowOpacity, toValue: NSNumber(floatLiteral: Double(opacity)))
}
/**
Creates a CABasicAnimation for the shadowRadius key path.
- Parameter radius: A CGFloat.
- Returns: A CABasicAnimation.
*/
static func shadow(radius: CGFloat) -> CABasicAnimation {
return MotionCAAnimation.createAnimation(keyPath: .shadowRadius, toValue: NSNumber(floatLiteral: Double(radius)))
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
public class MotionContext {
/// A reference of motion identifiers to source views.
internal var motionIdentifierToSourceView = [String: UIView]()
/// A reference of motion identifiers to destination views.
internal var motionIdentifierToDestinationView = [String: UIView]()
/// A reference of the snapshot to source/destination view.
internal var viewToSnapshot = [UIView: UIView]()
/// A reference to the view to view alpha value.
internal var viewToAlphas = [UIView: CGFloat]()
/// A reference of view to transition target state.
internal var viewToTargetState = [UIView: MotionTransitionState]()
/// A reference of the superview to the subviews snapshots.
internal var superviewToNoSnapshotSubviewMap = [UIView: [(Int, UIView)]]()
/// A reference to the default coordinate space for transitions.
internal var defaultCoordinateSpace = MotionCoordinateSpace.local
/// The container view holding all of the animating views.
public let container: UIView
/// A flattened list of all views from the source view controller.
public var fromViews: [UIView]!
/// A flattened list of all views from the destination view controller.
public var toViews: [UIView]!
/**
An initializer that accepts a container transition view.
- Parameter container: A UIView.
*/
internal init(container: UIView) {
self.container = container
}
}
internal extension MotionContext {
/**
Sets the fromViews and toViews within the transition context.
- Parameter fromViews: An Array of UIViews.
- Parameter toViews: An Array of UIViews.
*/
func set(fromViews: [UIView], toViews: [UIView]) {
self.fromViews = fromViews
self.toViews = toViews
map(views: fromViews, identifierMap: &motionIdentifierToSourceView)
map(views: toViews, identifierMap: &motionIdentifierToDestinationView)
}
/**
Maps the views to their respective identifier index.
- Parameter views: An Array of UIViews.
- Parameter identifierMap: A Dicionary of String to UIView pairs.
*/
func map(views: [UIView], identifierMap: inout [String: UIView]) {
for v in views {
v.layer.removeAllAnimations()
if container.convert(v.bounds, from: v).intersects(container.bounds) {
if let i = v.motionIdentifier {
identifierMap[i] = v
}
if let i = v.motionTransitions {
viewToTargetState[v] = MotionTransitionState(transitions: i)
}
}
}
}
}
public extension MotionContext {
/**
A subscript that takes a given view and retrieves a
MotionTransitionState if one exists.
- Parameter view: A UIView.
- Returns: An optional MotionTransitionState.
*/
subscript(view: UIView) -> MotionTransitionState? {
get {
return viewToTargetState[view]
}
set {
viewToTargetState[view] = newValue
}
}
}
public extension MotionContext {
/**
Retrieves a source view matching the motionIdentifier, nil if not found.
- Parameter for motionIdentifier: A String.
- Returns: An optional UIView.
*/
func sourceView(for motionIdentifier: String) -> UIView? {
return motionIdentifierToSourceView[motionIdentifier]
}
/**
Retrieves a destination view matching the motionIdentifier, nil if not found.
- Parameter for motionIdentifier: A String.
- Returns: An optional UIView.
*/
func destinationView(for motionIdentifier: String) -> UIView? {
return motionIdentifierToDestinationView[motionIdentifier]
}
/**
Retrieves the matching view with the same motionIdentifier found in the
source and destination view controllers.
- Returns: An optional UIView.
*/
func transitionPairedView(for view: UIView) -> UIView? {
if let i = view.motionIdentifier {
if view == sourceView(for: i) {
return destinationView(for: i)
} else if view == destinationView(for: i) {
return sourceView(for: i)
}
}
return nil
}
/**
Retrieves the snapshot view for a given view.
- Parameter for view: A UIView.
- Returns: A UIView.
*/
func snapshotView(for view: UIView) -> UIView {
if let v = viewToSnapshot[view] {
return v
}
var containerView = container
let coordinateSpace = viewToTargetState[view]?.coordinateSpace ?? defaultCoordinateSpace
switch coordinateSpace {
case .local:
containerView = view
while containerView != container, nil == viewToSnapshot[containerView], let superview = containerView.superview {
containerView = superview
}
if let snapshot = viewToSnapshot[containerView] {
containerView = snapshot
}
case .sameParent:
containerView = view.superview!
case .global:
break
}
unhide(view: view)
// Capture a snapshot without the alpha & cornerRadius values.
let oldCornerRadius = view.layer.cornerRadius
view.layer.cornerRadius = 0
let oldAlpha = view.alpha
view.alpha = 1
let snapshot: UIView
let snapshotType: MotionSnapshotType = self[view]?.snapshotType ?? .optimized
switch snapshotType {
case .normal:
snapshot = view.snapshotView(afterScreenUpdates: true)!
case .layerRender:
snapshot = view.slowSnapshotView()
case .noSnapshot:
if nil == superviewToNoSnapshotSubviewMap[view.superview!] {
superviewToNoSnapshotSubviewMap[view.superview!] = []
}
superviewToNoSnapshotSubviewMap[view.superview!]!.append((view.superview!.subviews.index(of: view)!, view))
snapshot = view
case .optimized:
#if os(tvOS)
snapshot = view.snapshotView(afterScreenUpdates: true)!
#else
if #available(iOS 9.0, *), let stackView = view as? UIStackView {
snapshot = stackView.slowSnapshotView()
} else if let imageView = view as? UIImageView, view.subviews.isEmpty {
let contentView = UIImageView(image: imageView.image)
contentView.frame = imageView.bounds
contentView.contentMode = imageView.contentMode
contentView.tintColor = imageView.tintColor
contentView.backgroundColor = imageView.backgroundColor
let snapShotView = UIView()
snapShotView.addSubview(contentView)
snapshot = snapShotView
} else if let navigationBar = view as? UINavigationBar, navigationBar.isTranslucent {
let newNavigationBar = UINavigationBar(frame: navigationBar.frame)
newNavigationBar.barStyle = navigationBar.barStyle
newNavigationBar.tintColor = navigationBar.tintColor
newNavigationBar.barTintColor = navigationBar.barTintColor
newNavigationBar.clipsToBounds = false
// Take a snapshot without the background.
navigationBar.layer.sublayers![0].opacity = 0
let realSnapshot = navigationBar.snapshotView(afterScreenUpdates: true)!
navigationBar.layer.sublayers![0].opacity = 1
newNavigationBar.addSubview(realSnapshot)
snapshot = newNavigationBar
} else if let effectView = view as? UIVisualEffectView {
snapshot = UIVisualEffectView(effect: effectView.effect)
snapshot.frame = effectView.bounds
} else {
snapshot = view.snapshotView(afterScreenUpdates: true)!
}
#endif
}
#if os(tvOS)
if let imageView = view as? UIImageView, imageView.adjustsImageWhenAncestorFocused {
snapshot.frame = imageView.focusedFrameGuide.layoutFrame
}
#endif
view.layer.cornerRadius = oldCornerRadius
view.alpha = oldAlpha
if .noSnapshot != snapshotType {
snapshot.layer.allowsGroupOpacity = false
if !(view is UINavigationBar), let contentView = snapshot.subviews.get(0) {
/**
The snapshot's contentView must have the cornerRadius value,
since the snapshot might not have maskToBounds set
*/
contentView.layer.cornerRadius = view.layer.cornerRadius
contentView.layer.masksToBounds = true
}
snapshot.layer.cornerRadius = view.layer.cornerRadius
snapshot.layer.zPosition = view.layer.zPosition
snapshot.layer.opacity = view.layer.opacity
snapshot.layer.isOpaque = view.layer.isOpaque
snapshot.layer.anchorPoint = view.layer.anchorPoint
snapshot.layer.masksToBounds = view.layer.masksToBounds
snapshot.layer.borderColor = view.layer.borderColor
snapshot.layer.borderWidth = view.layer.borderWidth
snapshot.layer.transform = view.layer.transform
snapshot.layer.contentsRect = view.layer.contentsRect
snapshot.layer.contentsScale = view.layer.contentsScale
if self[view]?.displayShadow ?? true {
snapshot.layer.shadowRadius = view.layer.shadowRadius
snapshot.layer.shadowOpacity = view.layer.shadowOpacity
snapshot.layer.shadowColor = view.layer.shadowColor
snapshot.layer.shadowOffset = view.layer.shadowOffset
snapshot.layer.shadowPath = view.layer.shadowPath
}
}
snapshot.frame = containerView.convert(view.bounds, from: view)
snapshot.motionIdentifier = view.motionIdentifier
hide(view: view)
if let pairedView = transitionPairedView(for: view), let pairedSnapshot = viewToSnapshot[pairedView] {
let siblingViews = pairedView.superview!.subviews
let nextSiblings = siblingViews[siblingViews.index(of: pairedView)!+1..<siblingViews.count]
containerView.addSubview(pairedSnapshot)
for subview in pairedView.subviews {
insertGlobalViewTree(view: subview)
}
for sibling in nextSiblings {
insertGlobalViewTree(view: sibling)
}
}
containerView.addSubview(snapshot)
viewToSnapshot[view] = snapshot
return snapshot
}
/**
Inserts the given view into the global context space.
- Parameter view: A UIView.
*/
func insertGlobalViewTree(view: UIView) {
if .global == viewToTargetState[view]?.coordinateSpace, let snapshot = viewToSnapshot[view] {
container.addSubview(snapshot)
}
for v in view.subviews {
insertGlobalViewTree(view: v)
}
}
/// Restores the transition subview map with its superview.
func clean() {
for (superview, subviews) in superviewToNoSnapshotSubviewMap {
for (index, view) in subviews.reversed() {
superview.insertSubview(view, at: index)
}
}
}
}
internal extension MotionContext {
/**
Hides a given view.
- Parameter view: A UIView.
*/
func hide(view: UIView) {
guard nil == viewToAlphas[view], .noSnapshot != self[view]?.snapshotType else {
return
}
if view is UIVisualEffectView {
view.isHidden = true
viewToAlphas[view] = 1
} else {
viewToAlphas[view] = view.isOpaque ? .infinity : view.alpha
view.alpha = 0
}
}
/**
Shows a given view that was hidden.
- Parameter view: A UIView.
*/
func unhide(view: UIView) {
guard let oldAlpha = viewToAlphas[view] else {
return
}
if view is UIVisualEffectView {
view.isHidden = false
} else if oldAlpha == .infinity {
view.alpha = 1
view.isOpaque = true
} else {
view.alpha = oldAlpha
}
viewToAlphas[view] = nil
}
/// Shows all given views that are hidden.
func unhideAll() {
for v in viewToAlphas.keys {
unhide(view: v)
}
viewToAlphas.removeAll()
}
/**
Show a given view and its subviews that are hidden.
- Parameter rootView: A UIView.
*/
func unhide(rootView: UIView) {
unhide(view: rootView)
for subview in rootView.subviews {
unhide(rootView: subview)
}
}
/// Removes all snapshots that are not using .useNoSnapshot.
func removeAllSnapshots() {
for (k, v) in viewToSnapshot {
if k != v {
v.removeFromSuperview()
}
}
}
/**
Removes the snapshots for a given view and all its subviews.
- Parameter rootView: A UIVIew.
*/
func removeSnapshots(rootView: UIView) {
if let v = viewToSnapshot[rootView], v != rootView {
v.removeFromSuperview()
}
for v in rootView.subviews {
removeSnapshots(rootView: v)
}
}
/**
Retrieves the snapshots for a given view and all its subviews.
- Parameter rootView: A UIView.
- Returns: An Array of UIViews.
*/
func snapshots(rootView: UIView) -> [UIView] {
var snapshots = [UIView]()
for v in rootView.flattenedViewHierarchy {
if let snapshot = viewToSnapshot[v] {
snapshots.append(snapshot)
}
}
return snapshots
}
/**
Sets the alpha values for a given view and its subviews to the
stored alpha value.
- Parameter rootView: A UIView.
*/
func loadViewAlpha(rootView: UIView) {
if let storedAlpha = rootView.motionAlpha {
rootView.alpha = storedAlpha
rootView.motionAlpha = nil
}
for subview in rootView.subviews {
loadViewAlpha(rootView: subview)
}
}
/**
Stores the alpha values for a given view and its subviews.
- Parameter rootView: A UIView.
*/
func storeViewAlpha(rootView: UIView) {
rootView.motionAlpha = viewToAlphas[rootView]
for subview in rootView.subviews {
storeViewAlpha(rootView: subview)
}
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
public class MotionController: NSObject {
/// A reference to the MotionContext.
public internal(set) var context: MotionContext!
/// A boolean indicating whether the transition interactive or not.
public var isInteractive: Bool {
return nil == displayLink
}
/// Progress of the current transition. 0 if no transition is happening.
public internal(set) var elapsedTime: TimeInterval = 0 {
didSet {
guard isTransitioning else {
return
}
updateTransitionObservers()
guard isInteractive else {
updatePlugins()
return
}
updateAnimators()
}
}
/// A boolean indicating whether a transition is active.
public var isTransitioning: Bool {
return nil != transitionContainer
}
/**
A view container used to hold all the animating views during a
transition.
*/
public internal(set) var container: UIView?
/// UIKit's supplied transition container.
internal var transitionContainer: UIView?
/// An optional completion callback.
internal var completionCallback: ((Bool) -> Void)?
/// Binds the render cycle to the transition animation.
internal var displayLink: CADisplayLink?
/// An Array of observers that are updated during a transition.
internal var transitionObservers: [MotionTransitionObserver]?
/// Max duration used by MotionAnimators and MotionPlugins.
public internal(set) var totalDuration: TimeInterval = 0
/// The currently running animation duration.
internal var currentAnimationDuration: TimeInterval = 0
/// The start time of the animation.
internal var beginTime: TimeInterval? {
didSet {
guard nil != beginTime else {
displayLink?.isPaused = true
displayLink?.remove(from: RunLoop.main, forMode: RunLoopMode(rawValue: RunLoopMode.commonModes.rawValue))
displayLink = nil
return
}
guard nil == displayLink else {
return
}
displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink?.add(to: RunLoop.main, forMode: RunLoopMode(rawValue: RunLoopMode.commonModes.rawValue))
}
}
/// A boolean indicating if the transition has finished.
internal var isFinished = true
/// An Array of MotionPreprocessors used during a transition.
internal fileprivate(set) lazy var preprocessors = [MotionPreprocessor]()
/// An Array of MotionAnimators used during a transition.
internal fileprivate(set) lazy var animators = [MotionAnimator]()
/// An Array of MotionPlugins used during a transition.
internal fileprivate(set) lazy var plugins = [MotionPlugin]()
/// The matching fromViews to toViews based on the motionIdentifier value.
internal fileprivate(set) lazy var transitionPairs = [(fromViews: [UIView], toViews: [UIView])]()
/// Plugins that are enabled during the transition.
internal static var enabledPlugins = [MotionPlugin.Type]()
/// Initializer.
internal override init() {}
}
public extension MotionController {
/**
Receive callbacks on each animation frame.
Observers will be cleaned when a transition completes.
- Parameter observer: A MotionTransitionObserver.
*/
func addTransitionObserver(observer: MotionTransitionObserver) {
if nil == transitionObservers {
transitionObservers = []
}
transitionObservers?.append(observer)
}
}
fileprivate extension MotionController {
/// Updates the transition observers.
func updateTransitionObservers() {
guard let observers = transitionObservers else {
return
}
for v in observers {
v.motion(transitionObserver: v, didUpdateWith: elapsedTime)
}
}
/// Updates the animators.
func updateAnimators() {
let t = elapsedTime * totalDuration
for a in animators {
a.seek(to: t)
}
}
/// Updates the plugins.
func updatePlugins() {
let t = elapsedTime * totalDuration
for p in plugins where p.requirePerFrameCallback {
p.seek(to: t)
}
}
}
fileprivate extension MotionController {
/**
Handler for the DisplayLink updates.
- Parameter _ link: CADisplayLink.
*/
@objc
func handleDisplayLink(_ link: CADisplayLink) {
guard isTransitioning else {
return
}
guard 0 < currentAnimationDuration else {
return
}
guard let t = beginTime else {
return
}
let cTime = CACurrentMediaTime() - t
if cTime > currentAnimationDuration {
elapsedTime = isFinished ? 1 : 0
beginTime = nil
complete(isFinished: isFinished)
} else {
var eTime = cTime / totalDuration
if !isFinished {
eTime = 1 - eTime
}
elapsedTime = max(0, min(1, eTime))
}
}
}
public extension MotionController {
/**
Updates the elapsed time for the interactive transition.
- Parameter elapsedTime t: the current progress, must be between -1...1.
*/
public func update(elapsedTime t: TimeInterval) {
guard isTransitioning else {
return
}
beginTime = nil
elapsedTime = max(-1, min(1, t))
}
/**
Finish the interactive transition.
Will stop the interactive transition and animate from the
current state to the **end** state
- Parameter isAnimated: A boolean indicating if the completion is animated.
*/
public func end(isAnimated: Bool = true) {
guard isTransitioning else {
return
}
guard isAnimated else {
complete(isFinished: true)
return
}
var t: TimeInterval = 0
for a in animators {
t = max(t, a.resume(at: elapsedTime * totalDuration, isReversed: false))
}
complete(after: t, isFinished: true)
}
/**
Cancel the interactive transition.
Will stop the interactive transition and animate from the
current state to the **begining** state
- Parameter isAnimated: A boolean indicating if the completion is animated.
*/
public func cancel(isAnimated: Bool = true) {
guard isTransitioning else {
return
}
guard isAnimated else {
complete(isFinished: false)
return
}
var d: TimeInterval = 0
for a in animators {
var t = elapsedTime
if t < 0 {
t = -t
}
d = max(d, a.resume(at: t * totalDuration, isReversed: true))
}
complete(after: d, isFinished: false)
}
/**
Override transition animations during an interactive animation.
For example:
Motion.shared.apply([.position(x:50, y:50)], to: view)
will set the view's position to 50, 50
- Parameter transitions: An Array of MotionTransitions.
- Parameter to view: A UIView.
*/
public func apply(transitions: [MotionTransition], to view: UIView) {
guard isTransitioning else {
return
}
let s = MotionTransitionState(transitions: transitions)
let v = context.transitionPairedView(for: view) ?? view
for a in animators {
a.apply(state: s, to: v)
}
}
}
internal extension MotionController {
/**
Load plugins, processors, animators, container, & context
The transitionContainer must already be set.
Subclasses should call context.set(fromViews: toViews) after
inserting fromViews & toViews into the container
*/
func prepareTransition() {
guard isTransitioning else {
return
}
prepareTransitionContainer()
prepareContext()
preparePreprocessors()
prepareAnimators()
preparePlugins()
}
/// Prepares the transition fromView & toView pairs.
func prepareTransitionPairs() {
guard isTransitioning else {
return
}
for a in animators {
let fv = context.fromViews.filter { (view: UIView) -> Bool in
return a.canAnimate(view: view, isAppearing: false)
}
let tv = context.toViews.filter {
return a.canAnimate(view: $0, isAppearing: true)
}
transitionPairs.append((fv, tv))
}
}
}
internal extension MotionController {
/// Executes the preprocessors' process function.
func processContext() {
guard isTransitioning else {
return
}
for x in preprocessors {
x.process(fromViews: context.fromViews, toViews: context.toViews)
}
}
/**
Animates the views. Subclasses should call `prepareTransition` &
`prepareTransitionPairs` before calling `animate`.
*/
func animate() {
guard isTransitioning else {
return
}
for (fv, tv) in transitionPairs {
for view in fv {
context.hide(view: view)
}
for view in tv {
context.hide(view: view)
}
}
var t: TimeInterval = 0
var b = false
for (i, a) in animators.enumerated() {
let d = a.animate(fromViews: transitionPairs[i].0, toViews: transitionPairs[i].1)
if .infinity == d {
b = true
} else {
t = max(t, d)
}
}
totalDuration = t
if b {
update(elapsedTime: 0)
} else {
complete(after: t, isFinished: true)
}
}
/**
Complete the transition.
- Parameter after: A TimeInterval.
- Parameter isFinished: A Boolean indicating if the transition
has completed.
*/
func complete(after: TimeInterval, isFinished: Bool) {
guard isTransitioning else {
return
}
if after <= 0.001 {
complete(isFinished: isFinished)
return
}
let v = (isFinished ? elapsedTime : 1 - elapsedTime) * totalDuration
self.isFinished = isFinished
currentAnimationDuration = after + v
beginTime = CACurrentMediaTime() - v
}
/**
Complete the transition.
- Parameter isFinished: A Boolean indicating if the transition
has completed.
*/
func complete(isFinished: Bool) {
guard isTransitioning else {
return
}
for a in animators {
a.clean()
}
transitionContainer?.isUserInteractionEnabled = true
let completion = completionCallback
transitionObservers = nil
transitionContainer = nil
completionCallback = nil
container = nil
context = nil
beginTime = nil
elapsedTime = 0
totalDuration = 0
preprocessors.removeAll()
animators.removeAll()
plugins.removeAll()
transitionPairs.removeAll()
completion?(isFinished)
}
}
fileprivate extension MotionController {
/// Prepares the transition container.
func prepareTransitionContainer() {
guard let v = transitionContainer else {
return
}
v.isUserInteractionEnabled = false
// a view to hold all the animating views
container = UIView(frame: v.bounds)
v.addSubview(container!)
}
/// Prepares the context.
func prepareContext() {
guard let v = container else {
return
}
context = MotionContext(container: v)
}
/// Prepares the preprocessors.
func preparePreprocessors() {
for x in [
IgnoreSubviewTransitionsPreprocessor(),
MatchPreprocessor(),
SourcePreprocessor(),
CascadePreprocessor(),
DurationPreprocessor()] as [MotionPreprocessor] {
preprocessors.append(x)
}
for x in preprocessors {
x.context = context
}
}
/// Prepares the animators.
func prepareAnimators() {
animators.append(MotionTransitionAnimator<MotionCoreAnimationViewContext>())
if #available(iOS 10, tvOS 10, *) {
animators.append(MotionTransitionAnimator<MotionViewPropertyViewContext>())
}
for v in animators {
v.context = context
}
}
/// Prepares the plugins.
func preparePlugins() {
for x in Motion.enabledPlugins.map({
return $0.init()
}) {
plugins.append(x)
}
for plugin in plugins {
preprocessors.append(plugin)
animators.append(plugin)
}
}
}
internal extension MotionController {
/**
Checks if a given plugin is enabled.
- Parameter plugin: A MotionPlugin.Type.
- Returns: A boolean indicating if the plugin is enabled or not.
*/
static func isEnabled(plugin: MotionPlugin.Type) -> Bool {
return nil != enabledPlugins.index(where: { return $0 == plugin })
}
/**
Enables a given plugin.
- Parameter plugin: A MotionPlugin.Type.
*/
static func enable(plugin: MotionPlugin.Type) {
disable(plugin: plugin)
enabledPlugins.append(plugin)
}
/**
Disables a given plugin.
- Parameter plugin: A MotionPlugin.Type.
*/
static func disable(plugin: MotionPlugin.Type) {
guard let index = enabledPlugins.index(where: { return $0 == plugin }) else {
return
}
enabledPlugins.remove(at: index)
}
}
internal extension MotionController {
// should call this after `prepareTransitionPairs` & before `processContext`
func insert<T>(preprocessor: MotionPreprocessor, before: T.Type) {
let i = preprocessors.index { $0 is T } ?? preprocessors.count
preprocessor.context = context
preprocessors.insert(preprocessor, at: i)
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
public enum MotionCoordinateSpace {
case global
case local
case sameParent
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
public class MotionIndependentController: MotionController {
/// An initializer.
public override init() {
super.init()
}
/**
Transitions source views to their corresponding destination view
within a given root view.
- Parameter rootView: A UIView.
- Parameter fromViews: An Array of UIViews.
- Parameter toViews: An Array of UIViews.
- Parameter completion: An optional callback.
*/
public func transition(rootView: UIView, fromViews: [UIView], toViews: [UIView], completion: ((Bool) -> Void)? = nil) {
transitionContainer = rootView
completionCallback = completion
prepareTransition()
prepareContext(fromViews: fromViews, toViews: toViews)
prepareTransitionPairs()
animate()
}
}
fileprivate extension MotionIndependentController {
/**
Prepares the context.
- Parameter fromViews: An Array of UIViews.
- PArameter toViews: An Array of UIViews.
*/
func prepareContext(fromViews: [UIView], toViews: [UIView]) {
context.defaultCoordinateSpace = .sameParent
context.set(fromViews: fromViews, toViews: toViews)
processContext()
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
open class MotionPlugin: NSObject, MotionPreprocessor, MotionAnimator {
weak public var context: MotionContext!
/**
Determines whether or not to receive `seekTo` callback on every frame.
Default is false.
When **requirePerFrameCallback** is **false**, the plugin needs to start its own animations inside `animate` & `resume`
The `seekTo` method is only being called during an interactive transition.
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`
*/
open var requirePerFrameCallback = false
public override required init() {}
/**
Called before any animation.
Override this method when you want to preprocess transitions for views
- Parameters:
- context: object holding all parsed and changed transitions,
- fromViews: A flattened list of all views from source ViewController
- toViews: A flattened list of all views from destination ViewController
To check a view's transitions:
context[view]
context[view, "transitionName"]
To set a view's transitions:
context[view] = [("transition1", ["parameter1"]), ("transition2", [])]
context[view, "transition1"] = ["parameter1", "parameter2"]
*/
open func process(fromViews: [UIView], toViews: [UIView]) {}
/**
- Returns: return true if the plugin can handle animating the view.
- Parameters:
- context: object holding all parsed and changed transitions,
- view: the view to check whether or not the plugin can handle the animation
- isAppearing: true if the view is isAppearing(i.e. a view in destination ViewController)
If return true, Motion won't animate and won't let any other plugins animate this view.
The view will also be hidden automatically during the animation.
*/
open func canAnimate(view: UIView, isAppearing: Bool) -> Bool { return false }
/**
Perform the animation.
Note: views in `fromViews` & `toViews` are hidden already. Unhide then if you need to take snapshots.
- Parameters:
- context: object holding all parsed and changed transitions,
- fromViews: A flattened list of all views from source ViewController (filtered by `canAnimate`)
- toViews: A flattened list of all views from destination ViewController (filtered by `canAnimate`)
- Returns: The duration needed to complete the animation
*/
open func animate(fromViews: [UIView], toViews: [UIView]) -> TimeInterval { return 0 }
/**
Called when all animations are completed.
Should perform cleanup and release any reference
*/
open func clean() {}
/**
For supporting interactive animation only.
This method is called when an interactive animation is in place
The plugin should pause the animation, and seek to the given progress
- Parameters:
- elapsedTime: time of the animation to seek to.
*/
open func seek(to elapsedTime: TimeInterval) {}
/**
For supporting interactive animation only.
This method is called when an interactive animation is ended
The plugin should resume the animation.
- Parameters:
- elapsedTime: will be the same value since last `seekTo`
- reverse: a boolean value indicating whether or not the animation should reverse
*/
open func resume(at elapsedTime: TimeInterval, isReversed: Bool) -> TimeInterval { return 0 }
/**
For supporting interactive animation only.
This method is called when user wants to override animation transitions during an interactive animation
- Parameters:
- state: the target state to override
- view: the view to override
*/
open func apply(state: MotionTransitionState, to view: UIView) {}
}
// methods for enable/disable the current plugin
extension MotionPlugin {
public static var isEnabled: Bool {
get {
return Motion.isEnabled(plugin: self)
}
set {
if newValue {
enable()
} else {
disable()
}
}
}
public static func enable() {
Motion.enable(plugin: self)
}
public static func disable() {
Motion.disable(plugin: self)
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
public enum MotionSnapshotType {
/**
This setting will optimize for different types of views.
For custom views or views with masking, .optimizedDefault might
create snapshots that appear differently than the actual view.
In that case, use .normal or .slowRender to disable the optimization.
*/
case optimized
/// snapshotView(afterScreenUpdates:)
case normal
/// layer.render(in: currentContext)
case layerRender
/**
This setting will not create a snapshot. It will animate the view directly.
This will mess up the view hierarchy, therefore, view controllers have to rebuild
their view structure after the transition finishes.
*/
case noSnapshot
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
public class MotionTransition {
/// A reference to the callback that applies the MotionTransitionState.
internal let apply: (inout MotionTransitionState) -> Void
/**
An initializer that accepts a given callback.
- Parameter applyFunction: A given callback.
*/
init(applyFunction: @escaping (inout MotionTransitionState) -> Void) {
apply = applyFunction
}
}
public extension MotionTransition {
/**
Animates the view with a matching motion identifier.
- Parameter _ identifier: A String.
- Returns: A MotionTransition.
*/
static func motionIdentifier(_ identifier: String) -> MotionTransition {
return MotionTransition {
$0.motionIdentifier = identifier
}
}
/**
Animates the view's current masksToBounds to the
given masksToBounds.
- Parameter masksToBounds: A boolean value indicating the
masksToBounds state.
- Returns: A MotionTransition.
*/
static func masksToBounds(_ masksToBounds: Bool) -> MotionTransition {
return MotionTransition {
$0.masksToBounds = masksToBounds
}
}
/**
Animates the view's current background color to the
given color.
- Parameter color: A UIColor.
- Returns: A MotionTransition.
*/
static func background(color: UIColor) -> MotionTransition {
return MotionTransition {
$0.backgroundColor = color.cgColor
}
}
/**
Animates the view's current border color to the
given color.
- Parameter color: A UIColor.
- Returns: A MotionTransition.
*/
static func border(color: UIColor) -> MotionTransition {
return MotionTransition {
$0.borderColor = color.cgColor
}
}
/**
Animates the view's current border width to the
given width.
- Parameter width: A CGFloat.
- Returns: A MotionTransition.
*/
static func border(width: CGFloat) -> MotionTransition {
return MotionTransition {
$0.borderWidth = width
}
}
/**
Animates the view's current corner radius to the
given radius.
- Parameter radius: A CGFloat.
- Returns: A MotionTransition.
*/
static func corner(radius: CGFloat) -> MotionTransition {
return MotionTransition {
$0.cornerRadius = radius
}
}
/**
Animates the view's current transform (perspective, scale, rotate)
to the given one.
- Parameter _ transform: A CATransform3D.
- Returns: A MotionTransition.
*/
static func transform(_ transform: CATransform3D) -> MotionTransition {
return MotionTransition {
$0.transform = transform
}
}
/**
Animates the view's current perspective to the given one through
a CATransform3D object.
- Parameter _ perspective: A CGFloat.
- Returns: A MotionTransition.
*/
static func perspective(_ perspective: CGFloat) -> MotionTransition {
return MotionTransition {
var t = $0.transform ?? CATransform3DIdentity
t.m34 = 1 / -perspective
$0.transform = t
}
}
/**
Animates the view's current rotate to the given x, y,
and z values.
- Parameter x: A CGFloat.
- Parameter y: A CGFloat.
- Parameter z: A CGFloat.
- Returns: A MotionTransition.
*/
static func rotate(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) -> MotionTransition {
return MotionTransition {
var t = $0.transform ?? CATransform3DIdentity
t = CATransform3DRotate(t, CGFloat(Double.pi) * x / 180, 1, 0, 0)
t = CATransform3DRotate(t, CGFloat(Double.pi) * y / 180, 0, 1, 0)
$0.transform = CATransform3DRotate(t, CGFloat(Double.pi) * z / 180, 0, 0, 1)
}
}
/**
Animates the view's current rotate to the given point.
- Parameter _ point: A CGPoint.
- Parameter z: A CGFloat, default is 0.
- Returns: A MotionTransition.
*/
static func rotate(_ point: CGPoint, z: CGFloat = 0) -> MotionTransition {
return .rotate(x: point.x, y: point.y, z: z)
}
/**
Rotate 2d.
- Parameter _ z: A CGFloat.
- Returns: A MotionTransition.
*/
static func rotate(_ z: CGFloat) -> MotionTransition {
return .rotate(z: z)
}
/**
Animates the view's current scale to the given x, y, z scale values.
- Parameter x: A CGFloat.
- Parameter y: A CGFloat.
- Parameter z: A CGFloat.
- Returns: A MotionTransition.
*/
static func scale(x: CGFloat = 1, y: CGFloat = 1, z: CGFloat = 1) -> MotionTransition {
return MotionTransition {
$0.transform = CATransform3DScale($0.transform ?? CATransform3DIdentity, x, y, z)
}
}
/**
Animates the view's current x & y scale to the given scale value.
- Parameter _ xy: A CGFloat.
- Returns: A MotionTransition.
*/
static func scale(_ xy: CGFloat) -> MotionTransition {
return .scale(x: xy, y: xy)
}
/**
Animates the view's current translation to the given
x, y, and z values.
- Parameter x: A CGFloat.
- Parameter y: A CGFloat.
- Parameter z: A CGFloat.
- Returns: A MotionTransition.
*/
static func translate(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) -> MotionTransition {
return MotionTransition {
$0.transform = CATransform3DTranslate($0.transform ?? CATransform3DIdentity, x, y, z)
}
}
/**
Animates the view's current translation to the given
point value (x & y), and a z value.
- Parameter _ point: A CGPoint.
- Parameter z: A CGFloat, default is 0.
- Returns: A MotionTransition.
*/
static func translate(_ point: CGPoint, z: CGFloat = 0) -> MotionTransition {
return .translate(x: point.x, y: point.y, z: z)
}
/**
Animates the view's current position to the given point.
- Parameter _ point: A CGPoint.
- Returns: A MotionTransition.
*/
static func position(_ point: CGPoint) -> MotionTransition {
return MotionTransition {
$0.position = point
}
}
/// Forces the view to not fade during a transition.
static var forceNonFade = MotionTransition {
$0.nonFade = true
}
/// Fades the view in during a transition.
static var fadeIn = MotionTransition.fade(1)
/// Fades the view out during a transition.
static var fadeOut = MotionTransition.fade(0)
/**
Animates the view's current opacity to the given one.
- Parameter to opacity: A Double.
- Returns: A MotionTransition.
*/
static func fade(_ opacity: Double) -> MotionTransition {
return MotionTransition {
$0.opacity = opacity
}
}
/**
Animates the view's current zPosition to the given position.
- Parameter _ position: An Int.
- Returns: A MotionTransition.
*/
static func zPosition(_ position: CGFloat) -> MotionTransition {
return MotionTransition {
$0.zPosition = position
}
}
/**
Animates the view's current size to the given one.
- Parameter _ size: A CGSize.
- Returns: A MotionTransition.
*/
static func size(_ size: CGSize) -> MotionTransition {
return MotionTransition {
$0.size = size
}
}
/**
Animates the view's current shadow path to the given one.
- Parameter path: A CGPath.
- Returns: A MotionTransition.
*/
static func shadow(path: CGPath) -> MotionTransition {
return MotionTransition {
$0.shadowPath = path
}
}
/**
Animates the view's current shadow color to the given one.
- Parameter color: A UIColor.
- Returns: A MotionTransition.
*/
static func shadow(color: UIColor) -> MotionTransition {
return MotionTransition {
$0.shadowColor = color.cgColor
}
}
/**
Animates the view's current shadow offset to the given one.
- Parameter offset: A CGSize.
- Returns: A MotionTransition.
*/
static func shadow(offset: CGSize) -> MotionTransition {
return MotionTransition {
$0.shadowOffset = offset
}
}
/**
Animates the view's current shadow opacity to the given one.
- Parameter opacity: A Float.
- Returns: A MotionTransition.
*/
static func shadow(opacity: Float) -> MotionTransition {
return MotionTransition {
$0.shadowOpacity = opacity
}
}
/**
Animates the view's current shadow radius to the given one.
- Parameter radius: A CGFloat.
- Returns: A MotionTransition.
*/
static func shadow(radius: CGFloat) -> MotionTransition {
return MotionTransition {
$0.shadowRadius = radius
}
}
/**
Animates the view's contents rect to the given one.
- Parameter rect: A CGRect.
- Returns: A MotionTransition.
*/
static func contents(rect: CGRect) -> MotionTransition {
return MotionTransition {
$0.contentsRect = rect
}
}
/**
Animates the view's contents scale to the given one.
- Parameter scale: A CGFloat.
- Returns: A MotionTransition.
*/
static func contents(scale: CGFloat) -> MotionTransition {
return MotionTransition {
$0.contentsScale = scale
}
}
/**
The duration of the view's animation.
- Parameter _ duration: A TimeInterval.
- Returns: A MotionTransition.
*/
static func duration(_ duration: TimeInterval) -> MotionTransition {
return MotionTransition {
$0.duration = duration
}
}
/**
Sets the view's animation duration to the longest
running animation within a transition.
*/
static var preferredDurationMatchesLongest = MotionTransition.duration(.infinity)
/**
Delays the animation of a given view.
- Parameter _ time: TimeInterval.
- Returns: A MotionTransition.
*/
static func delay(_ time: TimeInterval) -> MotionTransition {
return MotionTransition {
$0.delay = time
}
}
/**
Sets the view's timing function for the transition.
- Parameter _ timingFunction: A CAMediaTimingFunction.
- Returns: A MotionTransition.
*/
static func timingFunction(_ timingFunction: CAMediaTimingFunction) -> MotionTransition {
return MotionTransition {
$0.timingFunction = timingFunction
}
}
/**
Available in iOS 9+, animates a view using the spring API,
given a stiffness and damping.
- Parameter stiffness: A CGFlloat.
- Parameter damping: A CGFloat.
- Returns: A MotionTransition.
*/
@available(iOS 9, *)
static func spring(stiffness: CGFloat, damping: CGFloat) -> MotionTransition {
return MotionTransition {
$0.spring = (stiffness, damping)
}
}
/**
Animates the natural curve of a view. A value of 1 represents
a curve in a downward direction, and a value of -1
represents a curve in an upward direction.
- Parameter intensity: A CGFloat.
- Returns: A MotionTransition.
*/
static func arc(intensity: CGFloat = 1) -> MotionTransition {
return MotionTransition {
$0.arc = intensity
}
}
/**
Animates subviews with an increasing delay between each animation.
- Parameter delta: A TimeInterval.
- Parameter direction: A CascadeDirection.
- Parameter animationDelayedUntilMatchedViews: A boolean indicating whether
or not to delay the subview animation until all have started.
- Returns: A MotionTransition.
*/
static func cascade(delta: TimeInterval = 0.02, direction: CascadeDirection = .topToBottom, animationDelayedUntilMatchedViews: Bool = false) -> MotionTransition {
return MotionTransition {
$0.cascade = (delta, direction, animationDelayedUntilMatchedViews)
}
}
/**
Creates an overlay on the animating view with a given color and opacity.
- Parameter color: A UIColor.
- Parameter opacity: A CGFloat.
- Returns: A MotionTransition.
*/
static func overlay(color: UIColor, opacity: CGFloat) -> MotionTransition {
return MotionTransition {
$0.overlay = (color.cgColor, opacity)
}
}
}
public extension MotionTransition {
/**
Apply transitions directly to the view at the start of the transition.
The transitions supplied here won't be animated.
For source views, transitions are set directly at the begining of the animation.
For destination views, they replace the target state (final appearance).
*/
static func beginWith(transitions: [MotionTransition]) -> MotionTransition {
return MotionTransition {
if $0.beginState == nil {
$0.beginState = MotionTransitionStateWrapper(state: [])
}
$0.beginState?.state.append(contentsOf: transitions)
}
}
/**
Apply transitions directly to the view at the start of the transition if the view is
matched with another view. The transitions supplied here won't be animated.
For source views, transitions are set directly at the begining of the animation.
For destination views, they replace the target state (final appearance).
*/
static func beginWithIfMatched(transitions: [MotionTransition]) -> MotionTransition {
return MotionTransition {
if $0.beginStateIfMatched == nil {
$0.beginStateIfMatched = []
}
$0.beginStateIfMatched?.append(contentsOf: transitions)
}
}
/**
Use global coordinate space.
When using global coordinate space. The view becomes an independent view that is not
a subview of any view. It won't move when its parent view moves, and won't be affected
by parent view attributes.
When a view is matched, this is automatically enabled.
The `source` transition will also enable this.
*/
static var useGlobalCoordinateSpace = MotionTransition {
$0.coordinateSpace = .global
}
/// Use same parent coordinate space.
static var useSameParentCoordinateSpace = MotionTransition {
$0.coordinateSpace = .sameParent
}
/// Ignore all motion transition attributes for a view's direct subviews.
static var ignoreSubviewTransitions: MotionTransition = .ignoreSubviewTransitions()
/**
Ignore all motion transition attributes for a view's subviews.
- Parameter recursive: If false, will only ignore direct subviews' transitions.
default false.
*/
static func ignoreSubviewTransitions(recursive: Bool = false) -> MotionTransition {
return MotionTransition {
$0.ignoreSubviewTransitions = recursive
}
}
/**
This will create a snapshot optimized for different view types.
For custom views or views with masking, useOptimizedSnapshot might create snapshots
that appear differently than the actual view.
In that case, use .useNormalSnapshot or .useSlowRenderSnapshot to disable the optimization.
This transition actually does nothing by itself since .useOptimizedSnapshot is the default.
*/
static var useOptimizedSnapshot = MotionTransition {
$0.snapshotType = .optimized
}
/// Create a snapshot using snapshotView(afterScreenUpdates:).
static var useNormalSnapshot = MotionTransition {
$0.snapshotType = .normal
}
/**
Create a snapshot using layer.render(in: currentContext).
This is slower than .useNormalSnapshot but gives more accurate snapshots for some views
(eg. UIStackView).
*/
static var useLayerRenderSnapshot = MotionTransition {
$0.snapshotType = .layerRender
}
/**
Force Motion to not create any snapshots when animating this view.
This will mess up the view hierarchy, therefore, view controllers have to rebuild
their view structure after the transition finishes.
*/
static var useNoSnapshot = MotionTransition {
$0.snapshotType = .noSnapshot
}
/**
Force the view to animate (Motion will create animation contexts & snapshots for them, so
that they can be interactive).
*/
static var forceAnimate = MotionTransition {
$0.forceAnimate = true
}
/**
Force Motion to use scale based size animation. This will convert all .size transitions into
a .scale transition. This is to help Motion animate layers that doesn't support bounds animations.
This also gives better performance.
*/
static var useScaleBasedSizeChange = MotionTransition {
$0.useScaleBasedSizeChange = true
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import Foundation
public protocol MotionTransitionObserver {
/**
Executed when the elapsed time changes during a transition.
- Parameter transitionObserver: A MotionTransitionObserver.
- Parameter didUpdateWith elapsedTime: A TimeInterval.
*/
func motion(transitionObserver: MotionTransitionObserver, didUpdateWith elapsedTime: TimeInterval)
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
internal class MotionTransitionStateWrapper {
/// A reference to a MotionTransitionState.
internal var state: MotionTransitionState
/**
An initializer that accepts a given MotionTransitionState.
- Parameter state: A MotionTransitionState.
*/
internal init(state: MotionTransitionState) {
self.state = state
}
}
public struct MotionTransitionState {
/// The initial state that the transition will start at.
internal var beginState: MotionTransitionStateWrapper?
/// The start state if there is a match in the desition view controller.
public var beginStateIfMatched: [MotionTransition]?
/// A reference to the position.
public var position: CGPoint?
/// A reference to the size.
public var size: CGSize?
/// A reference to the transform.
public var transform: CATransform3D?
/// A reference to the opacity.
public var opacity: Double?
/// A reference to the cornerRadius.
public var cornerRadius: CGFloat?
/// A reference to the backgroundColor.
public var backgroundColor: CGColor?
/// A reference to the zPosition.
public var zPosition: CGFloat?
/// A reference to the contentsRect.
public var contentsRect: CGRect?
/// A reference to the contentsScale.
public var contentsScale: CGFloat?
/// A reference to the borderWidth.
public var borderWidth: CGFloat?
/// A reference to the borderColor.
public var borderColor: CGColor?
/// A reference to the shadowColor.
public var shadowColor: CGColor?
/// A reference to the shadowOpacity.
public var shadowOpacity: Float?
/// A reference to the shadowOffset.
public var shadowOffset: CGSize?
/// A reference to the shadowRadius.
public var shadowRadius: CGFloat?
/// A reference to the shadowPath.
public var shadowPath: CGPath?
/// A boolean for the masksToBounds state.
public var masksToBounds: Bool?
/// A boolean indicating whether to display a shadow or not.
public var displayShadow = true
/// A reference to the overlay settings.
public var overlay: (color: CGColor, opacity: CGFloat)?
/// A reference to the spring animation settings.
public var spring: (CGFloat, CGFloat)?
/// A time delay on starting the animation.
public var delay: TimeInterval = 0
/// The duration of the animation.
public var duration: TimeInterval?
/// The timing function value of the animation.
public var timingFunction: CAMediaTimingFunction?
/// The arc curve value.
public var arc: CGFloat?
/// The identifier value to match source and destination views.
public var motionIdentifier: String?
/// The cascading animation settings.
public var cascade: (TimeInterval, CascadeDirection, Bool)?
/**
A boolean indicating whether to ignore the subview transition
animations or not.
*/
public var ignoreSubviewTransitions: Bool?
/// The coordinate space to transition views within.
public var coordinateSpace: MotionCoordinateSpace?
/// Change the size of a view based on a scale factor.
public var useScaleBasedSizeChange: Bool?
/// The type of snapshot to use.
public var snapshotType: MotionSnapshotType?
/// Do not fade the view when transitioning.
public var nonFade = false
/// Force an animation.
public var forceAnimate = false
/// Custom target states.
public var custom: [String: Any]?
/**
An initializer that accepts an Array of MotionTransitions.
- Parameter transitions: An Array of MotionTransitions.
*/
init(transitions: [MotionTransition]) {
append(contentsOf: transitions)
}
}
extension MotionTransitionState {
/**
Adds a MotionTransition to the current state.
- Parameter _ element: A MotionTransition.
*/
public mutating func append(_ element: MotionTransition) {
element.apply(&self)
}
/**
Adds an Array of MotionTransitions to the current state.
- Parameter contentsOf elements: An Array of MotionTransitions.
*/
public mutating func append(contentsOf elements: [MotionTransition]) {
for v in elements {
v.apply(&self)
}
}
/**
A subscript that returns a custom value for a specified key.
- Parameter key: A String.
- Returns: An optional Any value.
*/
public subscript(key: String) -> Any? {
get {
return custom?[key]
}
set(value) {
if nil == custom {
custom = [:]
}
custom![key] = value
}
}
}
extension MotionTransitionState: ExpressibleByArrayLiteral {
/**
An initializer implementing the ExpressibleByArrayLiteral protocol.
- Parameter arrayLiteral elements: A list of MotionTransitions.
*/
public init(arrayLiteral elements: MotionTransition...) {
append(contentsOf: elements)
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
public enum CascadeDirection {
case topToBottom
case bottomToTop
case leftToRight
case rightToLeft
case radial(center:CGPoint)
case inverseRadial(center:CGPoint)
/// Based on the cascade direction a comparator is set.
var comparator: (UIView, UIView) -> Bool {
switch self {
case .topToBottom:
return { return $0.frame.minY < $1.frame.minY }
case .bottomToTop:
return { return $0.frame.maxY == $1.frame.maxY ? $0.frame.maxX > $1.frame.maxX : $0.frame.maxY > $1.frame.maxY }
case .leftToRight:
return { return $0.frame.minX < $1.frame.minX }
case .rightToLeft:
return { return $0.frame.maxX > $1.frame.maxX }
case .radial(let center):
return { return $0.center.distance(center) < $1.center.distance(center) }
case .inverseRadial(let center):
return { return $0.center.distance(center) > $1.center.distance(center) }
}
}
}
class CascadePreprocessor: MotionPreprocessor {
/// A reference to a MotionContext.
weak var context: MotionContext!
/**
Processes the transitionary views.
- Parameter fromViews: An Array of UIViews.
- Parameter toViews: An Array of UIViews.
*/
func process(fromViews: [UIView], toViews: [UIView]) {
process(views: fromViews)
process(views: toViews)
}
/**
Process an Array of views for the cascade animation.
- Parameter views: An Array of UIViews.
*/
func process(views: [UIView]) {
for v in views {
guard let (deltaTime, direction, delayMatchedViews) = context[v]?.cascade else {
continue
}
let parentView = v is UITableView ? v.subviews.get(0) ?? v : v
let sortedSubviews = parentView.subviews.sorted(by: direction.comparator)
let initialDelay = context[v]!.delay
let finalDelay = TimeInterval(sortedSubviews.count) * deltaTime + initialDelay
for (i, subview) in sortedSubviews.enumerated() {
let delay = TimeInterval(i) * deltaTime + initialDelay
func applyDelay(view: UIView) {
if context.transitionPairedView(for: view) == nil {
context[view]?.delay = delay
} else if delayMatchedViews, let paired = context.transitionPairedView(for: view) {
context[view]?.delay = finalDelay
context[paired]?.delay = finalDelay
}
for subview in view.subviews {
applyDelay(view: subview)
}
}
applyDelay(view: subview)
}
}
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
class DurationPreprocessor: MotionPreprocessor {
/// A reference to a MotionContext.
weak var context: MotionContext!
/**
Processes the transitionary views.
- Parameter fromViews: An Array of UIViews.
- Parameter toViews: An Array of UIViews.
*/
func process(fromViews: [UIView], toViews: [UIView]) {
var maxDuration: TimeInterval = 0
maxDuration = applyOptimizedDurationIfNoDuration(views:fromViews)
maxDuration = max(maxDuration, applyOptimizedDurationIfNoDuration(views:toViews))
setDurationForInfiniteDuration(views: fromViews, duration: maxDuration)
setDurationForInfiniteDuration(views: toViews, duration: maxDuration)
}
/**
Retrieves the optimized duration for a given UIView.
- Parameter for view: A UIView.
- Returns: A TimeInterval.
*/
func optimizedDuration(for view: UIView) -> TimeInterval {
let v = context[view]!
return view.optimizedDuration(fromPosition: context.container.convert(view.layer.position, from: view.superview),
toPosition: v.position,
size: v.size,
transform: v.transform)
}
/**
Applies the optimized duration for an Array of UIViews.
- Parameter views: An Array of UIViews.
- Returns: A TimeInterval.
*/
func applyOptimizedDurationIfNoDuration(views: [UIView]) -> TimeInterval {
var d: TimeInterval = 0
for v in views where nil != context[v] {
if nil == context[v]?.duration {
context[v]!.duration = optimizedDuration(for: v)
}
d = .infinity == context[v]!.duration! ?
max(d, optimizedDuration(for: v)) :
max(d, context[v]!.duration!)
}
return d
}
/**
Sets the duration if the duration of a transition is set to `.infinity`.
- Parameter views: An Array of UIViews.
- Parameter duration: A TimeInterval.
*/
func setDurationForInfiniteDuration(views: [UIView], duration: TimeInterval) {
for v in views where .infinity == context[v]?.duration {
context[v]!.duration = duration
}
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
class IgnoreSubviewTransitionsPreprocessor: MotionPreprocessor {
/// A reference to a MotionContext.
weak var context: MotionContext!
/**
Processes the transitionary views.
- Parameter fromViews: An Array of UIViews.
- Parameter toViews: An Array of UIViews.
*/
func process(fromViews: [UIView], toViews: [UIView]) {
process(views:fromViews)
process(views:toViews)
}
/**
Process an Array of views for the cascade animation.
- Parameter views: An Array of UIViews.
*/
func process(views: [UIView]) {
for v in views {
guard let recursive = context[v]?.ignoreSubviewTransitions else {
continue
}
let parentView = v is UITableView ? v.subviews.get(0) ?? v : v
guard recursive else {
for subview in parentView.subviews {
context[subview] = nil
}
continue
}
cleanSubviewTransitions(for: parentView)
}
}
}
extension IgnoreSubviewTransitionsPreprocessor {
/**
Clears the transition for a given view's subviews.
- Parameter for view: A UIView.
*/
fileprivate func cleanSubviewTransitions(for view: UIView) {
for v in view.subviews {
context[v] = nil
cleanSubviewTransitions(for: v)
}
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
class MatchPreprocessor: MotionPreprocessor {
/// A reference to a MotionContext.
weak var context: MotionContext!
/**
Processes the transitionary views.
- Parameter fromViews: An Array of UIViews.
- Parameter toViews: An Array of UIViews.
*/
func process(fromViews: [UIView], toViews: [UIView]) {
for tv in toViews {
guard let i = tv.motionIdentifier, let fv = context.sourceView(for: i) else { continue }
var tvState = context[tv] ?? MotionTransitionState()
var fvState = context[fv] ?? MotionTransitionState()
if let v = tvState.beginStateIfMatched {
tvState.append(.beginWith(transitions: v))
}
if let v = fvState.beginStateIfMatched {
fvState.append(.beginWith(transitions: v))
}
tvState.motionIdentifier = i
tvState.opacity = 0
fvState.motionIdentifier = i
fvState.arc = tvState.arc
fvState.duration = tvState.duration
fvState.timingFunction = tvState.timingFunction
fvState.delay = tvState.delay
fvState.spring = tvState.spring
let forceNonFade = tvState.nonFade || fvState.nonFade
let isNonOpaque = !fv.isOpaque || fv.alpha < 1 || !tv.isOpaque || tv.alpha < 1
if !forceNonFade && isNonOpaque {
// Cross fade if from/toViews are not opaque.
fvState.opacity = 0
} else {
// No cross fade in this case, fromView is always displayed during the transition.
fvState.opacity = nil
/**
We dont want two shadows showing up. Therefore we disable toView's
shadow when fromView is able to display its shadow.
*/
if !fv.layer.masksToBounds && fvState.displayShadow {
tvState.displayShadow = false
}
}
context[tv] = tvState
context[fv] = fvState
}
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
public protocol MotionPreprocessor: class {
/// A reference to a MotionContext.
weak var context: MotionContext! { get set }
/**
Processes the transitionary views.
- Parameter fromViews: An Array of UIViews.
- Parameter toViews: An Array of UIViews.
*/
func process(fromViews: [UIView], toViews: [UIView])
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
class SourcePreprocessor: MotionPreprocessor {
/// A reference to a MotionContext.
weak var context: MotionContext!
/**
Processes the transitionary views.
- Parameter fromViews: An Array of UIViews.
- Parameter toViews: An Array of UIViews.
*/
func process(fromViews: [UIView], toViews: [UIView]) {
for fv in fromViews {
guard let i = context[fv]?.motionIdentifier, let tv = context.destinationView(for: i) else {
continue
}
prepare(view: fv, for: tv)
}
for tv in toViews {
guard let i = context[tv]?.motionIdentifier, let fv = context.sourceView(for: i) else {
continue
}
prepare(view: tv, for: fv)
}
}
/**
Prepares a given view for a target view.
- Parameter view: A UIView.
- Parameter for targetView: A UIView.
*/
func prepare(view: UIView, for targetView: UIView) {
let targetPos = context.container.convert(targetView.layer.position, from: targetView.superview!)
var state = context[view]!
/**
Use global coordinate space since over target position is
converted from the global container
*/
state.coordinateSpace = .global
// Remove incompatible options.
state.position = targetPos
state.transform = nil
state.size = nil
state.cornerRadius = nil
if view.bounds.size != targetView.bounds.size {
state.size = targetView.bounds.size
}
if view.layer.cornerRadius != targetView.layer.cornerRadius {
state.cornerRadius = targetView.layer.cornerRadius
}
if view.layer.transform != targetView.layer.transform {
state.transform = targetView.layer.transform
}
if view.layer.shadowColor != targetView.layer.shadowColor {
state.shadowColor = targetView.layer.shadowColor
}
if view.layer.shadowOpacity != targetView.layer.shadowOpacity {
state.shadowOpacity = targetView.layer.shadowOpacity
}
if view.layer.shadowOffset != targetView.layer.shadowOffset {
state.shadowOffset = targetView.layer.shadowOffset
}
if view.layer.shadowRadius != targetView.layer.shadowRadius {
state.shadowRadius = targetView.layer.shadowRadius
}
if view.layer.shadowPath != targetView.layer.shadowPath {
state.shadowPath = targetView.layer.shadowPath
}
if view.layer.contentsRect != targetView.layer.contentsRect {
state.contentsRect = targetView.layer.contentsRect
}
if view.layer.contentsScale != targetView.layer.contentsScale {
state.contentsScale = targetView.layer.contentsScale
}
context[view] = state
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
public enum MotionTransitionType {
public enum Direction {
case left
case right
case up
case down
}
case none
case auto
case push(direction: Direction)
case pull(direction: Direction)
case cover(direction: Direction)
case uncover(direction: Direction)
case slide(direction: Direction)
case zoomSlide(direction: Direction)
case pageIn(direction: Direction)
case pageOut(direction: Direction)
case fade
case zoom
case zoomOut
indirect case selectBy(presenting: MotionTransitionType, dismissing: MotionTransitionType)
/**
Sets the presenting and dismissing transitions.
- Parameter presenting: A MotionTransitionType.
- Returns: A MotionTransitionType.
*/
public static func autoReverse(presenting: MotionTransitionType) -> MotionTransitionType {
return .selectBy(presenting: presenting, dismissing: presenting.reversed())
}
/// Returns a reversal transition.
func reversed() -> MotionTransitionType {
switch self {
case .push(direction: .up):
return .pull(direction: .down)
case .push(direction: .right):
return .pull(direction: .left)
case .push(direction: .down):
return .pull(direction: .up)
case .push(direction: .left):
return .pull(direction: .right)
case .pull(direction: .up):
return .push(direction: .down)
case .pull(direction: .right):
return .push(direction: .left)
case .pull(direction: .down):
return .push(direction: .up)
case .pull(direction: .left):
return .push(direction: .right)
case .cover(direction: .up):
return .uncover(direction: .down)
case .cover(direction: .right):
return .uncover(direction: .left)
case .cover(direction: .down):
return .uncover(direction: .up)
case .cover(direction: .left):
return .uncover(direction: .right)
case .uncover(direction: .up):
return .cover(direction: .down)
case .uncover(direction: .right):
return .cover(direction: .left)
case .uncover(direction: .down):
return .cover(direction: .up)
case .uncover(direction: .left):
return .cover(direction: .right)
case .slide(direction: .up):
return .slide(direction: .down)
case .slide(direction: .down):
return .slide(direction: .up)
case .slide(direction: .left):
return .slide(direction: .right)
case .slide(direction: .right):
return .slide(direction: .left)
case .zoomSlide(direction: .up):
return .zoomSlide(direction: .down)
case .zoomSlide(direction: .down):
return .zoomSlide(direction: .up)
case .zoomSlide(direction: .left):
return .zoomSlide(direction: .right)
case .zoomSlide(direction: .right):
return .zoomSlide(direction: .left)
case .pageIn(direction: .up):
return .pageOut(direction: .down)
case .pageIn(direction: .right):
return .pageOut(direction: .left)
case .pageIn(direction: .down):
return .pageOut(direction: .up)
case .pageIn(direction: .left):
return .pageOut(direction: .right)
case .pageOut(direction: .up):
return .pageIn(direction: .down)
case .pageOut(direction: .right):
return .pageIn(direction: .left)
case .pageOut(direction: .down):
return .pageIn(direction: .up)
case .pageOut(direction: .left):
return .pageIn(direction: .right)
case .zoom:
return .zoomOut
case .zoomOut:
return .zoom
default:
return self
}
}
}
class TransitionPreprocessor: MotionPreprocessor {
/// A reference to a MotionContext.
weak var context: MotionContext!
/// A reference to a Motion.
weak var motion: Motion?
/**
An initializer that accepts a given Motion instance.
- Parameter motion: A Motion.
*/
init(motion: Motion) {
self.motion = motion
}
/**
Processes the transitionary views.
- Parameter fromViews: An Array of UIViews.
- Parameter toViews: An Array of UIViews.
*/
func process(fromViews: [UIView], toViews: [UIView]) {
guard let m = motion else {
return
}
guard let fv = m.fromView else {
return
}
guard let tv = m.toView else {
return
}
var defaultAnimation = m.defaultAnimation
let isNavigationController = m.isNavigationController
let isTabBarController = m.isTabBarController
let toViewController = m.toViewController
let fromViewController = m.fromViewController
let isPresenting = m.isPresenting
let fromOverFullScreen = m.fromOverFullScreen
let toOverFullScreen = m.toOverFullScreen
let animators = m.animators
if case .auto = defaultAnimation {
if isNavigationController, let navAnim = toViewController?.navigationController?.motionNavigationTransitionType {
defaultAnimation = navAnim
} else if isTabBarController, let tabAnim = toViewController?.tabBarController?.motionTabBarTransitionType {
defaultAnimation = tabAnim
} else if let modalAnim = (isPresenting ? toViewController : fromViewController)?.motionModalTransitionType {
defaultAnimation = modalAnim
}
}
if case .selectBy(let presentAnim, let dismissAnim) = defaultAnimation {
defaultAnimation = isPresenting ? presentAnim : dismissAnim
}
if case .auto = defaultAnimation {
if animators.contains(where: { $0.canAnimate(view: tv, isAppearing: true) || $0.canAnimate(view: fv, isAppearing: false) }) {
defaultAnimation = .none
} else if isNavigationController {
defaultAnimation = isPresenting ? .push(direction:.left) : .pull(direction:.right)
} else if isTabBarController {
defaultAnimation = isPresenting ? .slide(direction:.left) : .slide(direction:.right)
} else {
defaultAnimation = .fade
}
}
if case .none = defaultAnimation {
return
}
context[fv] = [.timingFunction(.standard), .duration(0.35)]
context[tv] = [.timingFunction(.standard), .duration(0.35)]
let shadowState: [MotionTransition] = [.shadow(opacity: 0.5),
.shadow(color: .black),
.shadow(radius: 5),
.shadow(offset: .zero),
.masksToBounds(false)]
switch defaultAnimation {
case .push(let direction):
context[tv]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: true)),
.shadow(opacity: 0),
.beginWith(transitions: shadowState),
.timingFunction(.deceleration)])
context[fv]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: false) / 3),
.overlay(color: .black, opacity: 0.1),
.timingFunction(.deceleration)])
case .pull(let direction):
m.insertToViewFirst = true
context[fv]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: false)),
.shadow(opacity: 0),
.beginWith(transitions: shadowState)])
context[tv]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: true) / 3),
.overlay(color: .black, opacity: 0.1)])
case .slide(let direction):
context[fv]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: false))])
context[tv]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: true))])
case .zoomSlide(let direction):
context[fv]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: false)), .scale(0.8)])
context[tv]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: true)), .scale(0.8)])
case .cover(let direction):
context[tv]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: true)),
.shadow(opacity: 0),
.beginWith(transitions: shadowState),
.timingFunction(.deceleration)])
context[fv]!.append(contentsOf: [.overlay(color: .black, opacity: 0.1),
.timingFunction(.deceleration)])
case .uncover(let direction):
m.insertToViewFirst = true
context[fv]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: false)),
.shadow(opacity: 0),
.beginWith(transitions: shadowState)])
context[tv]!.append(contentsOf: [.overlay(color: .black, opacity: 0.1)])
case .pageIn(let direction):
context[tv]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: true)),
.shadow(opacity: 0),
.beginWith(transitions: shadowState),
.timingFunction(.deceleration)])
context[fv]!.append(contentsOf: [.scale(0.7),
.overlay(color: .black, opacity: 0.1),
.timingFunction(.deceleration)])
case .pageOut(let direction):
m.insertToViewFirst = true
context[fv]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: false)),
.shadow(opacity: 0),
.beginWith(transitions: shadowState)])
context[tv]!.append(contentsOf: [.scale(0.7),
.overlay(color: .black, opacity: 0.1)])
case .fade:
// TODO: clean up this. overFullScreen logic shouldn't be here
if !(fromOverFullScreen && !isPresenting) {
context[tv] = [.fadeOut]
}
#if os(tvOS)
context[fromView] = [.fade]
#else
if (!isPresenting && toOverFullScreen) || !fv.isOpaque || (fv.backgroundColor?.alphaComponent ?? 1) < 1 {
context[fv] = [.fadeOut]
}
#endif
context[tv]!.append(.preferredDurationMatchesLongest)
context[fv]!.append(.preferredDurationMatchesLongest)
case .zoom:
m.insertToViewFirst = true
context[fv]!.append(contentsOf: [.scale(1.3), .fadeOut])
context[tv]!.append(contentsOf: [.scale(0.7)])
case .zoomOut:
context[tv]!.append(contentsOf: [.scale(1.3), .fadeOut])
context[fv]!.append(contentsOf: [.scale(0.7)])
default:
fatalError("Not implemented")
}
}
/**
Shifts the transition by a given size.
- Parameter direction: A MotionTransitionType.Direction.
- Parameter isAppearing: A boolean indicating whether it is appearing
or not.
- Parameter size: An optional CGSize.
- Parameter transpose: A boolean indicating to change the `x` point for `y`
and `y` point for `x`.
- Returns: A CGPoint.
*/
func shift(direction: MotionTransitionType.Direction, isAppearing: Bool, size: CGSize? = nil, transpose: Bool = false) -> CGPoint {
let size = size ?? context.container.bounds.size
let point: CGPoint
switch direction {
case .left, .right:
point = CGPoint(x: (.right == direction) == isAppearing ? -size.width : size.width, y: 0)
case .up, .down:
point = CGPoint(x: 0, y: (.down == direction) == isAppearing ? -size.height : size.height)
}
if transpose {
return CGPoint(x: point.y, y: point.x)
}
return point
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment