Compare commits

..

98 Commits

Author SHA1 Message Date
Shin Yamamoto a225bf2cf1 Release v1.6.2 2019-07-10 18:55:09 +09:00
Shin Yamamoto 11a16092a7 Merge pull request #231 from SCENEE/prevent-found-nil-error
Prevent 'unexpectedly found nil' fatal error
2019-07-09 21:53:40 +09:00
Shin Yamamoto b9b7f940b9 Prevent 'unexpectedly found nil' fatal error
Use FloatingPanel.viewcontroller as an optional value instead of
an implicitly unwrapping optional one.
2019-07-09 20:06:16 +09:00
Shin Yamamoto 206475e6ab Merge pull request #232 from SCENEE/refactor-layout-adapter
Refactor layout adapter
2019-07-03 14:24:47 +09:00
Shin Yamamoto a4a68e5b39 Add test_surfaceView_constraintsUpdate() 2019-07-03 11:46:45 +09:00
Shin Yamamoto de7ab0e0cb Rename FloatingPanelViewTests to FloatingPanelSurfaceViewTests 2019-07-03 11:46:45 +09:00
Shin Yamamoto 5f7b5ce81c Add FloatingPanelLayoutTests & Utils 2019-07-03 11:46:45 +09:00
Shin Yamamoto 36d7ea5100 Improve testing speed 2019-07-03 11:34:21 +09:00
Shin Yamamoto 33f8cf3802 Modify FloatingPanel.distance(to:) 2019-07-03 11:34:21 +09:00
Shin Yamamoto f6da876fdf Add botomMostState prop 2019-07-03 11:34:21 +09:00
Shin Yamamoto 96c5dc7b74 Add FloatingPanelLayoutTests 2019-07-03 11:34:02 +09:00
Shin Yamamoto a37931b62d Merge pull request #230 from SCENEE/fix-scrollindicator
Fix the scroll indicator lock on a contentVC reset
2019-07-03 09:55:32 +09:00
Shin Yamamoto 5c848d9bf5 Fix the scroll indicator lock on a contentVC reset
The locking logic couldn't take care of the case where a content view
controller of a FloatingPanelController object is replaced.
2019-07-02 19:12:58 +09:00
Shin Yamamoto 265b805fa9 No more need FloatingPanel to conform UIScrollViewDelegate 2019-07-02 14:21:10 +09:00
Shin Yamamoto c4dfe33a5e Merge pull request #229 from SCENEE/release-1.6.1
Release v1.6.1
2019-06-29 09:31:17 +09:00
Shin Yamamoto 999eeb47ba Release v1.6.1 2019-06-29 08:33:24 +09:00
Shin Yamamoto a5bf02cfec Merge pull request #228 from SCENEE/fix-unexpected-layout-update
Fix an unexpected layout update on iOS13
2019-06-29 08:32:48 +09:00
Shin Yamamoto c10186e50a Prevent an unexpected layout update on iOS13
On iOS13, UITraitCollection.userInterfaceStyle can be changed
from .light to .dark when an app transitions to the background.
2019-06-29 07:41:52 +09:00
Shin Yamamoto 7a1cbf99d4 Rename setUpLayout to activateLayout 2019-06-28 20:23:10 +09:00
Shin Yamamoto c9c4000536 Merge pull request #225 from SCENEE/fix-seamless-scrolling
Remove workaround for tableView(_:didSelectRowAt:) issue
2019-06-19 10:34:57 +09:00
Shin Yamamoto 656bbc1b1c Remove workaround for tableView(_:didSelectRowAt:) issue
The workaround was added to avoid `tableView(_:didSelectRowAt:)` not
being called on first tap after the moving animation. However, it
doesn't only resolved the issue, but also has side effects.

For example, it affects the seamless scrolling in dragging up a panel from
half to full after bouncing it in the bottom buffer. The problem occurs
on "Tab2" sample of "Show Tab Bar".

Moreover the UITableView issue seems to be relieved on iOS 13.

