You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

214 lines
7.4 KiB

// The MIT License (MIT)
//
// 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
/**
### The singleton class/object for controlling interactive transitions.
```swift
Hero.shared
```
#### Use the following methods for controlling the interactive transition:
```swift
func update(progress:Double)
func end()
func cancel()
func apply(modifiers:[HeroModifier], to view:UIView)
```
*/
public class Hero: NSObject {
/// Shared singleton object for controlling the transition
public static var shared = HeroTransition()
}
public protocol HeroTransitionDelegate: class {
func heroTransition(_ hero: HeroTransition, didUpdate state: HeroTransitionState)
func heroTransition(_ hero: HeroTransition, didUpdate progress: Double)
}
open class HeroTransition: NSObject {
public weak var delegate: HeroTransitionDelegate?
public var defaultAnimation: HeroDefaultAnimationType = .auto
public var containerColor: UIColor = .black
public var isUserInteractionEnabled = false
public var viewOrderingStrategy: HeroViewOrderingStrategy = .auto
public internal(set) var state: HeroTransitionState = .possible {
didSet {
if state != .notified, state != .starting {
beginCallback?(state == .animating)
beginCallback = nil
}
delegate?.heroTransition(self, didUpdate: state)
}
}
public var isTransitioning: Bool { return state != .possible }
public internal(set) var isPresenting: Bool = true
@available(*, deprecated, message: "Use isTransitioning instead")
public var transitioning: Bool {
return isTransitioning
}
@available(*, deprecated, message: "Use isPresenting instead")
public var presenting: Bool {
return isPresenting
}
/// container we created to hold all animating views, will be a subview of the
/// transitionContainer when transitioning
public internal(set) var container: UIView!
/// this is the container supplied by UIKit
internal var transitionContainer: UIView?
internal var completionCallback: ((Bool) -> Void)?
internal var beginCallback: ((Bool) -> Void)?
internal var processors: [HeroPreprocessor] = []
internal var animators: [HeroAnimator] = []
internal var plugins: [HeroPlugin] = []
internal var animatingFromViews: [UIView] = []
internal var animatingToViews: [UIView] = []
internal static var enabledPlugins: [HeroPlugin.Type] = []
/// destination view controller
public internal(set) var toViewController: UIViewController?
/// source view controller
public internal(set) var fromViewController: UIViewController?
/// context object holding transition informations
public internal(set) var context: HeroContext!
/// whether or not we are handling transition interactively
public var interactive: Bool {
return !progressRunner.isRunning
}
internal var progressUpdateObservers: [HeroProgressUpdateObserver]?
/// max duration needed by the animators
public internal(set) var totalDuration: TimeInterval = 0.0
/// progress of the current transition. 0 if no transition is happening
public internal(set) var progress: Double = 0 {
didSet {
if state == .animating {
if let progressUpdateObservers = progressUpdateObservers {
for observer in progressUpdateObservers {
observer.heroDidUpdateProgress(progress: progress)
}
}
let timePassed = progress * totalDuration
if interactive {
for animator in animators {
animator.seekTo(timePassed: timePassed)
}
} else {
for plugin in plugins where plugin.requirePerFrameCallback {
plugin.seekTo(timePassed: timePassed)
}
}
transitionContext?.updateInteractiveTransition(CGFloat(progress))
}
delegate?.heroTransition(self, didUpdate: progress)
}
}
lazy var progressRunner: HeroProgressRunner = {
let runner = HeroProgressRunner()
runner.delegate = self
return runner
}()
/// a UIViewControllerContextTransitioning object provided by UIKit,
/// might be nil when transitioning. This happens when calling heroReplaceViewController
internal weak var transitionContext: UIViewControllerContextTransitioning?
internal var fullScreenSnapshot: UIView?
// By default, Hero will always appear to be interactive to UIKit. This forces it to appear non-interactive.
// Used when doing a hero_replaceViewController within a UINavigationController, to fix a bug with
// UINavigationController.setViewControllers not able to handle interactive transition
internal var forceNotInteractive = false
internal var forceFinishing: Bool?
internal var startingProgress: CGFloat?
internal var inNavigationController = false
internal var inTabBarController = false
internal var inContainerController: Bool {
return inNavigationController || inTabBarController
}
internal var toOverFullScreen: Bool {
return !inContainerController && (toViewController?.modalPresentationStyle == .overFullScreen || toViewController?.modalPresentationStyle == .overCurrentContext)
}
internal var fromOverFullScreen: Bool {
return !inContainerController && (fromViewController?.modalPresentationStyle == .overFullScreen || fromViewController?.modalPresentationStyle == .overCurrentContext)
}
internal var toView: UIView? { return toViewController?.view }
internal var fromView: UIView? { return fromViewController?.view }
public override init() { super.init() }
func complete(after: TimeInterval, finishing: Bool) {
guard [HeroTransitionState.animating, .starting, .notified].contains(state) else { return }
if after <= 1.0 / 120 {
complete(finished: finishing)
return
}
let totalTime: TimeInterval
if finishing {
totalTime = after / max((1 - progress), 0.01)
} else {
totalTime = after / max(progress, 0.01)
}
progressRunner.start(timePassed: progress * totalTime, totalTime: totalTime, reverse: !finishing)
}
// MARK: Observe Progress
/**
Receive callbacks on each animation frame.
Observers will be cleaned when transition completes
- Parameters:
- observer: the observer
*/
public func observeForProgressUpdate(observer: HeroProgressUpdateObserver) {
if progressUpdateObservers == nil {
progressUpdateObservers = []
}
progressUpdateObservers!.append(observer)
}
}
extension HeroTransition: HeroProgressRunnerDelegate {
func updateProgress(progress: Double) {
self.progress = progress
}
}