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
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)
|
|
}
|
|
}
|