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.
 
 
 
 

381 lines
14 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
internal class HeroViewControllerConfig: NSObject {
var modalAnimation: HeroDefaultAnimationType = .auto
var navigationAnimation: HeroDefaultAnimationType = .auto
var tabBarAnimation: HeroDefaultAnimationType = .auto
var storedSnapshot: UIView?
weak var previousNavigationDelegate: UINavigationControllerDelegate?
weak var previousTabBarDelegate: UITabBarControllerDelegate?
}
extension UIViewController: HeroCompatible { }
public extension HeroExtension where Base: UIViewController {
internal var config: HeroViewControllerConfig {
get {
if let config = objc_getAssociatedObject(base, &type(of: base).AssociatedKeys.heroConfig) as? HeroViewControllerConfig {
return config
}
let config = HeroViewControllerConfig()
self.config = config
return config
}
set { objc_setAssociatedObject(base, &type(of: base).AssociatedKeys.heroConfig, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
/// used for .overFullScreen presentation
internal var storedSnapshot: UIView? {
get { return config.storedSnapshot }
set { config.storedSnapshot = newValue }
}
/// default hero animation type for presenting & dismissing modally
var modalAnimationType: HeroDefaultAnimationType {
get { return config.modalAnimation }
set { config.modalAnimation = newValue }
}
// TODO: can be moved to internal later (will still be accessible via IB)
var modalAnimationTypeString: String? {
get { return config.modalAnimation.label }
set { config.modalAnimation = newValue?.parseOne() ?? .auto }
}
// TODO: can be moved to internal later (will still be accessible via IB)
var isEnabled: Bool {
get {
return base.transitioningDelegate is HeroTransition
}
set {
guard newValue != isEnabled else { return }
if newValue {
base.transitioningDelegate = Hero.shared
if let navi = base as? UINavigationController {
base.previousNavigationDelegate = navi.delegate
navi.delegate = Hero.shared
}
if let tab = base as? UITabBarController {
base.previousTabBarDelegate = tab.delegate
tab.delegate = Hero.shared
}
} else {
base.transitioningDelegate = nil
if let navi = base as? UINavigationController, navi.delegate is HeroTransition {
navi.delegate = base.previousNavigationDelegate
}
if let tab = base as? UITabBarController, tab.delegate is HeroTransition {
tab.delegate = base.previousTabBarDelegate
}
}
}
}
}
public extension UIViewController {
fileprivate struct AssociatedKeys {
static var heroConfig = "heroConfig"
}
@available(*, renamed: "hero.config")
internal var heroConfig: HeroViewControllerConfig {
get { return hero.config }
set { hero.config = newValue }
}
internal var previousNavigationDelegate: UINavigationControllerDelegate? {
get { return hero.config.previousNavigationDelegate }
set { hero.config.previousNavigationDelegate = newValue }
}
internal var previousTabBarDelegate: UITabBarControllerDelegate? {
get { return hero.config.previousTabBarDelegate }
set { hero.config.previousTabBarDelegate = newValue }
}
@available(*, renamed: "hero.storedSnapshot")
internal var heroStoredSnapshot: UIView? {
get { return hero.config.storedSnapshot }
set { hero.config.storedSnapshot = newValue }
}
@available(*, renamed: "hero.modalAnimationType")
var heroModalAnimationType: HeroDefaultAnimationType {
get { return hero.modalAnimationType }
set { hero.modalAnimationType = newValue }
}
@available(*, renamed: "hero.modalAnimationTypeString")
@IBInspectable var heroModalAnimationTypeString: String? {
get { return hero.modalAnimationTypeString }
set { hero.modalAnimationTypeString = newValue }
}
@available(*, renamed: "hero.isEnabled")
@IBInspectable var isHeroEnabled: Bool {
get { return hero.isEnabled }
set { hero.isEnabled = newValue }
}
}
public extension HeroExtension where Base: UINavigationController {
/// default hero animation type for push and pop within the navigation controller
var navigationAnimationType: HeroDefaultAnimationType {
get { return config.navigationAnimation }
set { config.navigationAnimation = newValue }
}
var navigationAnimationTypeString: String? {
get { return config.navigationAnimation.label }
set { config.navigationAnimation = newValue?.parseOne() ?? .auto }
}
}
extension UINavigationController {
@available(*, renamed: "hero.navigationAnimationType")
public var heroNavigationAnimationType: HeroDefaultAnimationType {
get { return hero.navigationAnimationType }
set { hero.navigationAnimationType = newValue }
}
// TODO: can be moved to internal later (will still be accessible via IB)
@available(*, renamed: "hero.navigationAnimationTypeString")
@IBInspectable public var heroNavigationAnimationTypeString: String? {
get { return hero.navigationAnimationTypeString }
set { hero.navigationAnimationTypeString = newValue }
}
}
public extension HeroExtension where Base: UITabBarController {
/// default hero animation type for switching tabs within the tab bar controller
var tabBarAnimationType: HeroDefaultAnimationType {
get { return config.tabBarAnimation }
set { config.tabBarAnimation = newValue }
}
var tabBarAnimationTypeString: String? {
get { return config.tabBarAnimation.label }
set { config.tabBarAnimation = newValue?.parseOne() ?? .auto }
}
}
public extension UITabBarController {
@available(*, renamed: "hero.tabBarAnimationType")
var heroTabBarAnimationType: HeroDefaultAnimationType {
get { return hero.tabBarAnimationType }
set { hero.tabBarAnimationType = newValue }
}
// TODO: can be moved to internal later (will still be accessible via IB)
@available(*, renamed: "hero.tabBarAnimationTypeString")
@IBInspectable var heroTabBarAnimationTypeString: String? {
get { return hero.tabBarAnimationTypeString }
set { hero.tabBarAnimationTypeString = newValue }
}
}
public extension HeroExtension where Base: UIViewController {
/**
Dismiss the current view controller with animation. Will perform a navigationController.popViewController
if the current view controller is contained inside a navigationController
*/
func dismissViewController(completion: (() -> Void)? = nil) {
if let navigationController = base.navigationController, navigationController.viewControllers.first != base {
navigationController.popViewController(animated: true)
} else {
base.dismiss(animated: true, completion: completion)
}
}
/**
Unwind to the root view controller using Hero
*/
func unwindToRootViewController() {
unwindToViewController { $0.presentingViewController == nil }
}
/**
Unwind to a specific view controller using Hero
*/
func unwindToViewController(_ toViewController: UIViewController) {
unwindToViewController { $0 == toViewController }
}
func unwindToViewController(withSelector: Selector) {
unwindToViewController { $0.responds(to: withSelector) }
}
/**
Unwind to a view controller with given class using Hero
*/
func unwindToViewController(withClass: AnyClass) {
unwindToViewController { $0.isKind(of: withClass) }
}
/**
Unwind to a view controller that the matchBlock returns true on.
*/
func unwindToViewController(withMatchBlock: (UIViewController) -> Bool) {
var target: UIViewController?
var current: UIViewController? = base
while target == nil && current != nil {
if let childViewControllers = (current as? UINavigationController)?.children ?? current!.navigationController?.children {
for vc in childViewControllers.reversed() {
if vc != base, withMatchBlock(vc) {
target = vc
break
}
}
}
if target == nil {
current = current!.presentingViewController
if let vc = current, withMatchBlock(vc) == true {
target = vc
}
}
}
if let target = target {
if target.presentedViewController != nil {
_ = target.navigationController?.popToViewController(target, animated: false)
let fromVC = base.navigationController ?? base
let toVC = target.navigationController ?? target
if target.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 Hero to use the current VC as the fromViewController
Hero.shared.fromViewController = fromVC
let snapshotView = fromVC.view.snapshotView(afterScreenUpdates: true)!
let targetSuperview = toVC.presentedViewController!.view!
if let visualEffectView = targetSuperview as? UIVisualEffectView {
visualEffectView.contentView.addSubview(snapshotView)
} else {
targetSuperview.addSubview(snapshotView)
}
}
toVC.dismiss(animated: true, completion: nil)
} else {
_ = target.navigationController?.popToViewController(target, animated: true)
}
} else {
// unwind target not found
}
}
/**
Replace the current view controller with another VC on the navigation/modal stack.
*/
func replaceViewController(with next: UIViewController, completion: (() -> Void)? = nil) {
let hero = next.transitioningDelegate as? HeroTransition ?? Hero.shared
if hero.isTransitioning {
print("hero.replaceViewController cancelled because Hero was doing a transition. Use Hero.shared.cancel(animated:false) or Hero.shared.end(animated:false) to stop the transition first before calling hero.replaceViewController.")
return
}
if let navigationController = base.navigationController {
var vcs = navigationController.children
if !vcs.isEmpty {
vcs.removeLast()
vcs.append(next)
}
if navigationController.hero.isEnabled {
hero.forceNotInteractive = true
}
navigationController.setViewControllers(vcs, animated: true)
} else if let container = base.view.superview {
let parentVC = base.presentingViewController
hero.transition(from: base, to: next, in: container) { [weak base] finished in
guard let base = base else { return }
guard finished else { return }
next.view.window?.addSubview(next.view)
if let parentVC = parentVC {
base.dismiss(animated: false) {
parentVC.present(next, animated: false, completion: completion)
}
} else {
UIApplication.shared.keyWindow?.rootViewController = next
}
}
}
}
}
extension UIViewController {
@available(*, renamed: "hero.dismissViewController")
@IBAction public func ht_dismiss(_ sender: UIView) {
hero.dismissViewController()
}
@available(*, renamed: "hero.replaceViewController(with:)")
public func heroReplaceViewController(with next: UIViewController) {
hero.replaceViewController(with: next)
}
// TODO: can be moved to internal later (will still be accessible via IB)
@available(*, renamed: "hero.dismissViewController")
@IBAction public func hero_dismissViewController() {
hero.dismissViewController()
}
// TODO: can be moved to internal later (will still be accessible via IB)
@available(*, renamed: "hero.unwindToRootViewController")
@IBAction public func hero_unwindToRootViewController() {
hero.unwindToRootViewController()
}
@available(*, renamed: "hero.unwindToViewController(_:)")
public func hero_unwindToViewController(_ toViewController: UIViewController) {
hero.unwindToViewController(toViewController)
}
@available(*, renamed: "hero.unwindToViewController(withSelector:)")
public func hero_unwindToViewController(withSelector: Selector) {
hero.unwindToViewController(withSelector: withSelector)
}
@available(*, renamed: "hero_unwindToViewController(withClass:)")
public func hero_unwindToViewController(withClass: AnyClass) {
hero.unwindToViewController(withClass: withClass)
}
@available(*, renamed: "hero.unwindToViewController(withMatchBlock:)")
public func hero_unwindToViewController(withMatchBlock: (UIViewController) -> Bool) {
hero.unwindToViewController(withMatchBlock: withMatchBlock)
}
@available(*, renamed: "hero.replaceViewController(with:)")
public func hero_replaceViewController(with next: UIViewController) {
hero.replaceViewController(with: next)
}
}