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

  1. // The MIT License (MIT)
  2. //
  3. // Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
  4. //
  5. // Permission is hereby granted, free of charge, to any person obtaining a copy
  6. // of this software and associated documentation files (the "Software"), to deal
  7. // in the Software without restriction, including without limitation the rights
  8. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the Software is
  10. // furnished to do so, subject to the following conditions:
  11. //
  12. // The above copyright notice and this permission notice shall be included in
  13. // all copies or substantial portions of the Software.
  14. //
  15. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  21. // THE SOFTWARE.
  22. import UIKit
  23. internal class HeroViewControllerConfig: NSObject {
  24. var modalAnimation: HeroDefaultAnimationType = .auto
  25. var navigationAnimation: HeroDefaultAnimationType = .auto
  26. var tabBarAnimation: HeroDefaultAnimationType = .auto
  27. var storedSnapshot: UIView?
  28. weak var previousNavigationDelegate: UINavigationControllerDelegate?
  29. weak var previousTabBarDelegate: UITabBarControllerDelegate?
  30. }
  31. extension UIViewController: HeroCompatible { }
  32. public extension HeroExtension where Base: UIViewController {
  33. internal var config: HeroViewControllerConfig {
  34. get {
  35. if let config = objc_getAssociatedObject(base, &type(of: base).AssociatedKeys.heroConfig) as? HeroViewControllerConfig {
  36. return config
  37. }
  38. let config = HeroViewControllerConfig()
  39. self.config = config
  40. return config
  41. }
  42. set { objc_setAssociatedObject(base, &type(of: base).AssociatedKeys.heroConfig, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
  43. }
  44. /// used for .overFullScreen presentation
  45. internal var storedSnapshot: UIView? {
  46. get { return config.storedSnapshot }
  47. set { config.storedSnapshot = newValue }
  48. }
  49. /// default hero animation type for presenting & dismissing modally
  50. var modalAnimationType: HeroDefaultAnimationType {
  51. get { return config.modalAnimation }
  52. set { config.modalAnimation = newValue }
  53. }
  54. // TODO: can be moved to internal later (will still be accessible via IB)
  55. var modalAnimationTypeString: String? {
  56. get { return config.modalAnimation.label }
  57. set { config.modalAnimation = newValue?.parseOne() ?? .auto }
  58. }
  59. // TODO: can be moved to internal later (will still be accessible via IB)
  60. var isEnabled: Bool {
  61. get {
  62. return base.transitioningDelegate is HeroTransition
  63. }
  64. set {
  65. guard newValue != isEnabled else { return }
  66. if newValue {
  67. base.transitioningDelegate = Hero.shared
  68. if let navi = base as? UINavigationController {
  69. base.previousNavigationDelegate = navi.delegate
  70. navi.delegate = Hero.shared
  71. }
  72. if let tab = base as? UITabBarController {
  73. base.previousTabBarDelegate = tab.delegate
  74. tab.delegate = Hero.shared
  75. }
  76. } else {
  77. base.transitioningDelegate = nil
  78. if let navi = base as? UINavigationController, navi.delegate is HeroTransition {
  79. navi.delegate = base.previousNavigationDelegate
  80. }
  81. if let tab = base as? UITabBarController, tab.delegate is HeroTransition {
  82. tab.delegate = base.previousTabBarDelegate
  83. }
  84. }
  85. }
  86. }
  87. }
  88. public extension UIViewController {
  89. fileprivate struct AssociatedKeys {
  90. static var heroConfig = "heroConfig"
  91. }
  92. @available(*, renamed: "hero.config")
  93. internal var heroConfig: HeroViewControllerConfig {
  94. get { return hero.config }
  95. set { hero.config = newValue }
  96. }
  97. internal var previousNavigationDelegate: UINavigationControllerDelegate? {
  98. get { return hero.config.previousNavigationDelegate }
  99. set { hero.config.previousNavigationDelegate = newValue }
  100. }
  101. internal var previousTabBarDelegate: UITabBarControllerDelegate? {
  102. get { return hero.config.previousTabBarDelegate }
  103. set { hero.config.previousTabBarDelegate = newValue }
  104. }
  105. @available(*, renamed: "hero.storedSnapshot")
  106. internal var heroStoredSnapshot: UIView? {
  107. get { return hero.config.storedSnapshot }
  108. set { hero.config.storedSnapshot = newValue }
  109. }
  110. @available(*, renamed: "hero.modalAnimationType")
  111. var heroModalAnimationType: HeroDefaultAnimationType {
  112. get { return hero.modalAnimationType }
  113. set { hero.modalAnimationType = newValue }
  114. }
  115. @available(*, renamed: "hero.modalAnimationTypeString")
  116. @IBInspectable var heroModalAnimationTypeString: String? {
  117. get { return hero.modalAnimationTypeString }
  118. set { hero.modalAnimationTypeString = newValue }
  119. }
  120. @available(*, renamed: "hero.isEnabled")
  121. @IBInspectable var isHeroEnabled: Bool {
  122. get { return hero.isEnabled }
  123. set { hero.isEnabled = newValue }
  124. }
  125. }
  126. public extension HeroExtension where Base: UINavigationController {
  127. /// default hero animation type for push and pop within the navigation controller
  128. var navigationAnimationType: HeroDefaultAnimationType {
  129. get { return config.navigationAnimation }
  130. set { config.navigationAnimation = newValue }
  131. }
  132. var navigationAnimationTypeString: String? {
  133. get { return config.navigationAnimation.label }
  134. set { config.navigationAnimation = newValue?.parseOne() ?? .auto }
  135. }
  136. }
  137. extension UINavigationController {
  138. @available(*, renamed: "hero.navigationAnimationType")
  139. public var heroNavigationAnimationType: HeroDefaultAnimationType {
  140. get { return hero.navigationAnimationType }
  141. set { hero.navigationAnimationType = newValue }
  142. }
  143. // TODO: can be moved to internal later (will still be accessible via IB)
  144. @available(*, renamed: "hero.navigationAnimationTypeString")
  145. @IBInspectable public var heroNavigationAnimationTypeString: String? {
  146. get { return hero.navigationAnimationTypeString }
  147. set { hero.navigationAnimationTypeString = newValue }
  148. }
  149. }
  150. public extension HeroExtension where Base: UITabBarController {
  151. /// default hero animation type for switching tabs within the tab bar controller
  152. var tabBarAnimationType: HeroDefaultAnimationType {
  153. get { return config.tabBarAnimation }
  154. set { config.tabBarAnimation = newValue }
  155. }
  156. var tabBarAnimationTypeString: String? {
  157. get { return config.tabBarAnimation.label }
  158. set { config.tabBarAnimation = newValue?.parseOne() ?? .auto }
  159. }
  160. }
  161. public extension UITabBarController {
  162. @available(*, renamed: "hero.tabBarAnimationType")
  163. var heroTabBarAnimationType: HeroDefaultAnimationType {
  164. get { return hero.tabBarAnimationType }
  165. set { hero.tabBarAnimationType = newValue }
  166. }
  167. // TODO: can be moved to internal later (will still be accessible via IB)
  168. @available(*, renamed: "hero.tabBarAnimationTypeString")
  169. @IBInspectable var heroTabBarAnimationTypeString: String? {
  170. get { return hero.tabBarAnimationTypeString }
  171. set { hero.tabBarAnimationTypeString = newValue }
  172. }
  173. }
  174. public extension HeroExtension where Base: UIViewController {
  175. /**
  176. Dismiss the current view controller with animation. Will perform a navigationController.popViewController
  177. if the current view controller is contained inside a navigationController
  178. */
  179. func dismissViewController(completion: (() -> Void)? = nil) {
  180. if let navigationController = base.navigationController, navigationController.viewControllers.first != base {
  181. navigationController.popViewController(animated: true)
  182. } else {
  183. base.dismiss(animated: true, completion: completion)
  184. }
  185. }
  186. /**
  187. Unwind to the root view controller using Hero
  188. */
  189. func unwindToRootViewController() {
  190. unwindToViewController { $0.presentingViewController == nil }
  191. }
  192. /**
  193. Unwind to a specific view controller using Hero
  194. */
  195. func unwindToViewController(_ toViewController: UIViewController) {
  196. unwindToViewController { $0 == toViewController }
  197. }
  198. func unwindToViewController(withSelector: Selector) {
  199. unwindToViewController { $0.responds(to: withSelector) }
  200. }
  201. /**
  202. Unwind to a view controller with given class using Hero
  203. */
  204. func unwindToViewController(withClass: AnyClass) {
  205. unwindToViewController { $0.isKind(of: withClass) }
  206. }
  207. /**
  208. Unwind to a view controller that the matchBlock returns true on.
  209. */
  210. func unwindToViewController(withMatchBlock: (UIViewController) -> Bool) {
  211. var target: UIViewController?
  212. var current: UIViewController? = base
  213. while target == nil && current != nil {
  214. if let childViewControllers = (current as? UINavigationController)?.children ?? current!.navigationController?.children {
  215. for vc in childViewControllers.reversed() {
  216. if vc != base, withMatchBlock(vc) {
  217. target = vc
  218. break
  219. }
  220. }
  221. }
  222. if target == nil {
  223. current = current!.presentingViewController
  224. if let vc = current, withMatchBlock(vc) == true {
  225. target = vc
  226. }
  227. }
  228. }
  229. if let target = target {
  230. if target.presentedViewController != nil {
  231. _ = target.navigationController?.popToViewController(target, animated: false)
  232. let fromVC = base.navigationController ?? base
  233. let toVC = target.navigationController ?? target
  234. if target.presentedViewController != fromVC {
  235. // UIKit's UIViewController.dismiss will jump to target.presentedViewController then perform the dismiss.
  236. // We overcome this behavior by inserting a snapshot into target.presentedViewController
  237. // And also force Hero to use the current VC as the fromViewController
  238. Hero.shared.fromViewController = fromVC
  239. let snapshotView = fromVC.view.snapshotView(afterScreenUpdates: true)!
  240. let targetSuperview = toVC.presentedViewController!.view!
  241. if let visualEffectView = targetSuperview as? UIVisualEffectView {
  242. visualEffectView.contentView.addSubview(snapshotView)
  243. } else {
  244. targetSuperview.addSubview(snapshotView)
  245. }
  246. }
  247. toVC.dismiss(animated: true, completion: nil)
  248. } else {
  249. _ = target.navigationController?.popToViewController(target, animated: true)
  250. }
  251. } else {
  252. // unwind target not found
  253. }
  254. }
  255. /**
  256. Replace the current view controller with another VC on the navigation/modal stack.
  257. */
  258. func replaceViewController(with next: UIViewController, completion: (() -> Void)? = nil) {
  259. let hero = next.transitioningDelegate as? HeroTransition ?? Hero.shared
  260. if hero.isTransitioning {
  261. 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.")
  262. return
  263. }
  264. if let navigationController = base.navigationController {
  265. var vcs = navigationController.children
  266. if !vcs.isEmpty {
  267. vcs.removeLast()
  268. vcs.append(next)
  269. }
  270. if navigationController.hero.isEnabled {
  271. hero.forceNotInteractive = true
  272. }
  273. navigationController.setViewControllers(vcs, animated: true)
  274. } else if let container = base.view.superview {
  275. let parentVC = base.presentingViewController
  276. hero.transition(from: base, to: next, in: container) { [weak base] finished in
  277. guard let base = base else { return }
  278. guard finished else { return }
  279. next.view.window?.addSubview(next.view)
  280. if let parentVC = parentVC {
  281. base.dismiss(animated: false) {
  282. parentVC.present(next, animated: false, completion: completion)
  283. }
  284. } else {
  285. UIApplication.shared.keyWindow?.rootViewController = next
  286. }
  287. }
  288. }
  289. }
  290. }
  291. extension UIViewController {
  292. @available(*, renamed: "hero.dismissViewController")
  293. @IBAction public func ht_dismiss(_ sender: UIView) {
  294. hero.dismissViewController()
  295. }
  296. @available(*, renamed: "hero.replaceViewController(with:)")
  297. public func heroReplaceViewController(with next: UIViewController) {
  298. hero.replaceViewController(with: next)
  299. }
  300. // TODO: can be moved to internal later (will still be accessible via IB)
  301. @available(*, renamed: "hero.dismissViewController")
  302. @IBAction public func hero_dismissViewController() {
  303. hero.dismissViewController()
  304. }
  305. // TODO: can be moved to internal later (will still be accessible via IB)
  306. @available(*, renamed: "hero.unwindToRootViewController")
  307. @IBAction public func hero_unwindToRootViewController() {
  308. hero.unwindToRootViewController()
  309. }
  310. @available(*, renamed: "hero.unwindToViewController(_:)")
  311. public func hero_unwindToViewController(_ toViewController: UIViewController) {
  312. hero.unwindToViewController(toViewController)
  313. }
  314. @available(*, renamed: "hero.unwindToViewController(withSelector:)")
  315. public func hero_unwindToViewController(withSelector: Selector) {
  316. hero.unwindToViewController(withSelector: withSelector)
  317. }
  318. @available(*, renamed: "hero_unwindToViewController(withClass:)")
  319. public func hero_unwindToViewController(withClass: AnyClass) {
  320. hero.unwindToViewController(withClass: withClass)
  321. }
  322. @available(*, renamed: "hero.unwindToViewController(withMatchBlock:)")
  323. public func hero_unwindToViewController(withMatchBlock: (UIViewController) -> Bool) {
  324. hero.unwindToViewController(withMatchBlock: withMatchBlock)
  325. }
  326. @available(*, renamed: "hero.replaceViewController(with:)")
  327. public func hero_replaceViewController(with next: UIViewController) {
  328. hero.replaceViewController(with: next)
  329. }
  330. }