Last active
August 23, 2021 03:33
-
-
Save borut-t/9b1b78eb314a4f9b577b252476583083 to your computer and use it in GitHub Desktop.
PannableViewControllerTests
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
import UIKit | |
class PannableViewController: ViewController { | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGesture(_:))) | |
view.addGestureRecognizer(panGesture) | |
} | |
} | |
// MARK: - Private Methods | |
private extension PannableViewController { | |
@objc func panGesture(_ gesture: UIPanGestureRecognizer) { | |
let translation = gesture.translation(in: view) | |
guard translation.y >= 0 else { return } | |
switch gesture.state { | |
case .began, .changed: | |
let offset = max(safeAreaTopInset, translation.y + safeAreaTopInset) | |
view.frame.origin.y = offset | |
case .ended: | |
let velocity = gesture.velocity(in: view) | |
let shouldDismiss = velocity.y > 0 && (velocity.y > minimumVelocityToHide || translation.y > view.frame.height * minimumScreenRatioToHide) | |
if shouldDismiss { | |
self.view.frame.origin.y = self.view.frame.height | |
self.dismiss(animated: false, completion: nil) | |
} else { | |
self.view.frame.origin.y = 0 | |
} | |
case .possible, .cancelled, .failed: | |
self.view.frame.origin.y = 0 | |
} | |
} | |
} |
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
import XCTest | |
@testable import Project | |
class PannableViewControllerTests: XCTestCase { | |
private let vc = PannableViewControllerInjector() | |
override func setUp() { | |
super.setUp() | |
_ = vc.view | |
} | |
func testSetup() { | |
XCTAssertNotNil(vc.view.gestureRecognizers?.first(where: { $0 is UIPanGestureRecognizerMock })) | |
} | |
func testPanBegan() { | |
vc.gestureRecognizer?.pan(location: nil, translation: .zero, state: .began) | |
XCTAssertEqual(vc.view.frame.minY, 0) | |
} | |
func testPanDownwards() { | |
vc.gestureRecognizer?.pan(location: nil, translation: CGPoint(x: 0, y: 200), state: .changed) | |
XCTAssertEqual(vc.view.frame.minY, 200) | |
} | |
func testPanUpwardsFromStart() { | |
vc.gestureRecognizer?.pan(location: nil, translation: CGPoint(x: 0, y: -200), state: .changed) | |
XCTAssertEqual(vc.view.frame.minY, 0) | |
} | |
func testPanUpwardsFromMiddle() { | |
vc.gestureRecognizer?.pan(location: nil, translation: CGPoint(x: 0, y: 200), state: .changed) | |
vc.gestureRecognizer?.pan(location: nil, translation: CGPoint(x: 0, y: 100), state: .changed) | |
XCTAssertEqual(vc.view.frame.minY, 100) | |
} | |
func testPanEndedShouldDismiss() { | |
vc.gestureRecognizer?.gestureVelocity = .init(x: 0, y: 1) | |
let offset = vc.view.frame.height * vc.minimumScreenRatioToHide + 1 | |
vc.gestureRecognizer?.pan(location: nil, translation: CGPoint(x: 0, y: offset), state: .ended) | |
XCTAssertEqual(vc.view.frame.minY, vc.view.frame.height) | |
} | |
func testPanEndedShouldResetDueToLowVelocity() { | |
vc.gestureRecognizer?.gestureVelocity = .init(x: 0, y: 0) | |
let offset = vc.view.frame.height * vc.minimumScreenRatioToHide + 1 | |
vc.gestureRecognizer?.pan(location: nil, translation: CGPoint(x: 0, y: offset), state: .ended) | |
XCTAssertEqual(vc.view.frame.minY, 0) | |
} | |
func testPanEndedShouldResetDueMinimumScreenRatioNotMet() { | |
vc.gestureRecognizer?.gestureVelocity = .init(x: 0, y: 1) | |
let offset = vc.view.frame.height * vc.minimumScreenRatioToHide - 1 | |
vc.gestureRecognizer?.pan(location: nil, translation: CGPoint(x: 0, y: offset), state: .ended) | |
XCTAssertEqual(vc.view.frame.minY, 0) | |
} | |
func testPanCancelled() { | |
vc.gestureRecognizer?.pan(location: nil, translation: CGPoint(x: 0, y: 1), state: .cancelled) | |
XCTAssertEqual(vc.view.frame.minY, 0) | |
} | |
} | |
private class PannableViewControllerInjector: PannableViewController { | |
var gestureRecognizer: UIPanGestureRecognizerMock? | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
guard let existingGestureRecognizer = view.gestureRecognizers?.first(where: { $0 is UIPanGestureRecognizer }) as? UIPanGestureRecognizer else { return } | |
view.removeGestureRecognizer(existingGestureRecognizer) | |
let action = Selector("panGesture:") // we need to create Objective-C selector instead because a caller metod is private | |
let newGestureRecognizer = UIPanGestureRecognizerMock(target: self, action: action) | |
view.addGestureRecognizer(newGestureRecognizer) | |
gestureRecognizer = newGestureRecognizer | |
} | |
} |
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
import UIKit | |
class UIPanGestureRecognizerMock: UIPanGestureRecognizer { | |
private let target: Any? | |
private let action: Selector? | |
var gestureState: UIGestureRecognizerState? | |
var gestureLocation: CGPoint? | |
var gestureTranslation: CGPoint? | |
var gestureVelocity: CGPoint? | |
override init(target: Any?, action: Selector?) { | |
self.target = target | |
self.action = action | |
super.init(target: target, action: action) | |
} | |
override func location(in view: UIView?) -> CGPoint { | |
if let gestureLocation = gestureLocation { | |
return gestureLocation | |
} | |
return super.location(in: view) | |
} | |
override func translation(in view: UIView?) -> CGPoint { | |
if let gestureTranslation = gestureTranslation { | |
return gestureTranslation | |
} | |
return super.translation(in: view) | |
} | |
override func velocity(in view: UIView?) -> CGPoint { | |
if let gestureVelocity = gestureVelocity { | |
return gestureVelocity | |
} | |
return super.velocity(in: view) | |
} | |
override var state: UIGestureRecognizerState { | |
get { | |
if let gestureState = gestureState { | |
return gestureState | |
} | |
return super.state | |
} | |
set { | |
self.state = newValue | |
} | |
} | |
} | |
extension UIPanGestureRecognizerMock { | |
func pan(location: CGPoint?, translation: CGPoint?, state: UIGestureRecognizerState) { | |
guard let action = action else { return } | |
gestureState = state | |
gestureLocation = location | |
gestureTranslation = translation | |
(target as? NSObject)?.perform(action, on: Thread.current, with: self, waitUntilDone: true) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment