Commit 0cddaeb7 by Daniel Jonathan Committed by GitHub

Merge pull request #1165 from OrkhanAlikhanov/interactive-swipe

[WIP] Adding interactive swipe
parents cbd13228 fb734416
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
*/ */
import UIKit import UIKit
import Motion
extension UIViewController { extension UIViewController {
/** /**
...@@ -51,19 +52,31 @@ private class MaterialTabBar: UITabBar { ...@@ -51,19 +52,31 @@ private class MaterialTabBar: UITabBar {
} }
open class BottomNavigationController: UITabBarController { open class BottomNavigationController: UITabBarController {
/// A Boolean that indicates if the swipe feature is enabled.. /// A Boolean that controls if the swipe feature is enabled.
open var isSwipeEnabled = false { open var isSwipeEnabled = true {
didSet { didSet {
guard isSwipeEnabled else { guard isSwipeEnabled else {
removeSwipeGestureRecognizers() removeSwipeGesture()
return return
} }
prepareSwipeGestureRecognizers() prepareSwipeGesture()
} }
} }
/** /**
A UIPanGestureRecognizer property internally used for the interactive
swipe.
*/
public private(set) var interactiveSwipeGesture: UIPanGestureRecognizer?
/**
A private integer for storing index of current view controller
during interactive transition.
*/
private var currentIndex = -1
/**
An initializer that initializes the object with a NSCoder object. An initializer that initializes the object with a NSCoder object.
- Parameter aDecoder: A NSCoder instance. - Parameter aDecoder: A NSCoder instance.
*/ */
...@@ -151,6 +164,97 @@ open class BottomNavigationController: UITabBarController { ...@@ -151,6 +164,97 @@ open class BottomNavigationController: UITabBarController {
view.contentScaleFactor = Screen.scale view.contentScaleFactor = Screen.scale
prepareTabBar() prepareTabBar()
isSwipeEnabled = true
isMotionEnabled = true
}
}
private extension BottomNavigationController {
/**
A target method contolling interactive swipe transition based on
gesture recognizer.
- Parameter _ gesture: A UIPanGestureRecognizer.
*/
@objc
func handleTransitionPan(_ gesture: UIPanGestureRecognizer) {
guard selectedIndex != NSNotFound else {
return
}
let translationX = gesture.translation(in: nil).x
let velocityX = gesture.velocity(in: nil).x
switch gesture.state {
case .began, .changed:
let isSlidingLeft = currentIndex == -1 ? velocityX < 0 : translationX < 0
if currentIndex == -1 {
currentIndex = selectedIndex
}
let nextIndex = currentIndex + (isSlidingLeft ? 1 : -1)
if selectedIndex != nextIndex {
/// 5 point threshold
guard abs(translationX) > 5 else {
return
}
if currentIndex != selectedIndex {
MotionTransition.shared.cancel(isAnimated: false)
}
guard canSelect(at: nextIndex) else {
return
}
selectedIndex = nextIndex
MotionTransition.shared.setCompletionCallbackForNextTransition { [weak self] isFinishing in
guard let `self` = self, isFinishing else {
return
}
self.delegate?.tabBarController?(self, didSelect: self.viewControllers![nextIndex])
}
} else {
let progress = abs(translationX / view.bounds.width)
MotionTransition.shared.update(Double(progress))
}
default:
let progress = (translationX + velocityX) / view.bounds.width
let isUserHandDirectionLeft = progress < 0
let isTargetHandDirectionLeft = selectedIndex > currentIndex
if isUserHandDirectionLeft == isTargetHandDirectionLeft && abs(progress) > 0.5 {
MotionTransition.shared.finish()
} else {
MotionTransition.shared.cancel()
}
currentIndex = -1
}
}
/// Prepares interactiveSwipeGesture.
func prepareSwipeGesture() {
guard nil == interactiveSwipeGesture else {
return
}
interactiveSwipeGesture = UIPanGestureRecognizer(target: self, action: #selector(handleTransitionPan))
view.addGestureRecognizer(interactiveSwipeGesture!)
}
/// Removes interactiveSwipeGesture.
func removeSwipeGesture() {
guard let v = interactiveSwipeGesture else {
return
}
view.removeGestureRecognizer(v)
interactiveSwipeGesture = nil
} }
} }
...@@ -167,29 +271,29 @@ private extension BottomNavigationController { ...@@ -167,29 +271,29 @@ private extension BottomNavigationController {
private extension BottomNavigationController { private extension BottomNavigationController {
/** /**
Selects a view controller at a given index. Checks if the view controller at a given index can be selected.
- Parameter at index: An Int. - Parameter at index: An Int.
*/ */
func select(at index: Int) { func canSelect(at index: Int) -> Bool {
guard index != selectedIndex else { guard index != selectedIndex else {
return return false
} }
let lastTabIndex = (tabBar.items?.count ?? 1) - 1 let lastTabIndex = (tabBar.items?.count ?? 1) - 1
guard (0...lastTabIndex).contains(index) else { guard (0...lastTabIndex).contains(index) else {
return return false
} }
guard !(index == lastTabIndex && tabBar.items?.last == moreNavigationController.tabBarItem) else { guard !(index == lastTabIndex && tabBar.items?.last == moreNavigationController.tabBarItem) else {
return return false
} }
let vc = viewControllers![index] let vc = viewControllers![index]
guard delegate?.tabBarController?(self, shouldSelect: vc) != false else { guard delegate?.tabBarController?(self, shouldSelect: vc) != false else {
return return false
} }
selectedIndex = index
delegate?.tabBarController?(self, didSelect: vc) return true
} }
} }
...@@ -206,43 +310,4 @@ private extension BottomNavigationController { ...@@ -206,43 +310,4 @@ private extension BottomNavigationController {
tabBar.backgroundImage = image tabBar.backgroundImage = image
tabBar.backgroundColor = .white tabBar.backgroundColor = .white
} }
/// Prepare the UISwipeGestureRecognizers.
func prepareSwipeGestureRecognizers() {
removeSwipeGestureRecognizers()
let right = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipeGesture(_:)))
right.direction = .right
view.addGestureRecognizer(right)
let left = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipeGesture(_:)))
left.direction = .left
view.addGestureRecognizer(left)
}
/// Remove the UISwipeGestureRecognizers.
func removeSwipeGestureRecognizers() {
view.gestureRecognizers?.compactMap {
$0 as? UISwipeGestureRecognizer
}.filter {
.left == $0.direction || .right == $0.direction
}.forEach {
view.removeGestureRecognizer($0)
}
}
}
private extension BottomNavigationController {
/**
A UISwipeGestureRecognizer that handles swipes.
- Parameter _ gesture: A UISwipeGestureRecognizer.
*/
@objc
func handleSwipeGesture(_ gesture: UISwipeGestureRecognizer) {
guard selectedIndex != NSNotFound else {
return
}
select(at: .right == gesture.direction ? selectedIndex - 1 : selectedIndex + 1)
}
} }
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
*/ */
import UIKit import UIKit
import Motion
open class TabItem: FlatButton { open class TabItem: FlatButton {
/// A dictionary of TabItemStates to UIColors for states. /// A dictionary of TabItemStates to UIColors for states.
...@@ -278,6 +279,7 @@ open class TabBar: Bar { ...@@ -278,6 +279,7 @@ open class TabBar: Bar {
didSet { didSet {
oldValue?.isSelected = false oldValue?.isSelected = false
selectedTabItem?.isSelected = true selectedTabItem?.isSelected = true
updateScrollView()
} }
} }
...@@ -574,6 +576,55 @@ extension TabBar { ...@@ -574,6 +576,55 @@ extension TabBar {
} }
} }
internal extension TabBar {
/**
Starts line transition for the index with the given duration.
- Parameter for index: An Int.
- Parameter duration: A TimeInterval.
*/
func startLineTransition(for index: Int, duration: TimeInterval = 0.35) {
guard let s = selectedTabItem, let currentIndex = tabItems.firstIndex(of: s) else {
return
}
guard currentIndex != index else {
return
}
let targetFrame = lineFrame(for: tabItems[index], forMotion: true)
line.transition(.size(targetFrame.size),
.position(targetFrame.origin),
.duration(duration))
line.motionViewTransition.start()
}
/**
Updates line transition to the given progress value.
- Parameter _ progress: A CGFloat.
*/
func updateLineTransition(_ progress: CGFloat) {
line.motionViewTransition.update(progress)
}
/**
Finishes line transition.
- Parameter isAnimated: A Boolean indicating if the change should be animated.
*/
func finishLineTransition(isAnimated: Bool = true) {
line.motionViewTransition.finish(isAnimated: isAnimated)
}
/**
Cancels line transition.
- Parameter isAnimated: A Boolean indicating if the change should be animated.
*/
func cancelLineTransition(isAnimated: Bool = true) {
line.motionViewTransition.cancel(isAnimated: isAnimated)
}
}
fileprivate extension TabBar { fileprivate extension TabBar {
/** /**
Removes the tabItem animation handler. Removes the tabItem animation handler.
......
...@@ -92,6 +92,24 @@ public protocol TabsControllerDelegate { ...@@ -92,6 +92,24 @@ public protocol TabsControllerDelegate {
*/ */
@objc @objc
optional func tabsController(tabsController: TabsController, didSelect viewController: UIViewController) optional func tabsController(tabsController: TabsController, didSelect viewController: UIViewController)
/**
A delegation method that is executed when the interactive transition to view controller
will be cancelled.
- Parameter tabsController: A TabsController.
- Parameter viewController: A UIViewController.
*/
@objc
optional func tabsController(tabsController: TabsController, willCancelSelecting viewController: UIViewController)
/**
A delegation method that is executed when the interactive transition to view controller
has been cancelled.
- Parameter tabsController: A TabsController.
- Parameter viewController: A UIViewController.
*/
@objc
optional func tabsController(tabsController: TabsController, didCancelSelecting viewController: UIViewController)
} }
open class TabsController: TransitionController { open class TabsController: TransitionController {
...@@ -110,9 +128,8 @@ open class TabsController: TransitionController { ...@@ -110,9 +128,8 @@ open class TabsController: TransitionController {
@IBInspectable @IBInspectable
public let tabBar = TabBar() public let tabBar = TabBar()
/// A Boolean that controls if the swipe feature is enabled.
/// A Boolean that indicates if the swipe feature is enabled.. open var isSwipeEnabled = true {
open var isSwipeEnabled = false {
didSet { didSet {
guard isSwipeEnabled else { guard isSwipeEnabled else {
removeSwipeGesture() removeSwipeGesture()
...@@ -149,6 +166,18 @@ open class TabsController: TransitionController { ...@@ -149,6 +166,18 @@ open class TabsController: TransitionController {
} }
/** /**
A UIPanGestureRecognizer property internally used for the interactive
swipe.
*/
public private(set) var interactiveSwipeGesture: UIPanGestureRecognizer?
/**
A private integer for storing index of target view controller
during interactive transition.
*/
private var targetIndex = -1
/**
An initializer that initializes the object with a NSCoder object. An initializer that initializes the object with a NSCoder object.
- Parameter aDecoder: A NSCoder instance. - Parameter aDecoder: A NSCoder instance.
*/ */
...@@ -212,36 +241,25 @@ fileprivate extension TabsController { ...@@ -212,36 +241,25 @@ fileprivate extension TabsController {
return return
} }
var isAuto = false if case .auto = motionTransitionType, case .auto = viewController.motionTransitionType {
switch motionTransitionType {
case .auto:
switch viewController.motionTransitionType {
case .auto:
isAuto = true
MotionTransition.shared.setAnimationForNextTransition(fvcIndex < tvcIndex ? .slide(direction: .left) : .slide(direction: .right)) MotionTransition.shared.setAnimationForNextTransition(fvcIndex < tvcIndex ? .slide(direction: .left) : .slide(direction: .right))
default:break
}
default:break
} }
if isTriggeredByUserInteraction { if isTriggeredByUserInteraction {
delegate?.tabsController?(tabsController: self, willSelect: viewController) delegate?.tabsController?(tabsController: self, willSelect: viewController)
} }
super.transition(to: viewController) { [weak self, viewController = viewController, completion = completion] (isFinishing) in super.transition(to: viewController) { [weak self] (isFinishing) in
guard let `self` = self else { guard let `self` = self else {
return return
} }
if isAuto {
MotionTransition.shared.setAnimationForNextTransition(.auto)
}
completion?(isFinishing) completion?(isFinishing)
if isTriggeredByUserInteraction { if isTriggeredByUserInteraction && isFinishing {
self.delegate?.tabsController?(tabsController: self, didSelect: viewController) self.delegate?.tabsController?(tabsController: self, didSelect: viewController)
} else {
self.delegate?.tabsController?(tabsController: self, didCancelSelecting: viewController)
} }
} }
} }
...@@ -279,38 +297,91 @@ fileprivate extension TabsController { ...@@ -279,38 +297,91 @@ fileprivate extension TabsController {
tabBar.tabItems = tabItems tabBar.tabItems = tabItems
tabBar.selectedTabItem = tabItems[selectedIndex] tabBar.selectedTabItem = tabItems[selectedIndex]
} }
}
/// Prepare Swipe Gesture. private extension TabsController {
func prepareSwipeGesture() { /**
removeSwipeGesture() A target method contolling interactive swipe transition based on
gesture recognizer.
- Parameter _ gesture: A UIPanGestureRecognizer.
*/
@objc
func handleTransitionPan(_ gesture: UIPanGestureRecognizer) {
let translationX = gesture.translation(in: nil).x
let velocityX = gesture.velocity(in: nil).x
let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipeGesture(gesture:))) switch gesture.state {
swipeRight.direction = .right case .began, .changed:
view.addGestureRecognizer(swipeRight) let isSlidingLeft = targetIndex == -1 ? velocityX < 0 : translationX < 0
let nextIndex = selectedIndex + (isSlidingLeft ? 1 : -1)
let swipeLeft = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipeGesture(gesture:))) guard nextIndex >= 0, nextIndex < viewControllers.count else {
swipeLeft.direction = .left return
view.addGestureRecognizer(swipeLeft)
} }
}
fileprivate extension TabsController { if targetIndex != nextIndex {
/// Remove Swipe Gesture. /// 5 point threshold
func removeSwipeGesture() { guard abs(translationX) > 5 else {
guard let v = view.gestureRecognizers else {
return return
} }
for gesture in v { if targetIndex != -1 {
guard let recognizer = gesture as? UISwipeGestureRecognizer else { delegate?.tabsController?(tabsController: self, willCancelSelecting: viewControllers[targetIndex])
continue tabBar.cancelLineTransition(isAnimated: false)
MotionTransition.shared.cancel(isAnimated: false)
} }
if .left == recognizer.direction || .right == recognizer.direction { if internalSelect(at: nextIndex, isTriggeredByUserInteraction: true, selectTabItem: false) {
view.removeGestureRecognizer(recognizer) tabBar.startLineTransition(for: nextIndex, duration: 0.35)
targetIndex = nextIndex
} }
} else {
let progress = abs(translationX / view.bounds.width)
tabBar.updateLineTransition(progress)
MotionTransition.shared.update(Double(progress))
}
default:
guard targetIndex != -1 else {
return
}
let progress = (translationX + velocityX) / view.bounds.width
let isUserHandDirectionLeft = progress < 0
let isTargetHandDirectionLeft = targetIndex > selectedIndex
if isUserHandDirectionLeft == isTargetHandDirectionLeft && abs(progress) > 0.5 {
tabBar.finishLineTransition()
MotionTransition.shared.finish()
} else {
tabBar.cancelLineTransition()
MotionTransition.shared.cancel()
delegate?.tabsController?(tabsController: self, willCancelSelecting: viewControllers[targetIndex])
}
targetIndex = -1
} }
} }
/// Prepares interactiveSwipeGesture.
func prepareSwipeGesture() {
guard nil == interactiveSwipeGesture else {
return
}
interactiveSwipeGesture = UIPanGestureRecognizer(target: self, action: #selector(handleTransitionPan))
container.addGestureRecognizer(interactiveSwipeGesture!)
}
/// Removes interactiveSwipeGesture.
func removeSwipeGesture() {
guard let v = interactiveSwipeGesture else {
return
}
container.removeGestureRecognizer(v)
interactiveSwipeGesture = nil
}
} }
fileprivate extension TabsController { fileprivate extension TabsController {
...@@ -363,30 +434,6 @@ fileprivate extension TabsController { ...@@ -363,30 +434,6 @@ fileprivate extension TabsController {
} }
} }
fileprivate extension TabsController {
/**
Handles the swipe gesture.
- Parameter gesture: A UIGestureRecognizer.
*/
@objc
func handleSwipeGesture(gesture: UIGestureRecognizer) {
if let swipeGesture = gesture as? UISwipeGestureRecognizer {
switch swipeGesture.direction {
case .right:
guard (selectedIndex - 1) >= 0 else { return }
internalSelect(at: selectedIndex - 1, isTriggeredByUserInteraction: true, selectTabItem: true)
case .left:
guard (selectedIndex + 1) < viewControllers.count else { return }
internalSelect(at: selectedIndex + 1, isTriggeredByUserInteraction: true, selectTabItem: true)
default:
break
}
}
}
}
extension TabsController { extension TabsController {
/** /**
Transitions to the view controller that is at the given index. Transitions to the view controller that is at the given index.
...@@ -425,6 +472,7 @@ extension TabsController { ...@@ -425,6 +472,7 @@ extension TabsController {
} }
self?.selectedIndex = index self?.selectedIndex = index
self?.tabBar.selectedTabItem = self?.tabBar.tabItems[index]
} }
return true return true
......
...@@ -131,26 +131,30 @@ open class TransitionController: ViewController { ...@@ -131,26 +131,30 @@ open class TransitionController: ViewController {
open func transition(to viewController: UIViewController, completion: ((Bool) -> Void)? = nil) { open func transition(to viewController: UIViewController, completion: ((Bool) -> Void)? = nil) {
prepare(viewController: viewController, in: container) prepare(viewController: viewController, in: container)
switch motionTransitionType { if case .auto = viewController.motionTransitionType {
case .auto:break
default:
switch viewController.motionTransitionType {
case .auto:
viewController.motionTransitionType = motionTransitionType viewController.motionTransitionType = motionTransitionType
default:break
}
} }
view.isUserInteractionEnabled = false view.isUserInteractionEnabled = false
MotionTransition.shared.transition(from: rootViewController, to: viewController, in: container) { [weak self, viewController = viewController, completion = completion] (isFinishing) in MotionTransition.shared.transition(from: rootViewController, to: viewController, in: container) { [weak self] isFinishing in
guard let s = self else { guard let s = self else {
return return
} }
s.rootViewController = viewController defer {
s.view.isUserInteractionEnabled = true s.view.isUserInteractionEnabled = true
completion?(isFinishing) completion?(isFinishing)
} }
guard isFinishing else {
s.removeViewController(viewController: viewController)
s.removeViewController(viewController: s.rootViewController)
s.prepare(viewController: s.rootViewController, in: s.container)
return
}
s.rootViewController = viewController
}
} }
open override func prepare() { open override func prepare() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment