Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a225bf2cf1 | |||
| 11a16092a7 | |||
| b9b7f940b9 | |||
| 206475e6ab | |||
| a4a68e5b39 | |||
| de7ab0e0cb | |||
| 5f7b5ce81c | |||
| 36d7ea5100 | |||
| 33f8cf3802 | |||
| f6da876fdf | |||
| 96c5dc7b74 | |||
| a37931b62d | |||
| 5c848d9bf5 | |||
| 265b805fa9 | |||
| c4dfe33a5e | |||
| 999eeb47ba | |||
| a5bf02cfec | |||
| c10186e50a | |||
| 7a1cbf99d4 | |||
| c9c4000536 | |||
| 656bbc1b1c | |||
| 3815a08af5 | |||
| 404fdb6496 | |||
| 573f355c15 | |||
| bd0c891795 | |||
| f4857a3da9 | |||
| e074c3caf1 | |||
| 0f4c7503b1 | |||
| 2cb142a31f | |||
| 2b05ea8d92 | |||
| d255e1ea4a | |||
| 6fcb817fb8 | |||
| e2ebfd01df | |||
| cf70929204 | |||
| 624e3f7553 | |||
| 3cc8538db3 | |||
| a9a65436bb | |||
| 353dabfc47 | |||
| 1bdf0f5b78 | |||
| 6696d7f71d | |||
| 59a6c7e576 | |||
| 0b0148635e | |||
| c354d8ea92 | |||
| 9562cdaccb | |||
| bcfff8a33a | |||
| f5c409ba90 | |||
| 2f23520330 | |||
| a95694cbfc | |||
| 6cfba6495f | |||
| b9f3de1c64 | |||
| c67b56e7af | |||
| bf39f07691 | |||
| a9e46f0de6 | |||
| 05478fa8fa | |||
| d123afc3f7 | |||
| b1b3c15300 | |||
| 49bae50739 | |||
| 9b5459af8e | |||
| 96d2ea57f5 | |||
| b78c5f4ece | |||
| 341522ccaa | |||
| 833628e42f | |||
| 50c1c6fdc9 | |||
| 213386e822 | |||
| 17317ed274 | |||
| 652ae8c967 | |||
| ec0e8cbdaf | |||
| c15d4c9035 | |||
| 39dfdd0ef0 | |||
| d25bc58249 | |||
| 194a197e83 | |||
| bd02f34bcf | |||
| 60f41e168f |
@@ -22,6 +22,7 @@ xcuserdata/
|
||||
*.moved-aside
|
||||
*.xccheckout
|
||||
*.xcscmblueprint
|
||||
*.xcsettings
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
|
||||
+28
-25
@@ -1,4 +1,4 @@
|
||||
language: swift
|
||||
language: objective-c
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
@@ -12,23 +12,29 @@ env:
|
||||
global:
|
||||
- LANG=en_US.UTF-8
|
||||
- LC_ALL=en_US.UTF-8
|
||||
skip_cleanup: true
|
||||
jobs:
|
||||
include:
|
||||
- stage: Build framework(swift 4.1)
|
||||
- stage: "Builds"
|
||||
osx_image: xcode9.4
|
||||
script:
|
||||
- xcodebuild -scheme FloatingPanel SWIFT_VERSION=4.1 clean build
|
||||
|
||||
- stage: Build framework(swift 4.2)
|
||||
script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=4.1 clean build
|
||||
name: "Swift 4.1"
|
||||
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=4.2 clean build
|
||||
osx_image: xcode10
|
||||
script:
|
||||
- xcodebuild -scheme FloatingPanel SWIFT_VERSION=4.2 clean build
|
||||
|
||||
- stage: Build framework(swift 5.0)
|
||||
name: "Swift 4.2"
|
||||
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.0 clean build
|
||||
osx_image: xcode10.2
|
||||
script:
|
||||
- xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.0 clean build
|
||||
name: "Swift 5.0"
|
||||
|
||||
- stage: "Tests"
|
||||
osx_image: xcode10.2
|
||||
script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=10.3.1,name=iPhone SE'
|
||||
name: "iPhone SE (iOS 10.3)"
|
||||
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=11.4,name=iPhone 7'
|
||||
osx_image: xcode10.2
|
||||
name: "iPhone 7 (iOS 11.4)"
|
||||
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=12.2,name=iPhone X'
|
||||
osx_image: xcode10.2
|
||||
name: "iPhone X (iOS 12.2)"
|
||||
|
||||
- stage: Carthage
|
||||
osx_image: xcode10.2
|
||||
@@ -38,22 +44,19 @@ jobs:
|
||||
script:
|
||||
- carthage build --no-skip-current
|
||||
|
||||
- stage: Podspec
|
||||
- stage: CocoaPods
|
||||
osx_image: xcode10.2
|
||||
script:
|
||||
- pod spec lint
|
||||
- pod lib lint
|
||||
|
||||
- stage: Build maps example
|
||||
- stage: Build examples
|
||||
osx_image: xcode10.2
|
||||
script:
|
||||
- xcodebuild -scheme Maps -sdk iphonesimulator clean build
|
||||
|
||||
- stage: Build stocks example
|
||||
script: xcodebuild -scheme Maps -sdk iphonesimulator clean build
|
||||
name: "Maps"
|
||||
- script: xcodebuild -scheme Stocks -sdk iphonesimulator clean build
|
||||
osx_image: xcode10.2
|
||||
script:
|
||||
- xcodebuild -scheme Stocks -sdk iphonesimulator clean build
|
||||
|
||||
- stage: Build samples example
|
||||
name: "Stocks"
|
||||
- script: xcodebuild -scheme Samples -sdk iphonesimulator clean build
|
||||
osx_image: xcode10.2
|
||||
script:
|
||||
- xcodebuild -scheme Samples -sdk iphonesimulator clean build
|
||||
name: "Samples"
|
||||
|
||||
@@ -21,7 +21,11 @@ class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate,
|
||||
|
||||
// Initialize FloatingPanelController and add the view
|
||||
fpc.surfaceView.backgroundColor = .clear
|
||||
fpc.surfaceView.cornerRadius = 9.0
|
||||
if #available(iOS 11, *) {
|
||||
fpc.surfaceView.cornerRadius = 9.0
|
||||
} else {
|
||||
fpc.surfaceView.cornerRadius = 0.0
|
||||
}
|
||||
fpc.surfaceView.shadowHidden = false
|
||||
|
||||
searchVC = storyboard?.instantiateViewController(withIdentifier: "SearchPanel") as? SearchPanelViewController
|
||||
@@ -135,7 +139,10 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl
|
||||
@IBOutlet weak var tableView: UITableView!
|
||||
@IBOutlet weak var searchBar: UISearchBar!
|
||||
@IBOutlet weak var visualEffectView: UIVisualEffectView!
|
||||
|
||||
|
||||
// For iOS 10 only
|
||||
private lazy var shadowLayer: CAShapeLayer = CAShapeLayer()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableView.dataSource = self
|
||||
@@ -150,9 +157,24 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
if #available(iOS 10, *) {
|
||||
if #available(iOS 11, *) {
|
||||
} else {
|
||||
// Exmaple: Add rounding corners on iOS 10
|
||||
visualEffectView.layer.cornerRadius = 9.0
|
||||
visualEffectView.clipsToBounds = true
|
||||
|
||||
// Exmaple: Add shadow manually on iOS 10
|
||||
view.layer.insertSublayer(shadowLayer, at: 0)
|
||||
let rect = visualEffectView.frame
|
||||
let path = UIBezierPath(roundedRect: rect,
|
||||
byRoundingCorners: [.topLeft, .topRight],
|
||||
cornerRadii: CGSize(width: 9.0, height: 9.0))
|
||||
shadowLayer.frame = visualEffectView.frame
|
||||
shadowLayer.shadowPath = path.cgPath
|
||||
shadowLayer.shadowColor = UIColor.black.cgColor
|
||||
shadowLayer.shadowOffset = CGSize(width: 0.0, height: 1.0)
|
||||
shadowLayer.shadowOpacity = 0.2
|
||||
shadowLayer.shadowRadius = 3.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ class SampleListViewController: UIViewController {
|
||||
case showNestedScrollView
|
||||
case showRemovablePanel
|
||||
case showIntrinsicView
|
||||
case showContentInset
|
||||
|
||||
var name: String {
|
||||
switch self {
|
||||
@@ -36,6 +37,7 @@ class SampleListViewController: UIViewController {
|
||||
case .showNestedScrollView: return "Show Nested ScrollView"
|
||||
case .showRemovablePanel: return "Show Removable Panel"
|
||||
case .showIntrinsicView: return "Show Intrinsic View"
|
||||
case .showContentInset: return "Show with ContentInset"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +53,7 @@ class SampleListViewController: UIViewController {
|
||||
case .showNestedScrollView: return "NestedScrollViewController"
|
||||
case .showRemovablePanel: return "DetailViewController"
|
||||
case .showIntrinsicView: return "IntrinsicViewController"
|
||||
case .showContentInset: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,6 +132,11 @@ class SampleListViewController: UIViewController {
|
||||
|
||||
// Enable tap-to-hide and removal interaction
|
||||
switch currentMenu {
|
||||
case .trackingTableView:
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSurface(tapGesture:)))
|
||||
tapGesture.cancelsTouchesInView = false
|
||||
tapGesture.numberOfTapsRequired = 2
|
||||
mainPanelVC.surfaceView.addGestureRecognizer(tapGesture)
|
||||
case .showRemovablePanel, .showIntrinsicView:
|
||||
mainPanelVC.isRemovalInteractionEnabled = true
|
||||
|
||||
@@ -159,8 +167,14 @@ class SampleListViewController: UIViewController {
|
||||
mainPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
|
||||
}
|
||||
|
||||
@objc func dismissDetailPanelVC() {
|
||||
detailPanelVC.removePanelFromParent(animated: true, completion: nil)
|
||||
@objc
|
||||
func handleSurface(tapGesture: UITapGestureRecognizer) {
|
||||
switch mainPanelVC.position {
|
||||
case .full:
|
||||
mainPanelVC.move(to: .half, animated: true)
|
||||
default:
|
||||
mainPanelVC.move(to: .full, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
|
||||
@@ -285,6 +299,18 @@ extension SampleListViewController: UITableViewDelegate {
|
||||
fpc.isRemovalInteractionEnabled = true
|
||||
|
||||
self.present(fpc, animated: true, completion: nil)
|
||||
|
||||
case .showContentInset:
|
||||
let contentViewController = UIViewController()
|
||||
contentViewController.view.backgroundColor = .green
|
||||
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.set(contentViewController: contentViewController)
|
||||
fpc.surfaceView.contentInsets = .init(top: 20, left: 20, bottom: 0, right: 20)
|
||||
|
||||
fpc.delegate = self
|
||||
fpc.isRemovalInteractionEnabled = true
|
||||
self.present(fpc, animated: true, completion: nil)
|
||||
default:
|
||||
detailPanelVC?.removePanelFromParent(animated: true, completion: nil)
|
||||
mainPanelVC?.removePanelFromParent(animated: true) {
|
||||
@@ -533,7 +559,7 @@ class InspectableViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
class DebugTableViewController: InspectableViewController, UITableViewDataSource, UITableViewDelegate {
|
||||
class DebugTableViewController: InspectableViewController {
|
||||
weak var tableView: UITableView!
|
||||
var items: [String] = []
|
||||
var itemHeight: CGFloat = 66.0
|
||||
@@ -651,7 +677,9 @@ class DebugTableViewController: InspectableViewController, UITableViewDataSource
|
||||
// Remove FloatingPanel from a view
|
||||
(self.parent as! FloatingPanelController).removePanelFromParent(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension DebugTableViewController: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return items.count
|
||||
}
|
||||
@@ -665,6 +693,12 @@ class DebugTableViewController: InspectableViewController, UITableViewDataSource
|
||||
cell.textLabel?.text = items[indexPath.row]
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
extension DebugTableViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
print("DebugTableViewController -- select row \(indexPath.row)")
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
return [
|
||||
@@ -922,6 +956,15 @@ extension TabBarContentViewController: FloatingPanelControllerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
|
||||
switch self.tabBarItem.tag {
|
||||
case 1:
|
||||
return TwoTabBarPanelBehavior()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func floatingPanelDidMove(_ vc: FloatingPanelController) {
|
||||
guard self.tabBarItem.tag == 2 else { return }
|
||||
|
||||
@@ -1042,19 +1085,29 @@ class TwoTabBarPanelLayout: FloatingPanelLayout {
|
||||
var supportedPositions: Set<FloatingPanelPosition> {
|
||||
return [.full, .half]
|
||||
}
|
||||
var topInteractionBuffer: CGFloat {
|
||||
return 100.0
|
||||
}
|
||||
var bottomInteractionBuffer: CGFloat {
|
||||
return 261.0 - 22.0
|
||||
}
|
||||
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
switch position {
|
||||
case .full: return 16.0
|
||||
case .full: return 100.0
|
||||
case .half: return 261.0
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TwoTabBarPanelBehavior: FloatingPanelBehavior {
|
||||
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
|
||||
return (edge == .bottom || edge == .top)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ThreeTabBarPanelLayout: FloatingPanelFullScreenLayout {
|
||||
weak var parentVC: UIViewController!
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "FloatingPanel"
|
||||
s.version = "1.5.1"
|
||||
s.version = "1.6.2"
|
||||
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
|
||||
s.description = <<-DESC
|
||||
FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
|
||||
@@ -14,7 +14,6 @@ The new interface displays the related contents and utilities in parallel as a u
|
||||
s.source = { :git => "https://github.com/SCENEE/FloatingPanel.git", :tag => "v#{s.version}" }
|
||||
s.source_files = "Framework/Sources/*.swift"
|
||||
s.swift_version = "4.0"
|
||||
s.pod_target_xcconfig = { 'APPLICATION_EXTENSION_API_ONLY' => 'YES' }
|
||||
|
||||
s.framework = "UIKit"
|
||||
|
||||
|
||||
@@ -7,20 +7,26 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
542753C622C49A6E00D17955 /* FloatingPanelLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */; };
|
||||
542753C822C49A8F00D17955 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C722C49A8F00D17955 /* Utils.swift */; };
|
||||
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */; };
|
||||
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */; };
|
||||
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */; };
|
||||
545DB9CB2151169500CA77B8 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 545DB9C12151169500CA77B8 /* FloatingPanel.framework */; };
|
||||
545DB9D02151169500CA77B8 /* FloatingPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* FloatingPanelTests.swift */; };
|
||||
545DB9D02151169500CA77B8 /* FloatingPanelControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* FloatingPanelControllerTests.swift */; };
|
||||
545DB9D22151169500CA77B8 /* FloatingPanel.h in Headers */ = {isa = PBXBuildFile; fileRef = 545DB9C42151169500CA77B8 /* FloatingPanel.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DD215118C800CA77B8 /* UIExtensions.swift */; };
|
||||
545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */; };
|
||||
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */; };
|
||||
54A6B6B122968B530077F348 /* FloatingPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B022968B530077F348 /* FloatingPanelTests.swift */; };
|
||||
54A6B6B622968F710077F348 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54A6B6B522968F710077F348 /* LaunchScreen.storyboard */; };
|
||||
54A6B6B82296A8520077F348 /* FloatingPanelSurfaceViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */; };
|
||||
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54ABD7AE216CCFF7002E6C13 /* Logger.swift */; };
|
||||
54CDC5D3215B6D5A007D205C /* FloatingPanelSurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */; };
|
||||
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */; };
|
||||
54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */; };
|
||||
54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */; };
|
||||
54E740CD218AFD67005C1A34 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E740CC218AFD67005C1A34 /* AppDelegate.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -31,9 +37,18 @@
|
||||
remoteGlobalIDString = 545DB9C02151169500CA77B8;
|
||||
remoteInfo = FloatingModalController;
|
||||
};
|
||||
54E740DC218AFE9F005C1A34 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 545DB9B82151169500CA77B8 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 54E740C9218AFD67005C1A34;
|
||||
remoteInfo = TestingHost;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelLayoutTests.swift; sourceTree = "<group>"; };
|
||||
542753C722C49A8F00D17955 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
|
||||
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelTransitioning.swift; sourceTree = "<group>"; };
|
||||
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelView.swift; sourceTree = "<group>"; };
|
||||
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBehavior.swift; sourceTree = "<group>"; };
|
||||
@@ -41,16 +56,22 @@
|
||||
545DB9C42151169500CA77B8 /* FloatingPanel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FloatingPanel.h; sourceTree = "<group>"; };
|
||||
545DB9C52151169500CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
545DB9CA2151169500CA77B8 /* FloatingPanelTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FloatingPanelTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
545DB9CF2151169500CA77B8 /* FloatingPanelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelTests.swift; sourceTree = "<group>"; };
|
||||
545DB9CF2151169500CA77B8 /* FloatingPanelControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelControllerTests.swift; sourceTree = "<group>"; };
|
||||
545DB9D12151169500CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
545DB9DD215118C800CA77B8 /* UIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; sourceTree = "<group>"; };
|
||||
545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelController.swift; sourceTree = "<group>"; };
|
||||
545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrabberHandleView.swift; sourceTree = "<group>"; };
|
||||
54A6B6B022968B530077F348 /* FloatingPanelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelTests.swift; sourceTree = "<group>"; };
|
||||
54A6B6B522968F710077F348 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelSurfaceViewTests.swift; sourceTree = "<group>"; };
|
||||
54ABD7AE216CCFF7002E6C13 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelSurfaceView.swift; sourceTree = "<group>"; };
|
||||
54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBackdropView.swift; sourceTree = "<group>"; };
|
||||
54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelLayout.swift; sourceTree = "<group>"; };
|
||||
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanel.swift; sourceTree = "<group>"; };
|
||||
54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FloatingPanelTesting.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
54E740CC218AFD67005C1A34 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
54E740D8218AFD6A005C1A34 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -69,6 +90,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
54E740C7218AFD67005C1A34 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
@@ -77,6 +105,7 @@
|
||||
children = (
|
||||
545DB9C32151169500CA77B8 /* Sources */,
|
||||
545DB9CE2151169500CA77B8 /* Tests */,
|
||||
54E740CB218AFD67005C1A34 /* TestingApp */,
|
||||
545DB9C22151169500CA77B8 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@@ -86,6 +115,7 @@
|
||||
children = (
|
||||
545DB9C12151169500CA77B8 /* FloatingPanel.framework */,
|
||||
545DB9CA2151169500CA77B8 /* FloatingPanelTests.xctest */,
|
||||
54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -113,12 +143,26 @@
|
||||
545DB9CE2151169500CA77B8 /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
545DB9CF2151169500CA77B8 /* FloatingPanelTests.swift */,
|
||||
54A6B6B022968B530077F348 /* FloatingPanelTests.swift */,
|
||||
542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */,
|
||||
54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */,
|
||||
545DB9CF2151169500CA77B8 /* FloatingPanelControllerTests.swift */,
|
||||
542753C722C49A8F00D17955 /* Utils.swift */,
|
||||
545DB9D12151169500CA77B8 /* Info.plist */,
|
||||
);
|
||||
path = Tests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54E740CB218AFD67005C1A34 /* TestingApp */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54E740CC218AFD67005C1A34 /* AppDelegate.swift */,
|
||||
54E740D8218AFD6A005C1A34 /* Info.plist */,
|
||||
54A6B6B522968F710077F348 /* LaunchScreen.storyboard */,
|
||||
);
|
||||
path = TestingApp;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
@@ -163,19 +207,37 @@
|
||||
);
|
||||
dependencies = (
|
||||
545DB9CD2151169500CA77B8 /* PBXTargetDependency */,
|
||||
54E740DD218AFE9F005C1A34 /* PBXTargetDependency */,
|
||||
);
|
||||
name = FloatingPanelTests;
|
||||
productName = FloatingModalControllerTests;
|
||||
productReference = 545DB9CA2151169500CA77B8 /* FloatingPanelTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
54E740C9218AFD67005C1A34 /* TestingApp */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 54E740D9218AFD6A005C1A34 /* Build configuration list for PBXNativeTarget "TestingApp" */;
|
||||
buildPhases = (
|
||||
54E740C6218AFD67005C1A34 /* Sources */,
|
||||
54E740C7218AFD67005C1A34 /* Frameworks */,
|
||||
54E740C8218AFD67005C1A34 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = TestingApp;
|
||||
productName = TestingHost;
|
||||
productReference = 54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
545DB9B82151169500CA77B8 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1000;
|
||||
LastSwiftUpdateCheck = 1010;
|
||||
LastUpgradeCheck = 1000;
|
||||
ORGANIZATIONNAME = scenee;
|
||||
TargetAttributes = {
|
||||
@@ -185,6 +247,10 @@
|
||||
};
|
||||
545DB9C92151169500CA77B8 = {
|
||||
CreatedOnToolsVersion = 10.0;
|
||||
TestTargetID = 54E740C9218AFD67005C1A34;
|
||||
};
|
||||
54E740C9218AFD67005C1A34 = {
|
||||
CreatedOnToolsVersion = 10.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -194,6 +260,7 @@
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 545DB9B72151169500CA77B8;
|
||||
productRefGroup = 545DB9C22151169500CA77B8 /* Products */;
|
||||
@@ -202,6 +269,7 @@
|
||||
targets = (
|
||||
545DB9C02151169500CA77B8 /* FloatingPanel */,
|
||||
545DB9C92151169500CA77B8 /* FloatingPanelTests */,
|
||||
54E740C9218AFD67005C1A34 /* TestingApp */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -221,6 +289,14 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
54E740C8218AFD67005C1A34 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
54A6B6B622968F710077F348 /* LaunchScreen.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -239,6 +315,7 @@
|
||||
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */,
|
||||
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */,
|
||||
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */,
|
||||
542753C822C49A8F00D17955 /* Utils.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -246,7 +323,18 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
545DB9D02151169500CA77B8 /* FloatingPanelTests.swift in Sources */,
|
||||
54A6B6B122968B530077F348 /* FloatingPanelTests.swift in Sources */,
|
||||
545DB9D02151169500CA77B8 /* FloatingPanelControllerTests.swift in Sources */,
|
||||
542753C622C49A6E00D17955 /* FloatingPanelLayoutTests.swift in Sources */,
|
||||
54A6B6B82296A8520077F348 /* FloatingPanelSurfaceViewTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
54E740C6218AFD67005C1A34 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
54E740CD218AFD67005C1A34 /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -258,6 +346,11 @@
|
||||
target = 545DB9C02151169500CA77B8 /* FloatingPanel */;
|
||||
targetProxy = 545DB9CC2151169500CA77B8 /* PBXContainerItemProxy */;
|
||||
};
|
||||
54E740DD218AFE9F005C1A34 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 54E740C9218AFD67005C1A34 /* TestingApp */;
|
||||
targetProxy = 54E740DC218AFE9F005C1A34 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
@@ -386,6 +479,7 @@
|
||||
545DB9D62151169500CA77B8 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -405,6 +499,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@@ -414,6 +509,7 @@
|
||||
545DB9D72151169500CA77B8 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -432,6 +528,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_VERSION = 4.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@@ -442,7 +539,9 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -450,8 +549,9 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalControllerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FloatingPanelTesting.app/FloatingPanelTesting";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -460,7 +560,9 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -468,7 +570,44 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalControllerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FloatingPanelTesting.app/FloatingPanelTesting";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
54E740DA218AFD6A005C1A34 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = TestingApp/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTesting;
|
||||
PRODUCT_NAME = FloatingPanelTesting;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
54E740DB218AFD6A005C1A34 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = TestingApp/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTesting;
|
||||
PRODUCT_NAME = FloatingPanelTesting;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
@@ -540,6 +679,7 @@
|
||||
54E79AE0224F6C9800717BC6 /* Test */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -559,6 +699,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG __FP_LOG";
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@@ -570,7 +711,9 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -578,7 +721,22 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalControllerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FloatingPanelTesting.app/FloatingPanelTesting";
|
||||
};
|
||||
name = Test;
|
||||
};
|
||||
54E8AC6A2286CFB6000C5A12 /* Test */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = TestingApp/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTesting;
|
||||
PRODUCT_NAME = FloatingPanelTesting;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Test;
|
||||
@@ -616,6 +774,16 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
54E740D9218AFD6A005C1A34 /* Build configuration list for PBXNativeTarget "TestingApp" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
54E740DA218AFD6A005C1A34 /* Debug */,
|
||||
54E740DB218AFD6A005C1A34 /* Release */,
|
||||
54E8AC6A2286CFB6000C5A12 /* Test */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 545DB9B82151169500CA77B8 /* Project object */;
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
skipped = "NO"
|
||||
parallelizable = "YES"
|
||||
testExecutionOrdering = "random">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "545DB9C92151169500CA77B8"
|
||||
|
||||
@@ -8,9 +8,9 @@ import UIKit.UIGestureRecognizerSubclass // For Xcode 9.4.1
|
||||
///
|
||||
/// FloatingPanel presentation model
|
||||
///
|
||||
class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate {
|
||||
class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
// MUST be a weak reference to prevent UI freeze on the presentation modally
|
||||
weak var viewcontroller: FloatingPanelController!
|
||||
weak var viewcontroller: FloatingPanelController?
|
||||
|
||||
let surfaceView: FloatingPanelSurfaceView
|
||||
let backdropView: FloatingPanelBackdropView
|
||||
@@ -21,13 +21,15 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
didSet {
|
||||
guard let scrollView = scrollView else { return }
|
||||
scrollView.panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
|
||||
scrollBouncable = scrollView.bounces
|
||||
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var state: FloatingPanelPosition = .hidden {
|
||||
didSet { viewcontroller.delegate?.floatingPanelDidChangePosition(viewcontroller) }
|
||||
didSet {
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidChangePosition(vc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isBottomState: Bool {
|
||||
@@ -38,15 +40,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
let panGestureRecognizer: FloatingPanelPanGestureRecognizer
|
||||
var isRemovalInteractionEnabled: Bool = false
|
||||
|
||||
fileprivate var animator: UIViewPropertyAnimator? {
|
||||
didSet {
|
||||
// This intends to avoid `tableView(_:didSelectRowAt:)` not being
|
||||
// called on first tap after the moving animation, but it doesn't
|
||||
// seem to be enough. The same issue happens on Apple Maps so it
|
||||
// might be an issue in `UITableView`.
|
||||
scrollView?.isUserInteractionEnabled = (animator == nil)
|
||||
}
|
||||
}
|
||||
fileprivate var animator: UIViewPropertyAnimator?
|
||||
|
||||
private var initialFrame: CGRect = .zero
|
||||
private var initialTranslationY: CGFloat = 0
|
||||
@@ -99,6 +93,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
|
||||
private func move(from: FloatingPanelPosition, to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
guard let vc = viewcontroller else {
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
if state != layoutAdapter.topMostState {
|
||||
lockScrollView()
|
||||
}
|
||||
@@ -108,11 +106,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
let animator: UIViewPropertyAnimator
|
||||
switch (from, to) {
|
||||
case (.hidden, let to):
|
||||
animator = behavior.addAnimator(self.viewcontroller, to: to)
|
||||
animator = behavior.addAnimator(vc, to: to)
|
||||
case (let from, .hidden):
|
||||
animator = behavior.removeAnimator(self.viewcontroller, from: from)
|
||||
animator = behavior.removeAnimator(vc, from: from)
|
||||
case (let from, let to):
|
||||
animator = behavior.moveAnimator(self.viewcontroller, from: from, to: to)
|
||||
animator = behavior.moveAnimator(vc, from: from, to: to)
|
||||
}
|
||||
|
||||
animator.addAnimations { [weak self] in
|
||||
@@ -124,6 +122,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
animator.addCompletion { [weak self] _ in
|
||||
guard let `self` = self else { return }
|
||||
self.animator = nil
|
||||
if self.state == self.layoutAdapter.topMostState {
|
||||
self.unlockScrollView()
|
||||
} else {
|
||||
self.lockScrollView()
|
||||
}
|
||||
completion?()
|
||||
}
|
||||
self.animator = animator
|
||||
@@ -131,6 +134,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
} else {
|
||||
self.state = to
|
||||
self.updateLayout(to: to)
|
||||
if self.state == self.layoutAdapter.topMostState {
|
||||
self.unlockScrollView()
|
||||
} else {
|
||||
self.lockScrollView()
|
||||
}
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
@@ -167,7 +175,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
|
||||
/* log.debug("shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */
|
||||
|
||||
if viewcontroller.delegate?.floatingPanel(viewcontroller, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
|
||||
if let vc = viewcontroller,
|
||||
vc.delegate?.floatingPanel(vc, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -181,8 +190,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
// and handle them in self.handle(panGesture:)
|
||||
return scrollView?.gestureRecognizers?.contains(otherGestureRecognizer) ?? false
|
||||
default:
|
||||
// Should always recognize tap/long press gestures in parallel
|
||||
return true
|
||||
// Should recognize tap/long press gestures in parallel when the surface view is at an anchor position.
|
||||
let surfaceFrame = surfaceView.layer.presentation()?.frame ?? surfaceView.frame
|
||||
return surfaceFrame.minY == layoutAdapter.positionY(for: state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,11 +223,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
}
|
||||
|
||||
if viewcontroller.delegate?.floatingPanel(viewcontroller, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
|
||||
if let vc = viewcontroller,
|
||||
vc.delegate?.floatingPanel(vc, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
switch otherGestureRecognizer {
|
||||
case is UIPanGestureRecognizer,
|
||||
is UISwipeGestureRecognizer,
|
||||
@@ -236,7 +246,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
let grabberAreaFrame = CGRect(x: surfaceView.bounds.origin.x,
|
||||
y: surfaceView.bounds.origin.y,
|
||||
width: surfaceView.bounds.width,
|
||||
height: FloatingPanelSurfaceView.topGrabberBarHeight * 2)
|
||||
height: surfaceView.topGrabberBarHeight * 2)
|
||||
return grabberAreaFrame
|
||||
}
|
||||
|
||||
@@ -268,13 +278,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
if grabberAreaFrame.contains(location) {
|
||||
// Preserve the current content offset in moving from full.
|
||||
scrollView.setContentOffset(initialScrollOffset, animated: false)
|
||||
} else {
|
||||
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
|
||||
if offset < 0 {
|
||||
fitToBounds(scrollView: scrollView)
|
||||
let translation = panGesture.translation(in: panGestureRecognizer.view!.superview)
|
||||
startInteraction(with: translation, at: location)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -286,15 +289,17 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
lockScrollView()
|
||||
}
|
||||
} else {
|
||||
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
|
||||
// Always show a scroll indicator at the top.
|
||||
if interactionInProgress {
|
||||
unlockScrollView()
|
||||
if offset > 0 {
|
||||
unlockScrollView()
|
||||
}
|
||||
} else {
|
||||
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
|
||||
if state == layoutAdapter.topMostState, offset < 0, velocity.y > 0 {
|
||||
fitToBounds(scrollView: scrollView)
|
||||
let translation = panGesture.translation(in: panGestureRecognizer.view!.superview)
|
||||
startInteraction(with: translation, at: location)
|
||||
// Hide a scroll indicator just before starting an interaction by swiping a panel down.
|
||||
if state == layoutAdapter.topMostState,
|
||||
offset < 0, velocity.y > 0 {
|
||||
lockScrollView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,16 +312,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
|
||||
if let animator = self.animator {
|
||||
log.debug("panel animation interrupted!!!")
|
||||
// Prevent aborting touch events when the current animator is
|
||||
// released almost at a target position. Because any tap gestures
|
||||
// shouldn't be disturbed at the position.
|
||||
if fabs(surfaceView.frame.minY - layoutAdapter.topY) > 40.0 {
|
||||
if animator.isInterruptible {
|
||||
animator.stopAnimation(false)
|
||||
animator.finishAnimation(at: .current)
|
||||
}
|
||||
self.animator = nil
|
||||
if animator.isInterruptible {
|
||||
animator.stopAnimation(false)
|
||||
animator.finishAnimation(at: .current)
|
||||
}
|
||||
self.animator = nil
|
||||
|
||||
// A user can stop a panel at the nearest Y of a target position
|
||||
if abs(surfaceView.frame.minY - layoutAdapter.topY) < 1.0 {
|
||||
@@ -325,7 +325,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
|
||||
if interactionInProgress == false,
|
||||
viewcontroller.delegate?.floatingPanelShouldBeginDragging(viewcontroller) == false {
|
||||
let vc = viewcontroller,
|
||||
vc.delegate?.floatingPanelShouldBeginDragging(vc) == false {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -429,7 +430,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
let dy = translation.y - initialTranslationY
|
||||
|
||||
layoutAdapter.updateInteractiveTopConstraint(diff: dy,
|
||||
allowsTopBuffer: allowsTopBuffer(for: dy))
|
||||
allowsTopBuffer: allowsTopBuffer(for: dy),
|
||||
with: behavior)
|
||||
|
||||
backdropView.alpha = getBackdropAlpha(with: translation)
|
||||
preserveContentVCLayoutIfNeeded()
|
||||
@@ -437,7 +439,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
let didMove = (pre != surfaceView.frame.minY)
|
||||
guard didMove else { return }
|
||||
|
||||
viewcontroller.delegate?.floatingPanelDidMove(viewcontroller)
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidMove(vc)
|
||||
}
|
||||
}
|
||||
|
||||
private func allowsTopBuffer(for translationY: CGFloat) -> Bool {
|
||||
@@ -455,11 +459,12 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
// Prevent stretching a view having a constraint to SafeArea.bottom in an overflow
|
||||
// from the full position because SafeArea is global in a screen.
|
||||
private func preserveContentVCLayoutIfNeeded() {
|
||||
guard let vc = viewcontroller else { return }
|
||||
// Must include topY
|
||||
if (surfaceView.frame.minY <= layoutAdapter.topY) {
|
||||
if !disabledBottomAutoLayout {
|
||||
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
|
||||
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
|
||||
vc.contentViewController?.view?.constraints.forEach({ (const) in
|
||||
switch vc.contentViewController?.layoutGuide.bottomAnchor {
|
||||
case const.firstAnchor:
|
||||
(const.secondItem as? UIView)?.disableAutoLayout()
|
||||
const.isActive = false
|
||||
@@ -474,8 +479,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
disabledBottomAutoLayout = true
|
||||
} else {
|
||||
if disabledBottomAutoLayout {
|
||||
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
|
||||
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
|
||||
vc.contentViewController?.view?.constraints.forEach({ (const) in
|
||||
switch vc.contentViewController?.layoutGuide.bottomAnchor {
|
||||
case const.firstAnchor:
|
||||
(const.secondItem as? UIView)?.enableAutoLayout()
|
||||
const.isActive = true
|
||||
@@ -516,21 +521,18 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
let velocityVector = (distance != 0) ? CGVector(dx: 0,
|
||||
dy: min(abs(velocity.y)/distance, behavior.removalVelocity)) : .zero
|
||||
|
||||
if shouldStartRemovalAnimation(with: velocityVector) {
|
||||
|
||||
viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity)
|
||||
self.startRemovalAnimation(with: velocityVector) { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
self.viewcontroller.dismiss(animated: false, completion: { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
self.viewcontroller.delegate?.floatingPanelDidEndRemove(self.viewcontroller)
|
||||
})
|
||||
if shouldStartRemovalAnimation(with: velocityVector), let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDraggingToRemove(vc, withVelocity: velocity)
|
||||
startRemovalAnimation(vc, with: velocityVector) { [weak self] in
|
||||
self?.finishRemovalAnimation()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
viewcontroller.delegate?.floatingPanelDidEndDragging(viewcontroller, withVelocity: velocity, targetPosition: targetPosition)
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDragging(vc, withVelocity: velocity, targetPosition: targetPosition)
|
||||
}
|
||||
|
||||
// Workaround: Disable a tracking scroll to prevent bouncing a scroll content in a panel animating
|
||||
let isScrollEnabled = scrollView?.isScrollEnabled
|
||||
@@ -563,8 +565,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
return true
|
||||
}
|
||||
|
||||
private func startRemovalAnimation(with velocityVector: CGVector, completion: (() -> Void)?) {
|
||||
let animator = self.behavior.removalInteractionAnimator(self.viewcontroller, with: velocityVector)
|
||||
private func startRemovalAnimation(_ vc: FloatingPanelController, with velocityVector: CGVector, completion: (() -> Void)?) {
|
||||
let animator = behavior.removalInteractionAnimator(vc, with: velocityVector)
|
||||
|
||||
animator.addAnimations { [weak self] in
|
||||
self?.updateLayout(to: .hidden)
|
||||
@@ -577,6 +579,13 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
animator.startAnimation()
|
||||
}
|
||||
|
||||
private func finishRemovalAnimation() {
|
||||
viewcontroller?.dismiss(animated: false) { [weak self] in
|
||||
guard let vc = self?.viewcontroller else { return }
|
||||
vc.delegate?.floatingPanelDidEndRemove(vc)
|
||||
}
|
||||
}
|
||||
|
||||
private func startInteraction(with translation: CGPoint, at location: CGPoint) {
|
||||
/* Don't lock a scroll view to show a scroll indicator after hitting the top */
|
||||
log.debug("startInteraction -- translation = \(translation.y), location = \(location.y)")
|
||||
@@ -587,6 +596,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
if grabberAreaFrame.contains(location) {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
} else {
|
||||
fitToBounds(scrollView: scrollView)
|
||||
settle(scrollView: scrollView)
|
||||
initialScrollOffset = scrollView.contentOffsetZero
|
||||
}
|
||||
@@ -595,7 +605,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
|
||||
initialTranslationY = translation.y
|
||||
|
||||
viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller)
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelWillBeginDragging(vc)
|
||||
}
|
||||
|
||||
layoutAdapter.startInteraction(at: state)
|
||||
|
||||
@@ -627,12 +639,14 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
|
||||
private func startAnimation(to targetPosition: FloatingPanelPosition, at distance: CGFloat, with velocity: CGPoint) {
|
||||
log.debug("startAnimation to \(targetPosition) -- distance = \(distance), velocity = \(velocity.y)")
|
||||
guard let vc = viewcontroller else { return }
|
||||
|
||||
isDecelerating = true
|
||||
viewcontroller.delegate?.floatingPanelWillBeginDecelerating(viewcontroller)
|
||||
|
||||
vc.delegate?.floatingPanelWillBeginDecelerating(vc)
|
||||
|
||||
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: min(abs(velocity.y)/distance, 30.0)) : .zero
|
||||
let animator = behavior.interactionAnimator(self.viewcontroller, to: targetPosition, with: velocityVector)
|
||||
let animator = behavior.interactionAnimator(vc, to: targetPosition, with: velocityVector)
|
||||
animator.addAnimations { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
self.state = targetPosition
|
||||
@@ -652,7 +666,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
self.isDecelerating = false
|
||||
self.animator = nil
|
||||
|
||||
self.viewcontroller.delegate?.floatingPanelDidEndDecelerating(self.viewcontroller)
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDecelerating(vc)
|
||||
}
|
||||
|
||||
if let scrollView = scrollView {
|
||||
log.debug("finishAnimation -- scroll offset = \(scrollView.contentOffset)")
|
||||
@@ -666,21 +682,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
|
||||
private func distance(to targetPosition: FloatingPanelPosition) -> CGFloat {
|
||||
let topY = layoutAdapter.topY
|
||||
let middleY = layoutAdapter.middleY
|
||||
let bottomY = layoutAdapter.bottomY
|
||||
let currentY = surfaceView.frame.minY
|
||||
|
||||
switch targetPosition {
|
||||
case .full:
|
||||
return CGFloat(abs(currentY - topY))
|
||||
case .half:
|
||||
return CGFloat(abs(currentY - middleY))
|
||||
case .tip:
|
||||
return CGFloat(abs(currentY - bottomY))
|
||||
case .hidden:
|
||||
fatalError("Now .hidden must not be used for a user interaction")
|
||||
}
|
||||
let targetY = layoutAdapter.positionY(for: targetPosition)
|
||||
return CGFloat(abs(currentY - targetY))
|
||||
}
|
||||
|
||||
private func directionalPosition(at currentY: CGFloat, with translation: CGPoint) -> FloatingPanelPosition {
|
||||
@@ -721,6 +725,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
|
||||
private func targetPosition(with velocity: CGPoint) -> (FloatingPanelPosition) {
|
||||
guard let vc = viewcontroller else { return state }
|
||||
let currentY = surfaceView.frame.minY
|
||||
let supportedPositions = layoutAdapter.supportedPositions
|
||||
|
||||
@@ -763,7 +768,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
fatalError("Now .hidden must not be used for a user interaction")
|
||||
}
|
||||
|
||||
let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: nextState), 1.0), 0.0)
|
||||
let redirectionalProgress = max(min(behavior.redirectionalProgress(vc, from: state, to: nextState), 1.0), 0.0)
|
||||
|
||||
let th1: CGFloat
|
||||
let th2: CGFloat
|
||||
@@ -776,7 +781,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
th2 = bottomY - (bottomY - middleY) * redirectionalProgress
|
||||
}
|
||||
|
||||
let decelerationRate = behavior.momentumProjectionRate(viewcontroller)
|
||||
let decelerationRate = behavior.momentumProjectionRate(vc)
|
||||
|
||||
let baseY = abs(bottomY - topY)
|
||||
let vecY = velocity.y / baseY
|
||||
@@ -786,7 +791,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
case ..<th1:
|
||||
switch pY {
|
||||
case bottomY...:
|
||||
return behavior.shouldProjectMomentum(viewcontroller, for: .tip) ? .tip : .half
|
||||
return behavior.shouldProjectMomentum(vc, for: .tip) ? .tip : .half
|
||||
case middleY...:
|
||||
return .half
|
||||
case topY...:
|
||||
@@ -797,7 +802,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
case ...middleY:
|
||||
switch pY {
|
||||
case bottomY...:
|
||||
return behavior.shouldProjectMomentum(viewcontroller, for: .tip) ? .tip : .half
|
||||
return behavior.shouldProjectMomentum(vc, for: .tip) ? .tip : .half
|
||||
case middleY...:
|
||||
return .half
|
||||
case topY...:
|
||||
@@ -814,7 +819,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
case topY...:
|
||||
return .half
|
||||
default:
|
||||
return behavior.shouldProjectMomentum(viewcontroller, for: .full) ? .full : .half
|
||||
return behavior.shouldProjectMomentum(vc, for: .full) ? .full : .half
|
||||
}
|
||||
default:
|
||||
switch pY {
|
||||
@@ -825,13 +830,14 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
case topY...:
|
||||
return .half
|
||||
default:
|
||||
return behavior.shouldProjectMomentum(viewcontroller, for: .full) ? .full : .half
|
||||
return behavior.shouldProjectMomentum(vc, for: .full) ? .full : .half
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func targetPosition(from positions: [FloatingPanelPosition], at currentY: CGFloat, velocity: CGPoint) -> FloatingPanelPosition {
|
||||
guard let vc = viewcontroller else { return state }
|
||||
assert(positions.count == 2)
|
||||
|
||||
let top = positions[0]
|
||||
@@ -841,11 +847,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
let bottomY = layoutAdapter.positionY(for: bottom)
|
||||
|
||||
let target = top == state ? bottom : top
|
||||
let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: target), 1.0), 0.0)
|
||||
let redirectionalProgress = max(min(behavior.redirectionalProgress(vc, from: state, to: target), 1.0), 0.0)
|
||||
|
||||
let th = topY + (bottomY - topY) * redirectionalProgress
|
||||
|
||||
let decelerationRate = behavior.momentumProjectionRate(viewcontroller)
|
||||
let decelerationRate = behavior.momentumProjectionRate(vc)
|
||||
let pY = project(initialVelocity: velocity.y, decelerationRate: decelerationRate) + currentY
|
||||
|
||||
switch currentY {
|
||||
@@ -869,13 +875,21 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
private func lockScrollView() {
|
||||
guard let scrollView = scrollView else { return }
|
||||
|
||||
if scrollView.isLocked {
|
||||
log.debug("Already scroll locked.")
|
||||
return
|
||||
}
|
||||
|
||||
scrollBouncable = scrollView.bounces
|
||||
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
|
||||
|
||||
scrollView.isDirectionalLockEnabled = true
|
||||
scrollView.bounces = false
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
}
|
||||
|
||||
private func unlockScrollView() {
|
||||
guard let scrollView = scrollView else { return }
|
||||
guard let scrollView = scrollView, scrollView.isLocked else { return }
|
||||
|
||||
scrollView.isDirectionalLockEnabled = false
|
||||
scrollView.bounces = scrollBouncable
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import UIKit
|
||||
|
||||
public protocol FloatingPanelBehavior {
|
||||
/// Asks the behavior object if the floating panel should project a momentum of a user interaction to move the proposed position.
|
||||
/// Asks the behavior if the floating panel should project a momentum of a user interaction to move the proposed position.
|
||||
///
|
||||
/// The default implementation of this method returns true. This method is called for a layout to support all positions(tip, half and full).
|
||||
/// Therefore, `proposedTargetPosition` can only be `FloatingPanelPosition.tip` or `FloatingPanelPosition.full`.
|
||||
@@ -57,6 +57,12 @@ public protocol FloatingPanelBehavior {
|
||||
///
|
||||
/// Default is a spring animator with 1.0 damping ratio. This method is called when FloatingPanelController.isRemovalInteractionEnabled is true.
|
||||
func removalInteractionAnimator(_ fpc: FloatingPanelController, with velocity: CGVector) -> UIViewPropertyAnimator
|
||||
|
||||
|
||||
/// Asks the behavior whether the rubber band effect is enabled in moving over a given edge of the surface view.
|
||||
///
|
||||
/// This method allows the behavior to activate the rubber band effect to a given edge of the surface view. By default, the effect is disabled.
|
||||
func allowsRubberBanding(for edge: UIRectEdge) -> Bool
|
||||
}
|
||||
|
||||
public extension FloatingPanelBehavior {
|
||||
@@ -114,6 +120,10 @@ public extension FloatingPanelBehavior {
|
||||
initialVelocity: velocity)
|
||||
return UIViewPropertyAnimator(duration: 0, timingParameters: timing)
|
||||
}
|
||||
|
||||
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private let defaultBehavior = FloatingPanelDefaultBehavior()
|
||||
|
||||
@@ -68,6 +68,10 @@ public enum FloatingPanelPosition: Int {
|
||||
case half
|
||||
case tip
|
||||
case hidden
|
||||
|
||||
static var allCases: [FloatingPanelPosition] {
|
||||
return [.full, .half, .tip, .hidden]
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
@@ -145,7 +149,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
}
|
||||
private var _contentViewController: UIViewController?
|
||||
|
||||
private var floatingPanel: FloatingPanel!
|
||||
private(set) var floatingPanel: FloatingPanel!
|
||||
private var preSafeAreaInsets: UIEdgeInsets = .zero // Capture the latest one
|
||||
private var safeAreaInsetsObservation: NSKeyValueObservation?
|
||||
private let modalTransition = FloatingPanelModalTransition()
|
||||
@@ -196,7 +200,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
self.view = view as UIView
|
||||
}
|
||||
|
||||
open override func viewDidLayoutSubviews() {
|
||||
open override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
if #available(iOS 11.0, *) {}
|
||||
else {
|
||||
@@ -207,7 +211,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
}
|
||||
}
|
||||
|
||||
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
|
||||
if view.translatesAutoresizingMaskIntoConstraints {
|
||||
@@ -216,14 +220,9 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
}
|
||||
}
|
||||
|
||||
open override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
open override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.willTransition(to: newCollection, with: coordinator)
|
||||
|
||||
// Change layout for a new trait collection
|
||||
reloadLayout(for: newCollection)
|
||||
setUpLayout()
|
||||
|
||||
floatingPanel.behavior = fetchBehavior(for: newCollection)
|
||||
self.prepare(for: newCollection)
|
||||
}
|
||||
|
||||
open override func viewWillDisappear(_ animated: Bool) {
|
||||
@@ -231,6 +230,15 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
safeAreaInsetsObservation = nil
|
||||
}
|
||||
|
||||
// MARK:- Internals
|
||||
func prepare(for newCollection: UITraitCollection) {
|
||||
guard newCollection.shouldUpdateLayout(from: traitCollection) else { return }
|
||||
// Change a layout & behavior for a new trait collection
|
||||
reloadLayout(for: newCollection)
|
||||
activateLayout()
|
||||
floatingPanel.behavior = fetchBehavior(for: newCollection)
|
||||
}
|
||||
|
||||
// MARK:- Privates
|
||||
|
||||
private func fetchLayout(for traitCollection: UITraitCollection) -> FloatingPanelLayout {
|
||||
@@ -257,7 +265,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
// Prevent an infinite loop on iOS 10: setUpLayout() -> viewDidLayoutSubviews() -> setUpLayout()
|
||||
preSafeAreaInsets = safeAreaInsets
|
||||
|
||||
setUpLayout()
|
||||
activateLayout()
|
||||
|
||||
switch contentInsetAdjustmentBehavior {
|
||||
case .always:
|
||||
@@ -282,7 +290,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
}
|
||||
}
|
||||
|
||||
private func setUpLayout() {
|
||||
private func activateLayout() {
|
||||
// preserve the current content offset
|
||||
let contentOffset = scrollView?.contentOffset
|
||||
|
||||
@@ -298,7 +306,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
public func show(animated: Bool = false, completion: (() -> Void)? = nil) {
|
||||
// Must apply the current layout here
|
||||
reloadLayout(for: traitCollection)
|
||||
setUpLayout()
|
||||
activateLayout()
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
// Must track the safeAreaInsets of `self.view` to update the layout.
|
||||
@@ -513,7 +521,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
/// animation block.
|
||||
public func updateLayout() {
|
||||
reloadLayout(for: traitCollection)
|
||||
setUpLayout()
|
||||
activateLayout()
|
||||
}
|
||||
|
||||
/// Returns the y-coordinate of the point at the origin of the surface view.
|
||||
|
||||
@@ -181,13 +181,11 @@ class FloatingPanelLayoutAdapter {
|
||||
}
|
||||
|
||||
var topMostState: FloatingPanelPosition {
|
||||
if supportedPositions.contains(.full) {
|
||||
return .full
|
||||
}
|
||||
if supportedPositions.contains(.half) {
|
||||
return .half
|
||||
}
|
||||
return .tip
|
||||
return supportedPositions.sorted(by: { $0.rawValue < $1.rawValue }).first ?? .hidden
|
||||
}
|
||||
|
||||
var bottomMostState: FloatingPanelPosition {
|
||||
return supportedPositions.sorted(by: { $0.rawValue < $1.rawValue }).last ?? .hidden
|
||||
}
|
||||
|
||||
var topY: CGFloat {
|
||||
@@ -412,12 +410,12 @@ class FloatingPanelLayoutAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
func updateInteractiveTopConstraint(diff: CGFloat, allowsTopBuffer: Bool) {
|
||||
func updateInteractiveTopConstraint(diff: CGFloat, allowsTopBuffer: Bool, with behavior: FloatingPanelBehavior) {
|
||||
defer {
|
||||
surfaceView.superview!.layoutIfNeeded() // MUST call here to update `surfaceView.frame`
|
||||
}
|
||||
|
||||
let minY: CGFloat = {
|
||||
let topMostConst: CGFloat = {
|
||||
var ret: CGFloat = 0.0
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout, is FloatingPanelFullScreenLayout:
|
||||
@@ -425,12 +423,9 @@ class FloatingPanelLayoutAdapter {
|
||||
default:
|
||||
ret = topY - safeAreaInsets.top
|
||||
}
|
||||
if allowsTopBuffer {
|
||||
ret -= layout.topInteractionBuffer
|
||||
}
|
||||
return max(ret, 0.0) // The top boundary is equal to the related topAnchor.
|
||||
}()
|
||||
let maxY: CGFloat = {
|
||||
let bottomMostConst: CGFloat = {
|
||||
var ret: CGFloat = 0.0
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout, is FloatingPanelFullScreenLayout:
|
||||
@@ -438,12 +433,34 @@ class FloatingPanelLayoutAdapter {
|
||||
default:
|
||||
ret = bottomY - safeAreaInsets.top
|
||||
}
|
||||
ret += layout.bottomInteractionBuffer
|
||||
return min(ret, bottomMaxY)
|
||||
}()
|
||||
let const = initialConst + diff
|
||||
let minConst = allowsTopBuffer ? topMostConst - layout.topInteractionBuffer : topMostConst
|
||||
let maxConst = bottomMostConst + layout.bottomInteractionBuffer
|
||||
|
||||
interactiveTopConstraint?.constant = max(minY, min(maxY, const))
|
||||
var const = initialConst + diff
|
||||
|
||||
// Rubberbanding top buffer
|
||||
if behavior.allowsRubberBanding(for: .top), const < topMostConst {
|
||||
let buffer = topMostConst - const
|
||||
const = topMostConst - rubberbandEffect(for: buffer, base: vc.view.bounds.height)
|
||||
}
|
||||
|
||||
// Rubberbanding bottom buffer
|
||||
if behavior.allowsRubberBanding(for: .bottom), const > bottomMostConst {
|
||||
let buffer = const - bottomMostConst
|
||||
const = bottomMostConst + rubberbandEffect(for: buffer, base: vc.view.bounds.height)
|
||||
}
|
||||
|
||||
interactiveTopConstraint?.constant = max(minConst, min(maxConst, const))
|
||||
}
|
||||
|
||||
// According to @chpwn's tweet: https://twitter.com/chpwn/status/285540192096497664
|
||||
// x = distance from the edge
|
||||
// c = constant value, UIScrollView uses 0.55
|
||||
// d = dimension, either width or height
|
||||
private func rubberbandEffect(for buffer: CGFloat, base: CGFloat) -> CGFloat {
|
||||
return (1.0 - (1.0 / ((buffer * 0.55 / base) + 1.0))) * base
|
||||
}
|
||||
|
||||
func activateLayout(of state: FloatingPanelPosition) {
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
class FloatingPanelSurfaceContentView: UIView {}
|
||||
|
||||
/// A view that presents a surface interface in a floating panel.
|
||||
public class FloatingPanelSurfaceView: UIView {
|
||||
|
||||
@@ -14,15 +12,38 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
///
|
||||
/// To use a custom grabber handle, hide this and then add the custom one
|
||||
/// to the surface view at appropriate coordinates.
|
||||
public var grabberHandle: GrabberHandleView!
|
||||
public let grabberHandle: GrabberHandleView = GrabberHandleView()
|
||||
|
||||
/// Offset of the grabber handle from the top
|
||||
public var grabberTopPadding: CGFloat = 6.0 { didSet {
|
||||
setNeedsUpdateConstraints()
|
||||
} }
|
||||
|
||||
/// The height of the grabber bar area
|
||||
public static var topGrabberBarHeight: CGFloat {
|
||||
return Default.grabberTopPadding * 2 + GrabberHandleView.Default.height // 17.0
|
||||
public var topGrabberBarHeight: CGFloat {
|
||||
return grabberTopPadding * 2 + grabberHandleHeight
|
||||
}
|
||||
|
||||
/// Grabber view width and height
|
||||
public var grabberHandleWidth: CGFloat = 36.0 { didSet {
|
||||
setNeedsUpdateConstraints()
|
||||
} }
|
||||
public var grabberHandleHeight: CGFloat = 5.0 { didSet {
|
||||
setNeedsUpdateConstraints()
|
||||
} }
|
||||
|
||||
/// A root view of a content view controller
|
||||
public weak var contentView: UIView!
|
||||
|
||||
/// The content insets specifying the insets around the content view.
|
||||
///
|
||||
/// - important: Currently the `bottom` inset is ignored.
|
||||
public var contentInsets: UIEdgeInsets = .zero {
|
||||
didSet {
|
||||
// Needs update constraints
|
||||
self.setNeedsUpdateConstraints()
|
||||
}
|
||||
}
|
||||
|
||||
private var color: UIColor? = .white { didSet { setNeedsLayout() } }
|
||||
var bottomOverflow: CGFloat = 0.0 // Must not call setNeedsLayout()
|
||||
@@ -36,7 +57,10 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
///
|
||||
/// `self.contentView` is masked with the top rounded corners automatically on iOS 11 and later.
|
||||
/// On iOS 10, they are not automatically masked because of a UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854
|
||||
public var cornerRadius: CGFloat = 0.0 { didSet { setNeedsLayout() } }
|
||||
public var cornerRadius: CGFloat {
|
||||
set { containerView.layer.cornerRadius = newValue; setNeedsLayout() }
|
||||
get { return containerView.layer.cornerRadius }
|
||||
}
|
||||
|
||||
/// A Boolean indicating whether the surface shadow is displayed.
|
||||
public var shadowHidden: Bool = false { didSet { setNeedsLayout() } }
|
||||
@@ -59,6 +83,10 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
/// The color of the surface border.
|
||||
public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } }
|
||||
|
||||
/// Offset of the container view from the top
|
||||
public var containerTopInset: CGFloat = 0.0 { didSet {
|
||||
setNeedsUpdateConstraints()
|
||||
} }
|
||||
|
||||
/// The view presents an actual surface shape.
|
||||
///
|
||||
@@ -66,105 +94,116 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
/// specified by other properties. The reason why they're not be applied to
|
||||
/// a content view directly is because it avoids any side-effects to the
|
||||
/// content view.
|
||||
public var containerView: UIView!
|
||||
public let containerView: UIView = UIView()
|
||||
|
||||
@available(*, unavailable, renamed: "containerView")
|
||||
public var backgroundView: UIView!
|
||||
|
||||
private var containerViewHeightConstraint: NSLayoutConstraint!
|
||||
private lazy var containerViewTopInsetConstraint: NSLayoutConstraint = containerView.topAnchor.constraint(equalTo: topAnchor, constant: containerTopInset)
|
||||
private lazy var containerViewHeightConstraint: NSLayoutConstraint = containerView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0)
|
||||
|
||||
/// The content view top constraint
|
||||
private var contentViewTopConstraint: NSLayoutConstraint?
|
||||
/// The content view left constraint
|
||||
private var contentViewLeftConstraint: NSLayoutConstraint?
|
||||
/// The content right constraint
|
||||
private var contentViewRightConstraint: NSLayoutConstraint?
|
||||
/// The content height constraint
|
||||
private var contentViewHeightConstraint: NSLayoutConstraint?
|
||||
|
||||
private struct Default {
|
||||
public static let grabberTopPadding: CGFloat = 6.0
|
||||
}
|
||||
private lazy var grabberHandleWidthConstraint: NSLayoutConstraint = grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandleWidth)
|
||||
private lazy var grabberHandleHeightConstraint: NSLayoutConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleHeight)
|
||||
private lazy var grabberHandleTopConstraint: NSLayoutConstraint = grabberHandle.topAnchor.constraint(equalTo: topAnchor, constant: grabberTopPadding)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
render()
|
||||
addSubViews()
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
render()
|
||||
addSubViews()
|
||||
}
|
||||
|
||||
private func render() {
|
||||
private func addSubViews() {
|
||||
super.backgroundColor = .clear
|
||||
self.clipsToBounds = false
|
||||
|
||||
let containerView = UIView()
|
||||
addSubview(containerView)
|
||||
self.containerView = containerView
|
||||
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerViewHeightConstraint = containerView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0)
|
||||
NSLayoutConstraint.activate([
|
||||
containerView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
|
||||
containerViewTopInsetConstraint,
|
||||
containerView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0),
|
||||
containerView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0),
|
||||
containerViewHeightConstraint,
|
||||
])
|
||||
|
||||
let grabberHandle = GrabberHandleView()
|
||||
addSubview(grabberHandle)
|
||||
self.grabberHandle = grabberHandle
|
||||
|
||||
grabberHandle.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
grabberHandle.topAnchor.constraint(equalTo: topAnchor, constant: Default.grabberTopPadding),
|
||||
grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandle.frame.width),
|
||||
grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandle.frame.height),
|
||||
grabberHandleWidthConstraint,
|
||||
grabberHandleHeightConstraint,
|
||||
grabberHandleTopConstraint,
|
||||
grabberHandle.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
public override func updateConstraints() {
|
||||
super.updateConstraints()
|
||||
containerViewTopInsetConstraint.constant = containerTopInset
|
||||
containerViewHeightConstraint.constant = bottomOverflow
|
||||
|
||||
contentViewTopConstraint?.constant = contentInsets.top
|
||||
contentViewLeftConstraint?.constant = contentInsets.left
|
||||
contentViewRightConstraint?.constant = contentInsets.right
|
||||
contentViewHeightConstraint?.constant = -containerTopInset
|
||||
|
||||
grabberHandleTopConstraint.constant = grabberTopPadding
|
||||
grabberHandleWidthConstraint.constant = grabberHandleWidth
|
||||
grabberHandleHeightConstraint.constant = grabberHandleHeight
|
||||
|
||||
super.updateConstraints()
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
log.debug("surface view frame = \(frame)")
|
||||
|
||||
updateLayers()
|
||||
updateContentViewMask()
|
||||
updateBorder()
|
||||
|
||||
contentView?.frame = bounds
|
||||
}
|
||||
|
||||
private func updateLayers() {
|
||||
containerView.backgroundColor = color
|
||||
|
||||
if cornerRadius != 0.0, containerView.layer.cornerRadius != cornerRadius {
|
||||
containerView.layer.masksToBounds = true
|
||||
containerView.layer.cornerRadius = cornerRadius
|
||||
}
|
||||
updateShadow()
|
||||
updateCornerRadius()
|
||||
updateBorder()
|
||||
}
|
||||
|
||||
private func updateShadow() {
|
||||
if shadowHidden == false {
|
||||
layer.shadowColor = shadowColor.cgColor
|
||||
layer.shadowOffset = shadowOffset
|
||||
layer.shadowOpacity = shadowOpacity
|
||||
layer.shadowRadius = shadowRadius
|
||||
if #available(iOS 11, *) {
|
||||
// For clear background. See also, https://github.com/SCENEE/FloatingPanel/pull/51.
|
||||
layer.shadowColor = shadowColor.cgColor
|
||||
layer.shadowOffset = shadowOffset
|
||||
layer.shadowOpacity = shadowOpacity
|
||||
layer.shadowRadius = shadowRadius
|
||||
} else {
|
||||
// Can't update `layer.shadow*` directly because of a UIVisualEffectView issue in iOS 10, https://forums.developer.apple.com/thread/50854
|
||||
// Instead, a user should display shadow appropriately.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateContentViewMask() {
|
||||
guard
|
||||
cornerRadius != 0.0,
|
||||
containerView.layer.cornerRadius != cornerRadius
|
||||
else { return }
|
||||
|
||||
private func updateCornerRadius() {
|
||||
guard containerView.layer.cornerRadius != 0.0 else {
|
||||
containerView.layer.masksToBounds = false
|
||||
return
|
||||
}
|
||||
containerView.layer.masksToBounds = true
|
||||
if #available(iOS 11, *) {
|
||||
// Don't use `contentView.clipToBounds` because it prevents content view from expanding the height of a subview of it
|
||||
// for the bottom overflow like Auto Layout settings of UIVisualEffectView in Main.storyboard of Example/Maps.
|
||||
// Because the bottom of contentView must be fit to the bottom of a screen to work the `safeLayoutGuide` of a content VC.
|
||||
containerView.layer.masksToBounds = true
|
||||
containerView.layer.cornerRadius = cornerRadius
|
||||
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
} else {
|
||||
// Don't use `contentView.layer.mask` because of a UIVisualEffectView issue in iOS 10, https://forums.developer.apple.com/thread/50854
|
||||
// Instead, a user can mask the content view manually in an application.
|
||||
// Can't use `containerView.layer.mask` because of a UIVisualEffectView issue in iOS 10, https://forums.developer.apple.com/thread/50854
|
||||
// Instead, a user should display rounding corners appropriately.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,11 +217,20 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
self.contentView = contentView
|
||||
/* contentView.frame = bounds */ // MUST NOT: Because the top safe area inset of a content VC will be incorrect.
|
||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let topConstraint = contentView.topAnchor.constraint(equalTo: topAnchor, constant: contentInsets.top)
|
||||
let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: contentInsets.left)
|
||||
let rightConstraint = rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: contentInsets.right)
|
||||
let heightConstraint = contentView.heightAnchor.constraint(equalTo: heightAnchor, constant: -containerTopInset)
|
||||
NSLayoutConstraint.activate([
|
||||
contentView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
|
||||
contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0),
|
||||
contentView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0),
|
||||
contentView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0)
|
||||
topConstraint,
|
||||
leftConstraint,
|
||||
rightConstraint,
|
||||
heightConstraint,
|
||||
])
|
||||
self.contentViewTopConstraint = topConstraint
|
||||
self.contentViewLeftConstraint = leftConstraint
|
||||
self.contentViewRightConstraint = rightConstraint
|
||||
self.contentViewHeightConstraint = heightConstraint
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,32 +6,30 @@
|
||||
import UIKit
|
||||
|
||||
public class GrabberHandleView: UIView {
|
||||
public struct Default {
|
||||
public static let width: CGFloat = 36.0
|
||||
public static let height: CGFloat = 5.0
|
||||
public static let barColor = UIColor(displayP3Red: 0.76, green: 0.77, blue: 0.76, alpha: 1.0)
|
||||
}
|
||||
|
||||
public var barColor = UIColor(displayP3Red: 0.76, green: 0.77, blue: 0.76, alpha: 1.0) { didSet { backgroundColor = barColor } }
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
render()
|
||||
}
|
||||
|
||||
init() {
|
||||
let size = CGSize(width: Default.width,
|
||||
height: Default.height)
|
||||
super.init(frame: CGRect(origin: .zero, size: size))
|
||||
self.backgroundColor = Default.barColor
|
||||
render()
|
||||
super.init(frame: .zero)
|
||||
backgroundColor = barColor
|
||||
}
|
||||
|
||||
private func render() {
|
||||
self.layer.masksToBounds = true
|
||||
self.layer.cornerRadius = frame.size.height * 0.5
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
render()
|
||||
}
|
||||
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let view = super.hitTest(point, with: event)
|
||||
return view == self ? nil : view
|
||||
}
|
||||
|
||||
private func render() {
|
||||
self.layer.masksToBounds = true
|
||||
self.layer.cornerRadius = frame.size.height * 0.5
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.5.1</string>
|
||||
<string>1.6.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
|
||||
@@ -14,7 +14,7 @@ struct Logger {
|
||||
private let osLog: OSLog
|
||||
private let s = DispatchSemaphore(value: 1)
|
||||
|
||||
private enum Level: Int, Comparable {
|
||||
enum Level: Int, Comparable {
|
||||
case debug = 0
|
||||
case info = 1
|
||||
case warning = 2
|
||||
@@ -47,6 +47,9 @@ struct Logger {
|
||||
}
|
||||
}
|
||||
|
||||
typealias Hook = ((String, Level) -> Void)
|
||||
var hook: Hook?
|
||||
|
||||
fileprivate init() {
|
||||
osLog = OSLog(subsystem: "com.scenee.FloatingPanel", category: "FloatingPanel")
|
||||
}
|
||||
@@ -65,6 +68,8 @@ struct Logger {
|
||||
}
|
||||
}()
|
||||
|
||||
hook?(log, level)
|
||||
|
||||
os_log("%@", log: osLog, type: level.osLogType, log)
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +107,9 @@ extension UIScrollView {
|
||||
var contentOffsetZero: CGPoint {
|
||||
return CGPoint(x: 0.0, y: 0.0 - contentInset.top)
|
||||
}
|
||||
var isLocked: Bool {
|
||||
return !showsVerticalScrollIndicator && !bounces && isDirectionalLockEnabled
|
||||
}
|
||||
}
|
||||
|
||||
extension UISpringTimingParameters {
|
||||
@@ -124,3 +127,12 @@ extension CGPoint {
|
||||
y: CGFloat.nan)
|
||||
}
|
||||
}
|
||||
|
||||
extension UITraitCollection {
|
||||
func shouldUpdateLayout(from previous: UITraitCollection) -> Bool {
|
||||
return previous.horizontalSizeClass != horizontalSizeClass
|
||||
|| previous.verticalSizeClass != verticalSizeClass
|
||||
|| previous.preferredContentSizeCategory != preferredContentSizeCategory
|
||||
|| previous.layoutDirection != layoutDirection
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/11/01.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
let rootVC = UIViewController(nibName: nil, bundle: nil)
|
||||
rootVC.view.backgroundColor = .gray
|
||||
|
||||
let window = UIWindow()
|
||||
window.rootViewController = rootVC
|
||||
window.makeKeyAndVisible()
|
||||
self.window = window
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13142" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12042"/>
|
||||
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Copyright © 2019 scenee. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="obG-Y5-kRd">
|
||||
<rect key="frame" x="0.0" y="626.5" width="375" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="TestingApp" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="GJd-Yh-RWb">
|
||||
<rect key="frame" x="0.0" y="202" width="375" height="43"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="obG-Y5-kRd" secondAttribute="centerX" id="5cz-MP-9tL"/>
|
||||
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="GJd-Yh-RWb" secondAttribute="centerX" id="Q3B-4B-g5h"/>
|
||||
<constraint firstItem="obG-Y5-kRd" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" constant="20" symbolic="YES" id="SfN-ll-jLj"/>
|
||||
<constraint firstAttribute="bottom" secondItem="obG-Y5-kRd" secondAttribute="bottom" constant="20" id="Y44-ml-fuU"/>
|
||||
<constraint firstItem="GJd-Yh-RWb" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="bottom" multiplier="1/3" constant="1" id="moa-c2-u7t"/>
|
||||
<constraint firstItem="GJd-Yh-RWb" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" constant="20" symbolic="YES" id="x7j-FC-K8j"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// 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 myVC = MyZombieViewController(nibName: nil, bundle: nil)
|
||||
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()
|
||||
}
|
||||
}
|
||||
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)
|
||||
XCTAssert(fpc.surfaceView.frame.minY == (fpc.view.bounds.height - fpc.layoutInsets.bottom) - fpc.layout.insetFor(position: .half)!)
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssert(fpc.surfaceView.frame.minY == (fpc.view.bounds.height - fpc.layoutInsets.bottom) - fpc.layout.insetFor(position: .tip)!)
|
||||
}
|
||||
|
||||
@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 nil
|
||||
}
|
||||
}
|
||||
let myDelegate = MyDelegate()
|
||||
let fpc = FloatingPanelController(delegate: myDelegate)
|
||||
let traitCollection = UITraitCollection(traitsFrom: [fpc.traitCollection,
|
||||
UITraitCollection(userInterfaceStyle: .dark)])
|
||||
XCTAssertEqual(traitCollection.userInterfaceStyle, .dark)
|
||||
fpc.prepare(for: traitCollection)
|
||||
}
|
||||
}
|
||||
|
||||
private class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
|
||||
var fpc: FloatingPanelController?
|
||||
override func viewDidLoad() {
|
||||
fpc = FloatingPanelController(delegate: self)
|
||||
fpc?.addPanel(toParent: self)
|
||||
}
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
return self
|
||||
}
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
|
||||
return self
|
||||
}
|
||||
var initialPosition: FloatingPanelPosition {
|
||||
return .half
|
||||
}
|
||||
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
switch position {
|
||||
case .full: return UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0
|
||||
case .half: return 262.0
|
||||
case .tip: return 69.0
|
||||
case .hidden: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2019/06/27.
|
||||
// Copyright © 2019 scenee. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class FloatingPanelLayoutTests: XCTestCase {
|
||||
override func setUp() {}
|
||||
override func tearDown() {}
|
||||
|
||||
func test_layoutAdapter_topAndBottomMostState() {
|
||||
let fpc = FloatingPanelController(delegate: nil)
|
||||
fpc.loadViewIfNeeded()
|
||||
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.topMostState, .full)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.bottomMostState, .tip)
|
||||
|
||||
class FloatingPanelLayoutWithHidden: FloatingPanelLayout {
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? { return nil }
|
||||
let initialPosition: FloatingPanelPosition = .hidden
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .half, .full]
|
||||
}
|
||||
class FloatingPanelLayout2Positions: FloatingPanelLayout {
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? { return nil }
|
||||
let initialPosition: FloatingPanelPosition = .tip
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.tip, .half]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayoutWithHidden()
|
||||
fpc.delegate = delegate
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.topMostState, .full)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.bottomMostState, .half) // Will fixed on fix-hidden-position branch
|
||||
|
||||
delegate.layout = FloatingPanelLayout2Positions()
|
||||
fpc.delegate = delegate
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.topMostState, .half)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.bottomMostState, .tip)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2019/05/23.
|
||||
// Copyright © 2019 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class FloatingPanelSurfaceViewTests: XCTestCase {
|
||||
override func setUp() {}
|
||||
|
||||
override func tearDown() {}
|
||||
|
||||
func test_surfaceView() {
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
XCTAssert(surface.contentView == nil)
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.grabberHandle.frame.minY == 6.0)
|
||||
XCTAssert(surface.grabberHandle.frame.width == surface.grabberHandleWidth)
|
||||
XCTAssert(surface.grabberHandle.frame.height == surface.grabberHandleHeight)
|
||||
surface.backgroundColor = .red
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.backgroundColor == surface.containerView.backgroundColor)
|
||||
}
|
||||
|
||||
func test_surfaceView_constraintsUpdate() {
|
||||
let window = UIWindow()
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
window.addSubview(surface)
|
||||
window.makeKeyAndVisible()
|
||||
XCTAssert(surface.contentView == nil)
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.grabberHandle.frame.minY == 6.0)
|
||||
XCTAssert(surface.grabberHandle.frame.width == surface.grabberHandleWidth)
|
||||
XCTAssert(surface.grabberHandle.frame.height == surface.grabberHandleHeight)
|
||||
|
||||
surface.grabberHandleWidth = 44.0
|
||||
surface.grabberHandleHeight = 12.0
|
||||
surface.layoutIfNeeded()
|
||||
waitRunLoop(secs: 0.000_001)
|
||||
XCTAssert(surface.grabberHandle.frame.width == surface.grabberHandleWidth, "\(surface.grabberHandle.frame.width) == \(surface.grabberHandleWidth)")
|
||||
XCTAssert(surface.grabberHandle.frame.height == surface.grabberHandleHeight, "\(surface.grabberHandle.frame.height) == \(surface.grabberHandleHeight)")
|
||||
window.resignKey()
|
||||
}
|
||||
|
||||
func test_surfaceView_cornderRaduis() {
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
XCTAssert(surface.cornerRadius == 0.0)
|
||||
XCTAssert(surface.containerView.layer.masksToBounds == false)
|
||||
|
||||
surface.cornerRadius = 10.0
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.cornerRadius == 10.0)
|
||||
XCTAssert(surface.containerView.layer.cornerRadius == 10.0)
|
||||
XCTAssert(surface.containerView.layer.masksToBounds == true)
|
||||
|
||||
surface.containerView.layer.cornerRadius = 12.0
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.cornerRadius == 12.0)
|
||||
XCTAssert(surface.containerView.layer.masksToBounds == true)
|
||||
|
||||
surface.cornerRadius = 0.0
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.cornerRadius == 0.0)
|
||||
XCTAssert(surface.containerView.layer.cornerRadius == 0.0)
|
||||
XCTAssert(surface.containerView.layer.masksToBounds == false)
|
||||
|
||||
surface.containerView.layer.cornerRadius = 12.0
|
||||
surface.setNeedsLayout()
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.cornerRadius == 12.0)
|
||||
XCTAssert(surface.containerView.layer.masksToBounds == true)
|
||||
}
|
||||
|
||||
func test_surfaceView_border() {
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
XCTAssert(surface.borderColor == nil)
|
||||
XCTAssert(surface.borderWidth == 0.0)
|
||||
|
||||
surface.borderColor = .red
|
||||
surface.borderWidth = 3.0
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.containerView.layer.borderColor == UIColor.red.cgColor)
|
||||
XCTAssert(surface.containerView.layer.borderWidth == 3.0)
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,78 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/09/18.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
// Created by Shin Yamamoto on 2019/05/23.
|
||||
// Copyright © 2019 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class ViewTests: XCTestCase {
|
||||
class FloatingPanelTests: XCTestCase {
|
||||
|
||||
override func setUp() {}
|
||||
|
||||
override func tearDown() {}
|
||||
|
||||
func test_WarningRetainCycle() {
|
||||
let myVC = MyZombieViewController(nibName: nil, bundle: nil)
|
||||
myVC.loadViewIfNeeded()
|
||||
// Check if there are memory leak warnings in console logs
|
||||
func test_scrolllock() {
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.loadViewIfNeeded()
|
||||
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
|
||||
let contentVC1 = UITableViewController(nibName: nil, bundle: nil)
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, true)
|
||||
|
||||
fpc.set(contentViewController: contentVC1)
|
||||
fpc.track(scrollView: contentVC1.tableView)
|
||||
fpc.show(animated: false, completion: nil) // half
|
||||
XCTAssertEqual(fpc.position, .half)
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, false)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, true)
|
||||
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, false)
|
||||
|
||||
let exp1 = expectation(description: "move to full with animation")
|
||||
fpc.move(to: .full, animated: true) {
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, true)
|
||||
exp1.fulfill()
|
||||
}
|
||||
wait(for: [exp1], timeout: 1.0)
|
||||
|
||||
let exp2 = expectation(description: "move to tip with animation")
|
||||
fpc.move(to: .tip, animated: false) {
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, false)
|
||||
exp2.fulfill()
|
||||
}
|
||||
wait(for: [exp2], timeout: 1.0)
|
||||
|
||||
// Reset the content vc
|
||||
let contentVC2 = UITableViewController(nibName: nil, bundle: nil)
|
||||
XCTAssertEqual(contentVC2.tableView.showsVerticalScrollIndicator, true)
|
||||
XCTAssertEqual(contentVC2.tableView.bounces, true)
|
||||
fpc.set(contentViewController: contentVC2)
|
||||
fpc.track(scrollView: contentVC2.tableView)
|
||||
fpc.show(animated: false, completion: nil)
|
||||
XCTAssertEqual(fpc.position, .half)
|
||||
XCTAssertEqual(contentVC2.tableView.showsVerticalScrollIndicator, false)
|
||||
XCTAssertEqual(contentVC2.tableView.bounces, false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
|
||||
var fpc: FloatingPanelController?
|
||||
override func viewDidLoad() {
|
||||
fpc = FloatingPanelController(delegate: self)
|
||||
fpc?.addPanel(toParent: self)
|
||||
}
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
return self
|
||||
}
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
|
||||
return self
|
||||
}
|
||||
var initialPosition: FloatingPanelPosition {
|
||||
return .half
|
||||
}
|
||||
|
||||
private protocol FloatingPanelTestLayout: FloatingPanelLayout {}
|
||||
private extension FloatingPanelTestLayout {
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
switch position {
|
||||
case .full: return UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0
|
||||
case .full: return 18.0
|
||||
case .half: return 262.0
|
||||
case .tip: return 69.0
|
||||
case .hidden: return nil
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2019/06/27.
|
||||
// Copyright © 2019 scenee. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@testable import FloatingPanel
|
||||
|
||||
func waitRunLoop(secs: TimeInterval = 0) {
|
||||
RunLoop.main.run(until: Date(timeIntervalSinceNow: secs))
|
||||
}
|
||||
|
||||
class FloatingPanelTestDelegate: FloatingPanelControllerDelegate {
|
||||
var layout: FloatingPanelLayout?
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
return layout
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// swift-tools-version:5.0
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "FloatingPanel",
|
||||
platforms: [
|
||||
.iOS(.v10)
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries produced by a package, and make them visible to other packages.
|
||||
.library(
|
||||
name: "FloatingPanel",
|
||||
targets: ["FloatingPanel"]),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
|
||||
.target(name: "FloatingPanel", path: "Framework/Sources"),
|
||||
],
|
||||
swiftLanguageVersions: [.version("5")]
|
||||
)
|
||||
Reference in New Issue
Block a user