diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a906ae0..b6fe0438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,6 @@ The changelog for `Hero`. Also see the [releases](https://github.com/HeroTransit - #595 - Add Accio supported badge - #619 - XCode 11/12 support in example - CI/CD improvements - ### Changed - #648 - Updated iOS version support diff --git a/Sources/Transition/HeroTransition+Complete.swift b/Sources/Transition/HeroTransition+Complete.swift index f4b87a61..80137904 100644 --- a/Sources/Transition/HeroTransition+Complete.swift +++ b/Sources/Transition/HeroTransition+Complete.swift @@ -89,10 +89,32 @@ extension HeroTransition { if isPresenting != finished, !inContainerController, transitionContext != nil { // only happens when present a .overFullScreen VC // bug: http://openradar.appspot.com/radar?id=5320103646199808 - container.window?.addSubview(isPresenting ? fromView : toView) + + // $workaround(eric): try to restore the view of the prensenting view controller to + // where it was otherwise. Simply putting the view back under window will leak the view in + // some edge cases, for example, when the presenting view was deeply nested under some + // exotic view hierarchy (e.g., react native views). as stated where the transition starts, + // `originalSuperview` remembers the original super view when the `presenting` transition + // animation starts, now it's safe to restore it where it was if possible. + if let superview = originalSuperview, superview.window != nil { + let view = isPresenting ? fromView : toView + superview.addSubview(view) + if let frame = originalFrame { + view.frame = frame + } + } else { + container.window?.addSubview(isPresenting ? fromView : toView) + } } } + // clear temporary states only when dismissing finishes. + if !isPresenting && finished { + originalSuperview = nil + originalFrame = nil + originalFrameInContainer = nil + } + if container.superview == transitionContainer { container.removeFromSuperview() } diff --git a/Sources/Transition/HeroTransition+Start.swift b/Sources/Transition/HeroTransition+Start.swift index 7ff7b4bf..bbc9d573 100644 --- a/Sources/Transition/HeroTransition+Start.swift +++ b/Sources/Transition/HeroTransition+Start.swift @@ -30,6 +30,13 @@ extension HeroTransition { state = .starting if let toView = toView, let fromView = fromView { + // remember the superview of the view of the `fromViewController` which is + // presenting the `toViewController` with `overFullscreen` `modalPresentationStyle`, + // so that we can restore the presenting view controller's view later on dismiss + if isPresenting && !inContainerController { + originalSuperview = fromView.superview + originalFrame = fromView.frame + } if let toViewController = toViewController, let transitionContext = transitionContext { toView.frame = transitionContext.finalFrame(for: toViewController) } else { @@ -111,11 +118,36 @@ extension HeroTransition { } if let toView = toView, let fromView = fromView { + // if we're presenting a view controller, remember the position & dimension + // of the view relative to the transition container so that we can: + // - correctly place the view in the transition container when presenting + // - correctly place the view back to where it was when dismissing + if isPresenting && !inContainerController { + originalFrameInContainer = fromView.superview?.convert( + fromView.frame, to: container + ) + } + + // when dismiss and before animating, place the `toView` to be animated + // with the correct position and dimension in the transition container. + // otherwise, there will be an apparent visual jagging when the animation begins. + if !isPresenting, let frame = originalFrameInContainer { + toView.frame = frame + } + context.loadViewAlpha(rootView: toView) context.loadViewAlpha(rootView: fromView) container.addSubview(toView) container.addSubview(fromView) + // when present and before animating, place the `fromView` to be animated + // with the correct position and dimension in the transition container to + // prevent any possible visual jagging when animation starts, even though not + // that apparent in some cases. + if isPresenting, let frame = originalFrameInContainer { + fromView.frame = frame + } + toView.updateConstraints() toView.setNeedsLayout() toView.layoutIfNeeded() diff --git a/Sources/Transition/HeroTransition.swift b/Sources/Transition/HeroTransition.swift index 1080f526..3ff7d836 100644 --- a/Sources/Transition/HeroTransition.swift +++ b/Sources/Transition/HeroTransition.swift @@ -96,6 +96,10 @@ open class HeroTransition: NSObject { internal var plugins: [HeroPlugin] = [] internal var animatingFromViews: [UIView] = [] internal var animatingToViews: [UIView] = [] + internal var originalSuperview: UIView? + internal var originalFrame: CGRect? + internal var originalFrameInContainer: CGRect? + internal static var enabledPlugins: [HeroPlugin.Type] = [] /// destination view controller diff --git a/docs/UsageGuide.md b/docs/UsageGuide.md new file mode 100644 index 00000000..1461fd3c --- /dev/null +++ b/docs/UsageGuide.md @@ -0,0 +1,60 @@ +# Usage + +## Storyboard + +1. In the Identity Inspector, for every pair of source/destination views, give each one the same `HeroID` attribute. +2. For any other views that you would like to animate, specify animation effects in the `Hero Modifier String` attribute. +3. Also in the Identity Inspector, enable Hero Transition on your destination view controller. + +## In Code + +1. Before doing a transition, set the desired `heroID` and `heroModifiers` to both your source and destination views. +2. Enable Hero for the destination view controller + + ```swift + viewController.hero.isEnabled = true + ``` + +### UINavigationController & UITabBarController + +Hero also supports transitions within a navigation controller or a tab bar controller—just set the 'hero.isEnabled' attribute to true on the UINavigationController/UITabBarController instance. + +## Attributes + +There are two important attributes to understand: `heroID` and `heroModifiers`. These are implemented as extensions (using associated objects) for `UIView`. Therefore, after the Hero library is imported, every `UIView` will have these two attributes. + +| Attribute Name | Description | +| --- | --- | +| `heroID` | Identifier for the view. Hero will automatically transition between views with the same `heroID` | +| `hero.modifiers` | Specifies the extra animations performed alongside the main transition. | + +## HeroID + +`heroID` is the identifier for the view. When doing a transition between two view controllers, Hero will search through all subviews for both controllers, and match any views with the same `heroID`. Whenever a pair is discovered, Hero will automatically transit the views from source state to destination state. + +## HeroModifiers + +Use `hero.modifiers` to specify animations alongside the main transition. Checkout [HeroModifier.swift](https://github.com/lkzhao/Hero/blob/master/Sources/HeroModifier.swift) for available modifiers. + +#### For example, to achieve the following effect, set the `hero.modifiers` to be + +```swift +view.hero.modifiers = [.fade, .translate(x:0, y:-250), .rotate(x:-1.6), .scale(1.5)] +``` + + + + +Note: For matched views, the target view's heroModifier will be used. The source view's heroModifier will be ignored. When dismissing, the target view is the presentingViewController's view and the source view is the presentedViewController's view. + +## HeroModifierString + +This is a string value. It provides another way to set `hero.modifiers`. It can be accessed through the storyboard. + +It must be in the following syntax: + +```swift +modifier1() modifier2(parameter1) modifier3(parameter1, parameter2) ... +``` + +Parameters must be between a pair of parentheses, separated by a comma, and each modifier must be separated by a space. Not all modifiers are settable this way.