Skip to content

Instantly share code, notes, and snippets.

@isaac-weisberg
Created April 23, 2018 06:35
Show Gist options
  • Save isaac-weisberg/ec1d73a2db4463e2ab58658c2727f563 to your computer and use it in GitHub Desktop.
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
//
// 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