* Core
* Support bottom/left/right positioned panel.
* Replace FloatingPanelPosition with FloatingPanelState
* Redefine FloatingPanelPosition
* Fix pan interaction of the intrinsic layout
* Allow to modify a target position
* Fix floating point problem
* Use UIScrollView.adjustedContentInset by default
* Add shouldDecelerate(to:)
* Rename contentOrigin to contentOffsetForPinning
* Improve initialLocation
* Introduce numeric springing
* Support moving a panel with numeric springing
* Update FloatingPanelBehavior
* Remove {top,Bottom}InteractionBuffer from FloatingPanelLayout
* Remove the move animator to be same as UIScrollView behavior
* Add floatingPanelDidMove(_:) tests in calling move(to:animated:)
* Update FloatingPanelControllerDelegate
* Move {add,remove}PanelAnimator delegation
* Add FloatingPanelBehaviorAdapter
* Modify access to `behavior` prop
* Update FloatingPanelLayout
* Support bottom/left/right positioned panel.
* Add anchoredPosition property
* Introduce FloatingLayoutAnchor & FloatingPanelIntrinsicLayoutAnchor
* Add FloatingPanelReferenceEdge
* Rename FloatingPanelDefaultLayout to FloatingPanelBottomLayout
* Update FloatingPanelLayoutAdapter
* Add LayoutAdapter.sortedDirectionalPositions
* Reimplement positionY(for:)
* Add edge{most,least}state
* Add orderedStates
* Add edge{Most,Least} properties in LayoutAdapter
* Modify `bottomOverflow` value and rename it to `containerOverflow`
* Modify access to `layout` prop
* Add FloatingPanelLayoutAnchoring/LayoutAnchor/IntrinsicLayoutAnchor
* Add FloatingPanel{Intrinsic}LayoutAnchor tests
* Update FloatingPanelController
* Add surfaceLocation
* update addPanelToParent API
* Update layout reloading logic
* Rename updateLayout() to invalidateLayout()
* Fix contentViewController access level
* Update FloatingPanelControllerDelegate
* Add floatingPanelDidEndDragging(_ vc:willDecelerate:)
* Update floatingPanelWillBeginDecelerating delegate method
* Remove behavior delegate method
* Reimplement the removal action
* Use floatingPanel(_:shouldRemoveAt:with:) delegate
* Update SurfaceView
* Support bottom/left/right positioned panel.
* Add FloatingPanelSurfaceAppearance
* Add SurfaceView.intrinsicContentSize
* Rename FloatingPanelSurfaceView.add(contentView:)
* GrabberHandle for bottom edge grabber
* Rename GrabberHandleView to FloatingPanelGrabberView
* Rename containerMargin & contentInsets to content{Margins,Padding}
* Rename bottomOverflow to containerOverflow
* ObjC Support
* Add @objc to FloatingPanelControllerDelegate
* Add @objc to FloatingPanelController
* Use @objc protocol FloatingPanelBehavior
* Redefine FloatingPanelState for objc.
* Update Utils
* Modify logger prefix
* Update extensions for left/right positions
* Update unit tests
* Update layout tests
* Add test_surfaceView_{contentView,containerView}
* Add test_positionY_top2bottom()
* Add test_moveTo_bottomEdge()
* Update test_warningRetainCycle()
* Fix FloatingPanelControllerTests
* Fix LayoutTests
* Fix FloatingPanelStateTests
* Fix FloatingPanelTest
356 lines
14 KiB
Swift
356 lines
14 KiB
Swift
//
|
|
// Created by Shin Yamamoto on 2018/09/18.
|
|
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
|
//
|
|
|
|
import XCTest
|
|
@testable import FloatingPanel
|
|
|
|
class FloatingPanelControllerTests: XCTestCase {
|
|
override func setUp() {}
|
|
override func tearDown() {}
|
|
|
|
func test_warningRetainCycle() {
|
|
let exp = expectation(description: "Warning retain cycle")
|
|
exp.expectedFulfillmentCount = 2 // For layout & behavior logs
|
|
log.hook = {(log, level) in
|
|
if log.contains("A memory leak will occur by a retain cycle because") {
|
|
XCTAssert(level == .warning)
|
|
exp.fulfill()
|
|
}
|
|
}
|
|
let myVC = MyZombieViewController(nibName: nil, bundle: nil)
|
|
myVC.loadViewIfNeeded()
|
|
wait(for: [exp], timeout: 10)
|
|
}
|
|
|
|
func test_addPanel() {
|
|
guard let rootVC = UIApplication.shared.keyWindow?.rootViewController else { fatalError() }
|
|
let fpc = FloatingPanelController()
|
|
fpc.addPanel(toParent: rootVC)
|
|
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .half).y)
|
|
fpc.move(to: .tip, animated: false)
|
|
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .tip).y)
|
|
}
|
|
|
|
@available(iOS 12.0, *)
|
|
func test_updateLayout_willTransition() {
|
|
class MyDelegate: FloatingPanelControllerDelegate {
|
|
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
|
|
if newCollection.userInterfaceStyle == .dark {
|
|
XCTFail()
|
|
}
|
|
return FloatingPanelBottomLayout()
|
|
}
|
|
}
|
|
let myDelegate = MyDelegate()
|
|
let fpc = FloatingPanelController(delegate: myDelegate)
|
|
let traitCollection = UITraitCollection(traitsFrom: [fpc.traitCollection,
|
|
UITraitCollection(userInterfaceStyle: .dark)])
|
|
XCTAssertEqual(traitCollection.userInterfaceStyle, .dark)
|
|
}
|
|
|
|
func test_moveTo() {
|
|
let delegate = FloatingPanelTestDelegate()
|
|
let fpc = FloatingPanelController(delegate: delegate)
|
|
XCTAssertEqual(delegate.position, .hidden)
|
|
fpc.showForTest()
|
|
XCTAssertEqual(delegate.position, .half)
|
|
|
|
fpc.hide()
|
|
XCTAssertEqual(delegate.position, .hidden)
|
|
|
|
fpc.move(to: .full, animated: false)
|
|
XCTAssertEqual(fpc.state, .full)
|
|
XCTAssertEqual(delegate.position, .full)
|
|
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .full).y)
|
|
|
|
fpc.move(to: .half, animated: false)
|
|
XCTAssertEqual(fpc.state, .half)
|
|
XCTAssertEqual(delegate.position, .half)
|
|
|
|
XCTAssertEqual(fpc.surfaceLocation, fpc.surfaceLocation(for: .half))
|
|
|
|
fpc.move(to: .tip, animated: false)
|
|
XCTAssertEqual(fpc.state, .tip)
|
|
XCTAssertEqual(delegate.position, .tip)
|
|
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .tip).y)
|
|
|
|
fpc.move(to: .hidden, animated: false)
|
|
XCTAssertEqual(fpc.state, .hidden)
|
|
XCTAssertEqual(delegate.position, .hidden)
|
|
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .hidden).y)
|
|
|
|
XCTContext.runActivity(named: "move to full(animated)") { act in
|
|
let exp = expectation(description: act.name)
|
|
fpc.move(to: .full, animated: true) {
|
|
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .full).y)
|
|
exp.fulfill()
|
|
}
|
|
XCTAssertEqual(fpc.state, .full)
|
|
XCTAssertEqual(delegate.position, .full)
|
|
wait(for: [exp], timeout: 0.5)
|
|
}
|
|
|
|
XCTContext.runActivity(named: "move to half(animated)") { act in
|
|
let exp = expectation(description: act.name)
|
|
fpc.move(to: .half, animated: true) {
|
|
XCTAssertEqual(fpc.surfaceLocation, fpc.surfaceLocation(for: .half))
|
|
exp.fulfill()
|
|
}
|
|
XCTAssertEqual(fpc.state, .half)
|
|
XCTAssertEqual(delegate.position, .half)
|
|
wait(for: [exp], timeout: 0.8)
|
|
}
|
|
|
|
XCTContext.runActivity(named: "move to tip(animated)") { act in
|
|
let exp = expectation(description: act.name)
|
|
fpc.move(to: .tip, animated: true) {
|
|
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .tip).y)
|
|
exp.fulfill()
|
|
}
|
|
XCTAssertEqual(fpc.state, .tip)
|
|
XCTAssertEqual(delegate.position, .tip)
|
|
wait(for: [exp], timeout: 0.8)
|
|
}
|
|
|
|
fpc.move(to: .hidden, animated: true)
|
|
XCTAssertEqual(fpc.state, .hidden)
|
|
XCTAssertEqual(delegate.position, .hidden)
|
|
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .hidden).y)
|
|
}
|
|
|
|
func test_moveTo_bottomEdge() {
|
|
class MyFloatingPanelTop2BottomLayout: FloatingPanelTop2BottomTestLayout {
|
|
override var initialState: FloatingPanelState { return .half }
|
|
}
|
|
let delegate = FloatingPanelTestDelegate()
|
|
let fpc = FloatingPanelController(delegate: delegate)
|
|
fpc.layout = MyFloatingPanelTop2BottomLayout()
|
|
XCTAssertEqual(delegate.position, .hidden)
|
|
fpc.showForTest()
|
|
XCTAssertEqual(delegate.position, .half)
|
|
|
|
fpc.hide()
|
|
XCTAssertEqual(delegate.position, .hidden)
|
|
|
|
fpc.move(to: .full, animated: false)
|
|
XCTAssertEqual(fpc.state, .full)
|
|
XCTAssertEqual(delegate.position, .full)
|
|
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .full).y)
|
|
|
|
fpc.move(to: .half, animated: false)
|
|
XCTAssertEqual(fpc.state, .half)
|
|
XCTAssertEqual(delegate.position, .half)
|
|
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .half).y)
|
|
|
|
fpc.move(to: .tip, animated: false)
|
|
XCTAssertEqual(fpc.state, .tip)
|
|
XCTAssertEqual(delegate.position, .tip)
|
|
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .tip).y)
|
|
|
|
fpc.move(to: .hidden, animated: false)
|
|
XCTAssertEqual(fpc.state, .hidden)
|
|
XCTAssertEqual(delegate.position, .hidden)
|
|
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .hidden).y)
|
|
|
|
XCTContext.runActivity(named: "move to full(animated)") { act in
|
|
let exp = expectation(description: act.name)
|
|
fpc.move(to: .full, animated: true) {
|
|
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .full).y)
|
|
exp.fulfill()
|
|
}
|
|
XCTAssertEqual(fpc.state, .full)
|
|
XCTAssertEqual(delegate.position, .full)
|
|
wait(for: [exp], timeout: 0.5)
|
|
}
|
|
|
|
XCTContext.runActivity(named: "move to half(animated)") { act in
|
|
let exp = expectation(description: act.name)
|
|
fpc.move(to: .half, animated: true) {
|
|
XCTAssertEqual(fpc.surfaceLocation, fpc.surfaceLocation(for: .half))
|
|
exp.fulfill()
|
|
}
|
|
XCTAssertEqual(fpc.state, .half)
|
|
XCTAssertEqual(delegate.position, .half)
|
|
wait(for: [exp], timeout: 0.8)
|
|
}
|
|
|
|
XCTContext.runActivity(named: "move to tip(animated)") { act in
|
|
let exp = expectation(description: act.name)
|
|
fpc.move(to: .tip, animated: true) {
|
|
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .tip).y)
|
|
exp.fulfill()
|
|
}
|
|
XCTAssertEqual(fpc.state, .tip)
|
|
XCTAssertEqual(delegate.position, .tip)
|
|
wait(for: [exp], timeout: 0.8)
|
|
}
|
|
|
|
fpc.move(to: .hidden, animated: true)
|
|
XCTAssertEqual(fpc.state, .hidden)
|
|
XCTAssertEqual(delegate.position, .hidden)
|
|
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .hidden).y)
|
|
}
|
|
|
|
func test_moveWithNearbyPosition() {
|
|
let delegate = FloatingPanelTestDelegate()
|
|
let fpc = FloatingPanelController(delegate: delegate)
|
|
XCTAssertEqual(delegate.position, .hidden)
|
|
fpc.showForTest()
|
|
|
|
XCTAssertEqual(fpc.nearbyState, .half)
|
|
|
|
fpc.hide()
|
|
XCTAssertEqual(fpc.nearbyState, .tip)
|
|
|
|
fpc.move(to: .full, animated: false)
|
|
XCTAssertEqual(fpc.nearbyState, .full)
|
|
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.surfaceLocation(for: .full).y)
|
|
}
|
|
|
|
func test_moveTo_didMoveDelegate() {
|
|
let delegate = FloatingPanelTestDelegate()
|
|
let fpc = FloatingPanelController(delegate: delegate)
|
|
XCTAssertEqual(delegate.position, .hidden)
|
|
fpc.showForTest()
|
|
|
|
XCTContext.runActivity(named: "move(to:animated:false") { act in
|
|
let exp = expectation(description: act.name)
|
|
exp.expectedFulfillmentCount = 1
|
|
var count = 0
|
|
delegate.didMoveCallback = { _ in
|
|
count += 1
|
|
exp.fulfill()
|
|
}
|
|
fpc.move(to: .full, animated: false)
|
|
wait(for: [exp], timeout: 1.0)
|
|
|
|
XCTAssertEqual(count, 1)
|
|
}
|
|
|
|
XCTContext.runActivity(named: "move(to:animated:true)") { act in
|
|
let exp = expectation(description: act.name)
|
|
exp.assertForOverFulfill = false
|
|
exp.expectedFulfillmentCount = 1
|
|
var count = 0
|
|
delegate.didMoveCallback = { _ in
|
|
count += 1
|
|
}
|
|
fpc.move(to: .half, animated: true) {
|
|
exp.fulfill()
|
|
}
|
|
wait(for: [exp], timeout: 1.0)
|
|
|
|
XCTAssertGreaterThan(count, 1)
|
|
}
|
|
|
|
XCTContext.runActivity(named: "move(to:animated:false) with animation") { act in
|
|
let exp = expectation(description: act.name)
|
|
exp.expectedFulfillmentCount = 1
|
|
var count = 0
|
|
delegate.didMoveCallback = { _ in
|
|
count += 1
|
|
}
|
|
UIView.animate(withDuration: 0.3) {
|
|
fpc.move(to: .full, animated: false) {
|
|
exp.fulfill()
|
|
}
|
|
}
|
|
wait(for: [exp], timeout: 1.0)
|
|
|
|
XCTAssertEqual(count, 1)
|
|
}
|
|
|
|
XCTContext.runActivity(named: "move(to:animated:true) with animation") { act in
|
|
let exp = expectation(description: act.name)
|
|
exp.assertForOverFulfill = false
|
|
exp.expectedFulfillmentCount = 1
|
|
var count = 0
|
|
delegate.didMoveCallback = { _ in
|
|
count += 1
|
|
}
|
|
UIView.animate(withDuration: 0.3) {
|
|
fpc.move(to: .half, animated: true) {
|
|
exp.fulfill()
|
|
}
|
|
}
|
|
wait(for: [exp], timeout: 1.0)
|
|
|
|
XCTAssertGreaterThan(count, 1)
|
|
}
|
|
}
|
|
|
|
func test_originSurfaceY() {
|
|
let fpc = FloatingPanelController(delegate: nil)
|
|
fpc.loadViewIfNeeded()
|
|
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
|
|
fpc.show(animated: false, completion: nil)
|
|
|
|
fpc.move(to: .full, animated: false)
|
|
XCTAssertEqual(fpc.surfaceLocation, fpc.surfaceLocation(for: .full))
|
|
fpc.move(to: .half, animated: false)
|
|
XCTAssertEqual(fpc.surfaceLocation, fpc.surfaceLocation(for: .half))
|
|
fpc.move(to: .tip, animated: false)
|
|
XCTAssertEqual(fpc.surfaceLocation, fpc.surfaceLocation(for: .tip))
|
|
fpc.move(to: .hidden, animated: false)
|
|
XCTAssertEqual(fpc.surfaceLocation, fpc.surfaceLocation(for: .hidden))
|
|
}
|
|
|
|
func test_contentMode() {
|
|
let fpc = FloatingPanelController(delegate: nil)
|
|
fpc.loadViewIfNeeded()
|
|
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
|
|
fpc.show(animated: false, completion: nil)
|
|
|
|
fpc.contentMode = .static
|
|
|
|
fpc.move(to: .full, animated: false)
|
|
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .full).y)
|
|
fpc.move(to: .half, animated: false)
|
|
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .full).y)
|
|
fpc.move(to: .tip, animated: false)
|
|
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .full).y)
|
|
|
|
fpc.contentMode = .fitToBounds
|
|
|
|
fpc.move(to: .full, animated: false)
|
|
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .full).y)
|
|
fpc.move(to: .half, animated: false)
|
|
print(1 / fpc.surfaceView.traitCollection.displayScale)
|
|
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .half).y)
|
|
fpc.move(to: .tip, animated: false)
|
|
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .tip).y)
|
|
}
|
|
}
|
|
|
|
private class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
|
|
var fpc: FloatingPanelController?
|
|
override func viewDidLoad() {
|
|
fpc = FloatingPanelController(delegate: self)
|
|
fpc?.addPanel(toParent: self)
|
|
fpc?.layout = self
|
|
fpc?.behavior = self
|
|
}
|
|
var anchorPosition: FloatingPanelPosition {
|
|
return .bottom
|
|
}
|
|
var initialState: FloatingPanelState {
|
|
return .half
|
|
}
|
|
|
|
var stateAnchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
|
return [
|
|
.full: FloatingPanelLayoutAnchor(absoluteInset: UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0,
|
|
edge: .top,
|
|
referenceGuide: .superview),
|
|
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0,
|
|
edge: .bottom,
|
|
referenceGuide: .superview),
|
|
.tip: FloatingPanelLayoutAnchor(absoluteInset: 60.0,
|
|
edge: .bottom,
|
|
referenceGuide: .superview),
|
|
]
|
|
}
|
|
}
|