Therefore I remove the workaround.
2019-06-19 09:39:56 +09:00
Shin Yamamoto 3815a08af5 Merge pull request #221 from SCENEE/fix-closing-panel-in-bounce
Fix closing panel during internal scroll view bounce
2019-06-17 08:04:56 +09:00
Shin Yamamoto 404fdb6496 Fix flushing a scroll indicator
1. A scroll indicator flushed at the first time when a tacking scroll view's
offset is zero and a user swipes down a panel at the top most position
2. A scroll indicator flushed at the first time when a tacking scroll view's
offset is zero and a user swipes up a panel at non top most position
2019-06-16 21:33:37 +09:00
Shin Yamamoto 573f355c15 Remove unnecessary code
There is not reason why the code is needed because the scroll tracking
logic is working well without it.
2019-06-16 21:32:35 +09:00
Shin Yamamoto bd0c891795 Fix closing panel during internal scroll view bounce
Now the scroll tracking is working well without the scroll offset handling
at the top most position in the callback of a scroll pan gesture.
2019-06-14 14:00:55 +09:00
Robbie Trencheny f4857a3da9 Add Swift Package Manager support (#219)
* Add Package.swift
2019-06-13 07:59:12 +09:00
Shin Yamamoto e074c3caf1 Merge pull request #220 from SCENEE/fix-removal-crash
Fix the crash while closeing via dragging
2019-06-12 08:56:31 +09:00
Shin Yamamoto 0f4c7503b1 Fix the crash while closeing via dragging
While closing the viewcontroller via dragging, calling floatPanelController's hide() will cause a crash.
2019-06-11 08:26:16 +09:00
Shin Yamamoto 2cb142a31f Merge pull request #213 from SCENEE/release-1.6.0
Release v1.6.0
2019-06-03 22:12:36 +09:00
Shin Yamamoto 2b05ea8d92 Release v1.6.0 2019-06-03 20:56:04 +09:00
Shin Yamamoto d255e1ea4a Call `super.updateConstraints()' as the final step 2019-06-03 20:51:58 +09:00
Shin Yamamoto 6fcb817fb8 Add the rubberbanding behavior for top & bottom buffer (#144)
* Add sample code
* Fix updateInteractiveTopConstraint()
    * {min,max}Y variables are confusing because it's not a value of coordinate Y, 
       but a constant value from the `interactiveTopConstraint`.
2019-06-01 16:19:09 +09:00
Shin Yamamoto e2ebfd01df Merge pull request #211 from SCENEE/avoid-weird-crash
* Use wholemodule compilation mode on Debug
* Set APPLICATION_EXTENSION_API_ONLY to YES by default
2019-06-01 13:55:00 +09:00
Sven Tiigi cf70929204 Added ContentInset Property on SurfaceView API (#200)
* Added Show ContentInset to Example application
2019-06-01 13:46:18 +09:00
Shin Yamamoto 624e3f7553 Set APPLICATION_EXTENSION_API_ONLY to YES by default 2019-05-31 13:34:26 +09:00
Shin Yamamoto 3cc8538db3 Use wholemodule compilation mode on Debug 2019-05-31 13:34:07 +09:00
Shin Yamamoto a9a65436bb Merge pull request #209 from SCENEE/improve-tests
Add unit tests
2019-05-27 22:21:37 +09:00
Shin Yamamoto 353dabfc47 Update Maps example for iOS 10 shadow 2019-05-25 16:07:22 +09:00
Shin Yamamoto 1bdf0f5b78 Remove unnecessary frame update 2019-05-25 16:07:22 +09:00
Shin Yamamoto 6696d7f71d Fix UIVisualEffectView on iOS10
This regression has happened since v1.2.0
2019-05-25 16:07:22 +09:00
Shin Yamamoto 59a6c7e576 Fix errors on simulator testing
Fix the following errors.
- 'dyld: program was built for a platform that is not supported by this runtime'
- 'dyld: Library not loaded: @rpath/libswiftCore.dylib'
2019-05-25 16:07:22 +09:00
Shin Yamamoto 0b0148635e Fix .travis.yml 2019-05-25 16:07:22 +09:00
Shin Yamamoto c354d8ea92 Modify FloatingSurfaceView.cornerRadius 2019-05-25 16:07:22 +09:00
Shin Yamamoto 9562cdaccb Clean up surface props 2019-05-25 16:07:22 +09:00
Shin Yamamoto bcfff8a33a Add FloatingPanelViewTests 2019-05-25 16:07:21 +09:00
Shin Yamamoto f5c409ba90 Fix test failed on iOS 10.3.1
This resolves the following error.

> xctest (86533) encountered an error (Failed to load the test bundle. (Underlying error: The bundle “FloatingPanelTests” couldn’t be loaded because it is damaged or missing necessary resources. The bundle is damaged or missing necessary resources. dlopen_preflight(..omitted../Build/Products/Test-iphonesimulator/FloatingPanelTests.xctest/FloatingPanelTests): no suitable image found.  Did find:
>	    ..omitted../Build/Products/Test-iphonesimulator/FloatingPanelTests.xctest/FloatingPanelTests: mach-o, but not built for iOS simulator))
2019-05-25 16:06:56 +09:00
Shin Yamamoto 2f23520330 Improve ViewTests.test_WarningRetainCycle() 2019-05-25 16:06:56 +09:00
Shin Yamamoto a95694cbfc Add testing jobs in Travic CI 2019-05-25 16:06:56 +09:00
Shin Yamamoto 6cfba6495f Add TestingApp 2019-05-25 16:06:52 +09:00
Shin Yamamoto b9f3de1c64 Merge pull request #207 from SCENEE/improve-surface-w-lazy-props
Improve surface w lazy props
2019-05-21 22:56:02 +09:00
Shin Yamamoto c67b56e7af Clean up code 2019-05-17 22:54:47 +09:00
Shin Yamamoto bf39f07691 Use lazy properties in the surface view 2019-05-16 10:43:41 +09:00
Shin Yamamoto a9e46f0de6 Merge pull request #204 from SCENEE/fix-scroll-lock-2
Avoid calling {lock,unlock}ScrollView() unexpectedly
2019-05-16 10:41:04 +09:00
Shin Yamamoto 05478fa8fa Avoid calling {lock,unlock}ScrollView() unexpectedly 2019-05-11 15:16:58 +09:00
Shin Yamamoto d123afc3f7 Fix a crash 2019-05-11 13:40:58 +09:00
Shin Yamamoto b1b3c15300 Merge pull request #201 from SCENEE/fix-scroll-lock
Fix scroll lock
2019-05-11 12:51:53 +09:00
Shin Yamamoto 49bae50739 Merge pull request #203 from SCENEE/patch-pr194
Improve surface container API
2019-05-11 12:49:48 +09:00
Shin Yamamoto 9b5459af8e Fix the content height changed by a container inset 2019-05-11 12:11:04 +09:00
Shin Yamamoto 96d2ea57f5 Clean up prop naming 2019-05-11 11:53:05 +09:00
Shin Yamamoto b78c5f4ece Decouple between grabberTopPadding and containerTopInset 2019-05-11 11:02:06 +09:00
Shin Yamamoto 341522ccaa Fix grabberhandle corner 2019-05-11 11:02:06 +09:00
Shin Yamamoto 833628e42f Update the content view height by containerTopInset 2019-05-11 11:02:06 +09:00
Shin Yamamoto 50c1c6fdc9 Fix the dependency on ordering prop updates 2019-05-11 11:02:06 +09:00
Shin Yamamoto 213386e822 Fix the sign of containerTopInset 2019-05-11 11:02:06 +09:00
Shin Yamamoto 17317ed274 Merge pull request #194 from nderkach/master
Grabber handle and top offset customizations
2019-05-11 10:58:25 +09:00
Shin Yamamoto 652ae8c967 Remove a condition to prevent animation cancel
Because I can't confirm any effect to fix that selecting a table view cell
sometimes isn't working after flicking to half from full.
2019-05-04 16:15:10 +09:00
Shin Yamamoto ec0e8cbdaf Avoid any tap gesture recognition while dragging a panel 2019-05-04 16:03:19 +09:00
Shin Yamamoto c15d4c9035 Fix the moving animation's interruption 2019-05-04 16:03:19 +09:00
Shin Yamamoto 39dfdd0ef0 Remove an unused code 2019-05-04 16:03:19 +09:00
Shin Yamamoto d25bc58249 Add a sample for tap-to-move 2019-05-04 16:03:18 +09:00
Shin Yamamoto 194a197e83 Fix a scroll lock after moving a panel 2019-05-04 14:46:39 +09:00
Shin Yamamoto bd02f34bcf Merge pull request #196 from SCENEE/release-1.5.1
Release v1.5.1
2019-04-26 13:43:54 +09:00
Shin Yamamoto 7d5f03bb6e Release v1.5.1 2019-04-25 16:18:59 +09:00
Nikolay Derkach 60f41e168f configure grabber handle and top container offset 2019-04-24 14:07:46 +02:00
Shin Yamamoto 680b16aa25 Merge pull request #185 from SCENEE/fix-tap-abort
Fix a touch abort on the interaction interrupted
2019-04-24 14:54:20 +09:00
Shin Yamamoto 2394c03dca Polish a doc comment 2019-04-20 16:00:22 +09:00
Shin Yamamoto c8f211f2bf fix typo 2019-04-19 08:56:54 +09:00
Shin Yamamoto 9076ba8933 Prevent an UIScrollViewDelayedTouchesBeganGestureRecognizer failed
It causes tableView(_:didSelectRowAt:) not being called on first tap
after an nimation. The change hasn't fixed the problem completely, but
it works better.
2019-04-19 08:46:51 +09:00
Shin Yamamoto 08e79bfc5c Merge pull request #191 from SCENEE/add-pages-sample
Add pages sample
2019-04-17 19:22:40 +09:00
Shin Yamamoto e4808516aa Merge pull request #190 from SCENEE/fix-surface-mask
Fix surface mask
2019-04-17 19:21:56 +09:00
Shin Yamamoto 5888104e98 Remove obsolete SWIFT_WHOLE_MODULE_OPTIMIZATION setting
Removing this setting will fix a warning emitted by Xcode to update
project settings.
2019-04-17 09:36:36 +09:00
Shin Yamamoto 2b8d29759a Rename the remaining backgroundView variable 2019-04-16 22:53:06 +09:00
Shin Yamamoto 8e4b56ff17 Fix the doc comment 2019-04-16 07:51:27 +09:00
Shin Yamamoto 23c5761c14 Update README 2019-04-15 21:05:46 +09:00
Shin Yamamoto 835ec0c3a0 Replace 'backgroundView' with 'containerView' 2019-04-15 21:05:16 +09:00
Shin Yamamoto d2dce0b6f8 Add a doc comment 2019-04-15 21:00:19 +09:00
Shin Yamamoto 3f8628af01 Fix typo 2019-04-11 22:52:49 +09:00
Shin Yamamoto 40c6fae07c Add a sample for panels in UIPageViewController 2019-04-11 22:52:29 +09:00
Shin Yamamoto e1185fda93 Merge pull request #188 from SCENEE/allow-subclassing
Allow FloatingPanelController subclassing
2019-04-11 22:31:37 +09:00
Shin Yamamoto 458ed903c5 Update README 2019-04-11 11:28:35 +09:00
Shin Yamamoto 4b640f4f01 Add updateBorder() 2019-04-11 11:25:11 +09:00
Shin Yamamoto 8743c5efd0 Fix the mask bounds of the surface view
- Move `contentView` into `backgroundView` to fix the mask
- Also fix the mask problem on iOS 10-11. See for detail,
https://github.com/SCENEE/FloatingPanel/issues/109.
2019-04-11 11:21:16 +09:00
Shin Yamamoto 9bd9d31d40 Merge pull request #186 from zntfdr/patch-1
Fix FloatingPanelBehavior.swift typos
2019-04-08 21:24:48 +09:00
Federico Zanetello 7e3d720720 Fix FloatingPanelBehavior.swift typos 2019-04-08 08:26:30 +07:00
Shin Yamamoto 7a512191ab Fix a touch abort on the interaction interrupted
Because touch events seem to be cancelled when an animator is released.
2019-04-06 11:25:38 +09:00
Shin Yamamoto af767863bb Merge pull request #177 from SCENEE/support-swift5
[Release v1.5.0] Support both of Swift 5 and 4.2
2019-04-06 10:54:54 +09:00
Shin Yamamoto 1b233f4f87 Update README 2019-04-05 08:21:49 +09:00
Shin Yamamoto 5df36a6601 Allow FloatingPanelController subclassing 2019-03-29 21:20:02 +09:00
26 changed files with 1155 additions and 306 deletions
+1
View File
@@ -22,6 +22,7 @@ xcuserdata/
*.moved-aside
*.xccheckout
*.xcscmblueprint
*.xcsettings
## Obj-C/Swift specific
*.hmap
+28 -25
View File
@@ -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"
+25 -3
View File
@@ -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
}
}
+155 -42
View File
@@ -9,12 +9,7 @@
import UIKit
import FloatingPanel
/**
- Attention: `FloatingPanelLayout` must not be applied by the parent view
controller of a floating panel. But here `SampleListViewController` adopts it
purposely to check if the library prints an appropriate warning.
*/
class SampleListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, FloatingPanelControllerDelegate, FloatingPanelLayout {
class SampleListViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
enum Menu: Int, CaseIterable {
@@ -24,9 +19,11 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case showModal
case showFloatingPanelModal
case showTabBar
case showPageView
case showNestedScrollView
case showRemovablePanel
case showIntrinsicView
case showContentInset
var name: String {
switch self {
@@ -36,9 +33,11 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .showModal: return "Show Modal"
case .showFloatingPanelModal: return "Show Floating Panel Modal"
case .showTabBar: return "Show Tab Bar"
case .showPageView: return "Show Page View"
case .showNestedScrollView: return "Show Nested ScrollView"
case .showRemovablePanel: return "Show Removable Panel"
case .showIntrinsicView: return "Show Intrinsic View"
case .showContentInset: return "Show with ContentInset"
}
}
@@ -50,9 +49,11 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .showModal: return "ModalViewController"
case .showFloatingPanelModal: return nil
case .showTabBar: return "TabBarViewController"
case .showPageView: return nil
case .showNestedScrollView: return "NestedScrollViewController"
case .showRemovablePanel: return "DetailViewController"
case .showIntrinsicView: return "IntrinsicViewController"
case .showContentInset: return nil
}
}
}
@@ -66,6 +67,19 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
var mainPanelObserves: [NSKeyValueObservation] = []
var settingsObserves: [NSKeyValueObservation] = []
lazy var pages: [UIViewController] = {
let page1 = FloatingPanelController(delegate: self)
page1.view.backgroundColor = .blue
page1.show()
let page2 = FloatingPanelController(delegate: self)
page2.view.backgroundColor = .red
page2.show()
let page3 = FloatingPanelController(delegate: self)
page3.view.backgroundColor = .green
page3.show()
return [page1, page2, page3]
}()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
@@ -118,6 +132,11 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
// 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
@@ -148,8 +167,14 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
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) {
@@ -164,31 +189,6 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
}
}
// MARK:- TableViewDatasource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if #available(iOS 11.0, *) {
if navigationController?.navigationBar.prefersLargeTitles == true {
return Menu.allCases.count + 30
} else {
return Menu.allCases.count
}
} else {
return Menu.allCases.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if Menu.allCases.count > indexPath.row {
let menu = Menu.allCases[indexPath.row]
cell.textLabel?.text = menu.name
} else {
cell.textLabel?.text = "\(indexPath.row) row"
}
return cell
}
// MARK:- Actions
@IBAction func showDebugMenu(_ sender: UIBarButtonItem) {
guard settingsPanelVC == nil else { return }
@@ -213,9 +213,34 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
// Add FloatingPanel to self.view
settingsPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
}
}
// MARK:- TableViewDelegate
extension SampleListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if #available(iOS 11.0, *) {
if navigationController?.navigationBar.prefersLargeTitles == true {
return Menu.allCases.count + 30
} else {
return Menu.allCases.count
}
} else {
return Menu.allCases.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if Menu.allCases.count > indexPath.row {
let menu = Menu.allCases[indexPath.row]
cell.textLabel?.text = menu.name
} else {
cell.textLabel?.text = "\(indexPath.row) row"
}
return cell
}
}
extension SampleListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard Menu.allCases.count > indexPath.row else { return }
let menu = Menu.allCases[indexPath.row]
@@ -246,6 +271,22 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .showModal, .showTabBar:
let modalVC = contentVC
present(modalVC, animated: true, completion: nil)
case .showPageView:
let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
let closeButton = UIButton(type: .custom)
pageVC.view.addSubview(closeButton)
closeButton.setTitle("Close", for: .normal)
closeButton.translatesAutoresizingMaskIntoConstraints = false
closeButton.addTarget(self, action: #selector(dismissPresentedVC), for: .touchUpInside)
NSLayoutConstraint.activate([
closeButton.topAnchor.constraint(equalTo: pageVC.layoutGuide.topAnchor, constant: 16.0),
closeButton.leftAnchor.constraint(equalTo: pageVC.view.leftAnchor, constant: 16.0),
])
pageVC.dataSource = self
pageVC.setViewControllers([pages[0]], direction: .forward, animated: false, completion: nil)
present(pageVC, animated: true, completion: nil)
case .showFloatingPanelModal:
let fpc = FloatingPanelController()
let contentVC = self.storyboard!.instantiateViewController(withIdentifier: "DetailViewController")
@@ -258,6 +299,18 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
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) {
@@ -266,6 +319,12 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
}
}
@objc func dismissPresentedVC() {
self.presentedViewController?.dismiss(animated: true, completion: nil)
}
}
extension SampleListViewController: FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
if vc == settingsPanelVC {
return IntrinsicPanelLayout()
@@ -290,6 +349,9 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
switch currentMenu {
case .showNestedScrollView:
return (vc.contentViewController as? NestedScrollViewController)?.nestedScrollView.gestureRecognizers?.contains(gestureRecognizer) ?? false
case .showPageView:
// Tips: Need to allow recognizing the pan gesture of UIPageViewController simultaneously.
return true
default:
return false
}
@@ -303,7 +365,14 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
break
}
}
}
/**
- Attention: `FloatingPanelLayout` must not be applied by the parent view
controller of a floating panel. But here `SampleListViewController` adopts it
purposely to check if the library prints an appropriate warning.
*/
extension SampleListViewController: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .half
}
@@ -318,6 +387,23 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
}
}
extension SampleListViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard
let index = pages.firstIndex(of: viewController),
index + 1 < pages.count
else { return nil }
return pages[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard
let index = pages.firstIndex(of: viewController),
index - 1 >= 0
else { return nil }
return pages[index - 1]
}
}
class IntrinsicPanelLayout: FloatingPanelIntrinsicLayout { }
class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
@@ -473,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
@@ -591,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
}
@@ -605,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 [
@@ -817,7 +911,7 @@ class TabBarContentViewController: UIViewController {
dismiss(animated: true, completion: nil)
}
// MAKR: - Private
// MARK: - Private
@objc
private func changeTab3Mode(_ sender: UISwitch) {
@@ -835,9 +929,9 @@ extension TabBarContentViewController: UITextViewDelegate {
guard self.tabBarItem.tag == 2 else { return }
// Reset an invalid content offset by a user after updating the layout
// of `consoleVC.textView`.
// NOTE: FloatingPanel doesn't implicity reset the offset(i.e.
// NOTE: FloatingPanel doesn't implicitly reset the offset(i.e.
// Using KVO of `scrollView.contentOffset`). Because it can lead to an
// infinit loop if a user also resets a content offset as below and,
// infinite loop if a user also resets a content offset as below and,
// in the situation, a user has to modify the library.
if fpc.position != .full, fpc.surfaceView.frame.minY < fpc.originYOfSurface(for: .full) {
scrollView.contentOffset = .zero
@@ -862,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 }
@@ -876,7 +979,7 @@ extension TabBarContentViewController: FloatingPanelControllerDelegate {
}
case .changeOffset:
/*
Bad solution: Manipulate scoll content inset
Bad solution: Manipulate scroll content inset
FloatingPanelController keeps a content offset in moving a panel
so that changing content inset or offset causes a buggy behavior.
@@ -918,7 +1021,7 @@ extension TabBarContentViewController: FloatingPanelControllerDelegate {
consoleVC.textViewTopConstraint?.constant = (vc.position == .full) ? vc.layoutInsets.top : 17.0
case .changeOffset:
/* Bad Solution: Manipulate scoll content inset */
/* Bad Solution: Manipulate scroll content inset */
guard let scrollView = consoleVC.textView else { return }
var insets = vc.adjustedContentInsets
insets.top = (vc.position == .full) ? vc.layoutInsets.top : 0.0
@@ -982,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!
@@ -1047,7 +1160,7 @@ class SettingsViewController: InspectableViewController {
override func viewDidLoad() {
versionLabel.text = "Version: \(Bundle.main.infoDictionary?["CFBundleVersion"] ?? "--")"
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {
+1 -2
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "1.5.0"
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 = { 'SWIFT_WHOLE_MODULE_OPTIMIZATION' => 'YES', '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"
+102 -75
View File
@@ -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 {
@@ -91,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()
}
@@ -100,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
@@ -116,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
@@ -123,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?()
}
}
@@ -159,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
}
@@ -173,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)
}
}
@@ -205,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,
@@ -228,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
}
@@ -260,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 {
@@ -278,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()
}
}
}
@@ -303,17 +316,17 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
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 {
if abs(surfaceView.frame.minY - layoutAdapter.topY) < 1.0 {
surfaceView.frame.origin.y = layoutAdapter.topY
}
}
if interactionInProgress == false,
viewcontroller.delegate?.floatingPanelShouldBeginDragging(viewcontroller) == false {
let vc = viewcontroller,
vc.delegate?.floatingPanelShouldBeginDragging(vc) == false {
return
}
@@ -417,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()
@@ -425,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 {
@@ -443,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
@@ -462,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
@@ -504,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
@@ -551,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)
@@ -565,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)")
@@ -575,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
}
@@ -583,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)
@@ -615,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
@@ -636,10 +662,13 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
private func finishAnimation(at targetPosition: FloatingPanelPosition) {
log.debug("finishAnimation to \(targetPosition)")
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)")
@@ -653,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 {
@@ -708,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
@@ -750,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
@@ -763,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
@@ -773,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...:
@@ -784,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...:
@@ -801,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 {
@@ -812,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]
@@ -828,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 {
@@ -856,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
+13 -3
View File
@@ -6,10 +6,10 @@
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).
/// Therfore, `proposedTargetPosition` can only be `FloatingPanelPosition.tip` or `FloatingPanelPosition.full`.
/// Therefore, `proposedTargetPosition` can only be `FloatingPanelPosition.tip` or `FloatingPanelPosition.full`.
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool
/// Returns a deceleration rate to calculate a target position projected a dragging momentum.
@@ -19,7 +19,7 @@ public protocol FloatingPanelBehavior {
/// Returns the progress to redirect to the previous position.
///
/// The progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates the floating panel is impossible to move to the next posiiton. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits.
/// The progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates the floating panel is impossible to move to the next position. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits.
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> CGFloat
/// Returns a UIViewPropertyAnimator object to project a floating panel to a position on finger up if the user dragged.
@@ -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()
+27 -19
View File
@@ -68,12 +68,16 @@ public enum FloatingPanelPosition: Int {
case half
case tip
case hidden
static var allCases: [FloatingPanelPosition] {
return [.full, .half, .tip, .hidden]
}
}
///
/// A container view controller to display a floating panel to present contents in parallel as a user wants.
///
public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate {
open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate {
/// Constants indicating how safe area insets are added to the adjusted content inset.
public enum ContentInsetAdjustmentBehavior: Int {
case always
@@ -145,7 +149,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
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()
@@ -181,7 +185,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
// MARK:- Overrides
/// Creates the view that the controller manages.
override public func loadView() {
open override func loadView() {
assert(self.storyboard == nil, "Storyboard isn't supported")
let view = FloatingPanelPassThroughView()
@@ -196,7 +200,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
self.view = view as UIView
}
public override func viewDidLayoutSubviews() {
open override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {}
else {
@@ -207,7 +211,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
}
public 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,21 +220,25 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
}
public 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)
}
public override func viewWillDisappear(_ animated: Bool) {
open override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
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 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
// Prevent an infinite loop on iOS 10: setUpLayout() -> viewDidLayoutSubviews() -> setUpLayout()
preSafeAreaInsets = safeAreaInsets
setUpLayout()
activateLayout()
switch contentInsetAdjustmentBehavior {
case .always:
@@ -282,7 +290,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
}
private func setUpLayout() {
private func activateLayout() {
// preserve the current content offset
let contentOffset = scrollView?.contentOffset
@@ -298,7 +306,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
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.
@@ -455,14 +463,14 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
@available(*, unavailable, renamed: "set(contentViewController:)")
public override func show(_ vc: UIViewController, sender: Any?) {
open override func show(_ vc: UIViewController, sender: Any?) {
if let target = self.parent?.targetViewController(forAction: #selector(UIViewController.show(_:sender:)), sender: sender) {
target.show(vc, sender: sender)
}
}
@available(*, unavailable, renamed: "set(contentViewController:)")
public override func showDetailViewController(_ vc: UIViewController, sender: Any?) {
open override func showDetailViewController(_ vc: UIViewController, sender: Any?) {
if let target = self.parent?.targetViewController(forAction: #selector(UIViewController.showDetailViewController(_:sender:)), sender: sender) {
target.showDetailViewController(vc, sender: sender)
}
@@ -513,7 +521,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// animation block.
public func updateLayout() {
reloadLayout(for: traitCollection)
setUpLayout()
activateLayout()
}
/// Returns the y-coordinate of the point at the origin of the surface view.
+33 -16
View File
@@ -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) {
+127 -66
View File
@@ -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,117 +83,154 @@ public class FloatingPanelSurfaceView: UIView {
/// The color of the surface border.
public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } }
public var backgroundView: UIView!
private var backgroundHeightConstraint: NSLayoutConstraint!
/// Offset of the container view from the top
public var containerTopInset: CGFloat = 0.0 { didSet {
setNeedsUpdateConstraints()
} }
private struct Default {
public static let grabberTopPadding: CGFloat = 6.0
}
/// The view presents an actual surface shape.
///
/// It renders the background color, border line and top rounded corners,
/// 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 let containerView: UIView = UIView()
@available(*, unavailable, renamed: "containerView")
public var backgroundView: UIView!
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 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 backgroundView = UIView()
addSubview(backgroundView)
self.backgroundView = backgroundView
backgroundView.translatesAutoresizingMaskIntoConstraints = false
backgroundHeightConstraint = backgroundView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0)
addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
backgroundView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
backgroundView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0),
backgroundView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0),
backgroundHeightConstraint,
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() {
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()
backgroundHeightConstraint.constant = bottomOverflow
}
public override func layoutSubviews() {
super.layoutSubviews()
log.debug("surface view frame = \(frame)")
updateLayers()
updateContentViewMask()
containerView.backgroundColor = color
contentView?.layer.borderColor = borderColor?.cgColor
contentView?.layer.borderWidth = borderWidth
contentView?.frame = bounds
updateShadow()
updateCornerRadius()
updateBorder()
}
private func updateLayers() {
backgroundView.backgroundColor = color
if cornerRadius != 0.0, backgroundView.layer.cornerRadius != cornerRadius {
backgroundView.layer.masksToBounds = true
backgroundView.layer.cornerRadius = cornerRadius
}
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
let contentView = contentView,
cornerRadius != 0.0,
contentView.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.
contentView.layer.masksToBounds = true
contentView.layer.cornerRadius = cornerRadius
contentView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
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.
}
}
private func updateBorder() {
containerView.layer.borderColor = borderColor?.cgColor
containerView.layer.borderWidth = borderWidth
}
func add(contentView: UIView) {
insertSubview(contentView, belowSubview: grabberHandle)
containerView.addSubview(contentView)
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.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
topConstraint,
leftConstraint,
rightConstraint,
heightConstraint,
])
self.contentViewTopConstraint = topConstraint
self.contentViewLeftConstraint = leftConstraint
self.contentViewRightConstraint = rightConstraint
self.contentViewHeightConstraint = heightConstraint
}
}
+12 -14
View File
@@ -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
}
}
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.5.0</string>
<string>1.6.2</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+6 -1
View File
@@ -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)
}
+12
View File
@@ -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
}
}
+22
View File
@@ -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
}
}
+43
View File
@@ -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)
}
}
+57 -26
View File
@@ -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
}
}
}
+18
View File
@@ -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
}
}
+27
View File
@@ -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")]
)
+9 -4
View File
@@ -68,7 +68,9 @@ Examples are here.
## Requirements
FloatingPanel is written in Swift. It can be built by Xcode 9.4.1 or later. Compatible with iOS 10.0+.
FloatingPanel is written in Swift 4.0+. It can be built by Xcode 9.4.1 or later. Compatible with iOS 10.0+.
✏️ The default Swift version is 4.0 because it avoids build errors with Carthage on each Xcode version from the source compatibility between Swift 4.0, 4.2 and 5.0.
## Installation
@@ -81,6 +83,8 @@ it, simply add the following line to your Podfile:
pod 'FloatingPanel'
```
✏️ To suppress "Swift Conversion" warnings in Xcode, please set a Swift version to `SWIFT_VERSION` for the project in your Podfile. It will be resolved in CocoaPods v1.7.0.
### Carthage
For [Carthage](https://github.com/Carthage/Carthage), add the following to your `Cartfile`:
@@ -142,7 +146,7 @@ self.present(fpc, animated: true, completion: nil)
You can show a floating panel over UINavigationController from the container view controllers as a modality of `.overCurrentContext` style.
NOTE: FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [FloatingPanelTransitioning](https://github.com/SCENEE/FloatingPanel/blob/master/Framework/Sources/FloatingPanelTransitioning.swift).
✏️ FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [FloatingPanelTransitioning](https://github.com/SCENEE/FloatingPanel/blob/master/Framework/Sources/FloatingPanelTransitioning.swift).
## View hierarchy
@@ -152,8 +156,9 @@ NOTE: FloatingPanelController has the custom presentation controller. If you wou
FloatingPanelController.view (FloatingPanelPassThroughView)
├─ .backdropView (FloatingPanelBackdropView)
└─ .surfaceView (FloatingPanelSurfaceView)
├─ .contentView == FloatingPanelController.contentViewController.view
└─ .grabberHandle (GrabberHandleView)
├─ .containerView (UIView)
└─ .contentView (FloatingPanelController.contentViewController.view)
└─ .grabberHandle (GrabberHandleView)
```
## Usage