Created
April 23, 2018 06:35
-
-
Save isaac-weisberg/ec1d73a2db4463e2ab58658c2727f563 to your computer and use it in GitHub Desktop.
An override of UITabBarController. Alternative to usage of UINavigationController. Allows to bundle several UIViewController's into a single controller to simplify bundling and life-cycle management
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ControllerChain.swift | |
// Pobjects | |
// | |
// Created by Endoral Work on 4/11/18. | |
// Copyright © 2018 endoralwork-idsi. All rights reserved. | |
// | |
import UIKit | |
protocol ControllerChainDelegate: class { | |
func didRecognizeSwipeGesture(_ sender: UIScreenEdgePanGestureRecognizer) | |
} | |
@IBDesignable class ControllerChain: UITabBarController { | |
private enum ChainRecognizerType { | |
case pop | |
case push | |
} | |
let interactivePopGestureRecognizer = UIScreenEdgePanGestureRecognizer() | |
let interactivePushGestureRecognizer = UIScreenEdgePanGestureRecognizer() | |
@IBInspectable var swipePopEnabled: Bool = true { | |
didSet { interactivePopGestureRecognizer.isEnabled = swipePopEnabled } | |
} | |
@IBInspectable var swipePushEnabled: Bool = true { | |
didSet { interactivePushGestureRecognizer.isEnabled = swipePushEnabled } | |
} | |
@IBInspectable var usesCustomNavigationItem: Bool = true { | |
didSet { applyNavigationItem(of: selectedViewController) } | |
} | |
@IBInspectable var hidesTabBar: Bool = true { | |
didSet { tabBar.isHidden = hidesTabBar } | |
} | |
@IBOutlet private(set) weak var customNavigationItem: UINavigationItem! | |
weak var controllerChainDelegate: ControllerChainDelegate? | |
private(set) var transitionDuration: CFTimeInterval = 0.3 | |
override var selectedIndex: Int { | |
willSet { | |
guard | |
let fromController = viewControllers?[safeCall: selectedIndex], | |
fromController !== viewControllers?[safeCall: newValue] else { | |
return | |
} | |
guard let destinationController = viewControllers?[safeCall: newValue] else { return } | |
animateTransition(from: fromController, to: destinationController, reversed: newValue < selectedIndex) | |
} | |
didSet { | |
if let controller = viewControllers?[safeCall: selectedIndex] { | |
applyNavigationItem(of: controller) | |
} | |
} | |
} | |
override var selectedViewController: UIViewController? { | |
willSet { | |
guard | |
let fromController = selectedViewController, | |
fromController !== newValue else { | |
return | |
} | |
guard let destinationController = newValue else { return } | |
let fromIndex = viewControllers?.index(of: fromController) ?? 0 | |
let toIndex = viewControllers?.index(of: destinationController) ?? 0 | |
animateTransition(from: fromController, to: destinationController, reversed: toIndex < fromIndex) | |
} | |
didSet { | |
if let controller = selectedViewController { | |
applyNavigationItem(of: controller) | |
} | |
} | |
} | |
required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
tabBar.isHidden = hidesTabBar | |
add(recognizer: interactivePopGestureRecognizer, for: .pop) | |
add(recognizer: interactivePushGestureRecognizer, for: .push) | |
} | |
@IBAction private func forwardClick(_ sender: Any) { | |
setNextController() | |
} | |
@IBAction private func backClick(_ sender: Any) { | |
setPreviousController() | |
} | |
@objc private func interactivePopGestureRecognized(_ sender: UIScreenEdgePanGestureRecognizer) { | |
if case .began = sender.state { | |
if let interactiveGestureRecognizerDelegate = controllerChainDelegate { | |
interactiveGestureRecognizerDelegate.didRecognizeSwipeGesture(sender) | |
} else { | |
setPreviousController() | |
} | |
} | |
// TODO: additional playback management | |
} | |
@objc private func interactivePushGestureRecognized(_ sender: UIScreenEdgePanGestureRecognizer) { | |
if case .began = sender.state { | |
if let interactiveGestureRecognizerDelegate = controllerChainDelegate { | |
interactiveGestureRecognizerDelegate.didRecognizeSwipeGesture(sender) | |
} else { | |
setNextController() | |
} | |
} | |
// TODO: additional playback management | |
} | |
func setSelectedIndex(to index: Int, alternatively action: Action) { | |
guard controllerExists(at: index) else { | |
controllerArrayEdgeReached(for: action) | |
return | |
} | |
selectedIndex = index | |
} | |
func setNextController(orPerform action: Action = .pop(animated: true)) { | |
let newIndex = selectedIndex + 1 | |
setSelectedIndex(to: newIndex, alternatively: action) | |
} | |
func setPreviousController(orPerform action: Action = .pop(animated: true)) { | |
let newIndex = selectedIndex - 1 | |
setSelectedIndex(to: newIndex, alternatively: action) | |
} | |
enum Action { | |
case none | |
case pop(animated: Bool) | |
case push(controller: UIViewController, animated: Bool) | |
case custom(action: () -> Void) | |
} | |
} | |
private extension ControllerChain { | |
func controllerExists(at index: Int) -> Bool { | |
return viewControllers?[safeCall: index] != nil | |
} | |
func controllerArrayEdgeReached(for action: Action) { | |
switch action { | |
case .none: | |
break | |
case .pop(animated: let isAnimated): | |
if let navigationController = navigationController { | |
navigationController.popViewController(animated: isAnimated) | |
} else { | |
dismiss(animated: isAnimated) | |
} | |
case .push(controller: let nextController, animated: let isAnimated): | |
if let navigationController = navigationController { | |
navigationController.pushViewController(nextController, animated: isAnimated) | |
} else { | |
present(nextController, animated: isAnimated) | |
} | |
case .custom(action: let action): | |
action() | |
} | |
} | |
private func add(recognizer: UIScreenEdgePanGestureRecognizer, for type: ChainRecognizerType) { | |
switch type { | |
case .pop: | |
recognizer.addTarget(self, action: #selector(interactivePopGestureRecognized(_:))) | |
recognizer.edges = .left | |
recognizer.isEnabled = swipePopEnabled | |
case .push: | |
recognizer.addTarget(self, action: #selector(interactivePushGestureRecognized(_:))) | |
recognizer.edges = .right | |
recognizer.isEnabled = swipePushEnabled | |
} | |
recognizer.minimumNumberOfTouches = 1 | |
recognizer.maximumNumberOfTouches = 1 | |
recognizer.cancelsTouchesInView = true | |
recognizer.delaysTouchesBegan = false | |
recognizer.delaysTouchesEnded = true | |
view.addGestureRecognizer(recognizer) | |
} | |
func applyNavigationItem(_ navItem: UINavigationItem?) { | |
title = navItem?.title | |
navigationItem.leftBarButtonItems = navItem?.leftBarButtonItems | |
navigationItem.rightBarButtonItems = navItem?.rightBarButtonItems | |
} | |
func applyNavigationItem(of viewController: UIViewController?) { | |
if usesCustomNavigationItem { | |
applyNavigationItem(customNavigationItem) | |
} else { | |
applyNavigationItem(viewController?.navigationItem) | |
} | |
} | |
func animateTransition(from source: UIViewController, to destination: UIViewController, reversed: Bool = false) { | |
if let fromView = source.view { | |
view.insertSubview(fromView, at: 0) | |
CATransaction.begin() | |
CATransaction.setCompletionBlock { fromView.removeFromSuperview() } | |
let transition = CATransition() | |
transition.duration = transitionDuration | |
transition.type = kCATransitionFade | |
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) | |
fromView.layer.add(transition, forKey: kCATransition) | |
CATransaction.commit() | |
} | |
if let toView = destination.view { | |
let transition = CATransition() | |
transition.duration = transitionDuration | |
transition.type = kCATransitionPush | |
transition.subtype = reversed ? kCATransitionFromLeft : kCATransitionFromRight | |
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) | |
toView.layer.add(transition, forKey: kCATransition) | |
} | |
} | |
} | |
private extension Array { | |
subscript(safeCall index: Index) -> Element? { | |
get { return indices.contains(index) ? self[index] : nil } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment