Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added interactive swipe #1165

Merged
merged 6 commits into from
Sep 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 116 additions & 51 deletions Sources/iOS/BottomNavigationController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
*/

import UIKit
import Motion

extension UIViewController {
/**
Expand All @@ -51,18 +52,30 @@ private class MaterialTabBar: UITabBar {
}

open class BottomNavigationController: UITabBarController {
/// A Boolean that indicates if the swipe feature is enabled..
open var isSwipeEnabled = false {
/// A Boolean that controls if the swipe feature is enabled.
open var isSwipeEnabled = true {
didSet {
guard isSwipeEnabled else {
removeSwipeGestureRecognizers()
removeSwipeGesture()
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.
- Parameter aDecoder: A NSCoder instance.
Expand Down Expand Up @@ -151,6 +164,97 @@ open class BottomNavigationController: UITabBarController {
view.contentScaleFactor = Screen.scale

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
daniel-jonathan marked this conversation as resolved.
Show resolved Hide resolved

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

Expand All @@ -167,29 +271,29 @@ 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.
*/
func select(at index: Int) {
func canSelect(at index: Int) -> Bool {
guard index != selectedIndex else {
return
return false
}

let lastTabIndex = (tabBar.items?.count ?? 1) - 1
guard (0...lastTabIndex).contains(index) else {
return
return false
}

guard !(index == lastTabIndex && tabBar.items?.last == moreNavigationController.tabBarItem) else {
return
return false
}

let vc = viewControllers![index]
guard delegate?.tabBarController?(self, shouldSelect: vc) != false else {
return
return false
}
selectedIndex = index
delegate?.tabBarController?(self, didSelect: vc)

return true
}
}

Expand All @@ -206,43 +310,4 @@ private extension BottomNavigationController {
tabBar.backgroundImage = image
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)
}
}
51 changes: 51 additions & 0 deletions Sources/iOS/TabBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
*/

import UIKit
import Motion

open class TabItem: FlatButton {
/// A dictionary of TabItemStates to UIColors for states.
Expand Down Expand Up @@ -278,6 +279,7 @@ open class TabBar: Bar {
didSet {
oldValue?.isSelected = false
selectedTabItem?.isSelected = true
updateScrollView()
}
}

Expand Down Expand Up @@ -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 {
/**
Removes the tabItem animation handler.
Expand Down
Loading