Skip to content

Instantly share code, notes, and snippets.

@borut-t
Last active August 23, 2021 03:33
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save borut-t/9b1b78eb314a4f9b577b252476583083 to your computer and use it in GitHub Desktop.
Save borut-t/9b1b78eb314a4f9b577b252476583083 to your computer and use it in GitHub Desktop.
PannableViewControllerTests
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
}
}
}
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
}
}
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